Nuevo plugin Additionals 2.0.20
22
plugins/additionals/.codeclimate.yml
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
engines:
|
||||||
|
fixme:
|
||||||
|
enabled: false
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
rubocop:
|
||||||
|
enabled: true
|
||||||
|
duplication:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
languages:
|
||||||
|
- "ruby"
|
||||||
|
ratings:
|
||||||
|
paths:
|
||||||
|
- "**.css"
|
||||||
|
- "**.erb"
|
||||||
|
- "**.js"
|
||||||
|
- "**.rb"
|
||||||
|
exclude_paths:
|
||||||
|
- "test/"
|
||||||
|
- "assets/javascripts/"
|
||||||
|
- "assets/stylesheets/jquery.*"
|
17
plugins/additionals/.eslintrc.yml
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
jquery: true
|
||||||
|
extends: 'eslint:recommended'
|
||||||
|
rules:
|
||||||
|
indent:
|
||||||
|
- error
|
||||||
|
- 2
|
||||||
|
linebreak-style:
|
||||||
|
- error
|
||||||
|
- unix
|
||||||
|
quotes:
|
||||||
|
- error
|
||||||
|
- single
|
||||||
|
semi:
|
||||||
|
- error
|
||||||
|
- always
|
9
plugins/additionals/.gitignore
vendored
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
.DS_Store
|
||||||
|
.buildpath
|
||||||
|
coverage/
|
||||||
|
tmp/
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
docs/_build
|
||||||
|
docs/_static
|
||||||
|
docs/_templates
|
49
plugins/additionals/.rubocop.yml
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
Rails:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
TargetRubyVersion: 2.2
|
||||||
|
TargetRailsVersion: 5.2
|
||||||
|
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Max: 65
|
||||||
|
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Max: 60
|
||||||
|
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 20
|
||||||
|
|
||||||
|
Metrics/LineLength:
|
||||||
|
Max: 140
|
||||||
|
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Max: 60
|
||||||
|
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 25
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/CreateTableWithTimestamps:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# app/model/application_record.rb is missing in redmine, we can't use ApplicationRecord
|
||||||
|
Rails/ApplicationRecord:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/AutoResourceCleanup:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/ExpandPathArguments:
|
||||||
|
Enabled: false
|
39
plugins/additionals/.slim-lint.yml
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
linters:
|
||||||
|
LineLength:
|
||||||
|
max: 140
|
||||||
|
RuboCop:
|
||||||
|
ignored_cops:
|
||||||
|
- Layout/AlignArray
|
||||||
|
- Layout/AlignHash
|
||||||
|
- Layout/AlignParameters
|
||||||
|
- Layout/BlockEndNewline
|
||||||
|
- Layout/EmptyLineAfterGuardClause
|
||||||
|
- Layout/FirstParameterIndentation
|
||||||
|
- Layout/IndentArray
|
||||||
|
- Layout/IndentationConsistency
|
||||||
|
- Layout/IndentationWidth
|
||||||
|
- Layout/IndentHash
|
||||||
|
- Layout/MultilineArrayBraceLayout
|
||||||
|
- Layout/MultilineAssignmentLayout
|
||||||
|
- Layout/MultilineBlockLayout
|
||||||
|
- Layout/MultilineHashBraceLayout
|
||||||
|
- Layout/MultilineMethodCallBraceLayout
|
||||||
|
- Layout/MultilineMethodCallIndentation
|
||||||
|
- Layout/MultilineMethodDefinitionBraceLayout
|
||||||
|
- Layout/MultilineOperationIndentation
|
||||||
|
- Layout/TrailingBlankLines
|
||||||
|
- Layout/TrailingWhitespace
|
||||||
|
- Lint/BlockAlignment
|
||||||
|
- Lint/EndAlignment
|
||||||
|
- Lint/Void
|
||||||
|
- Metrics/BlockLength
|
||||||
|
- Metrics/BlockNesting
|
||||||
|
- Metrics/LineLength
|
||||||
|
- Naming/FileName
|
||||||
|
- Rails/OutputSafety
|
||||||
|
- Style/ConditionalAssignment
|
||||||
|
- Style/FrozenStringLiteralComment
|
||||||
|
- Style/IdenticalConditionalBranches
|
||||||
|
- Style/IfUnlessModifier
|
||||||
|
- Style/Next
|
||||||
|
- Style/WhileUntilModifier
|
163
plugins/additionals/.stylelintrc.json
Executable file
|
@ -0,0 +1,163 @@
|
||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-unknown": true,
|
||||||
|
"block-no-empty": true,
|
||||||
|
"color-no-invalid-hex": true,
|
||||||
|
"comment-no-empty": true,
|
||||||
|
"declaration-block-no-duplicate-properties": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"consecutive-duplicates-with-different-values"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"declaration-block-no-redundant-longhand-properties": true,
|
||||||
|
"declaration-block-no-shorthand-property-overrides": true,
|
||||||
|
"font-family-no-duplicate-names": true,
|
||||||
|
"font-family-no-missing-generic-family-keyword": true,
|
||||||
|
"function-calc-no-unspaced-operator": true,
|
||||||
|
"function-linear-gradient-no-nonstandard-direction": true,
|
||||||
|
"keyframe-declaration-no-important": true,
|
||||||
|
"media-feature-name-no-unknown": true,
|
||||||
|
"no-descending-specificity": true,
|
||||||
|
"no-duplicate-at-import-rules": true,
|
||||||
|
"no-duplicate-selectors": true,
|
||||||
|
"no-empty-source": true,
|
||||||
|
"no-extra-semicolons": true,
|
||||||
|
"no-invalid-double-slash-comments": true,
|
||||||
|
"property-no-unknown": true,
|
||||||
|
"selector-pseudo-class-no-unknown": true,
|
||||||
|
"selector-pseudo-element-no-unknown": true,
|
||||||
|
"selector-type-no-unknown": true,
|
||||||
|
"string-no-newline": true,
|
||||||
|
"unit-no-unknown": true,
|
||||||
|
"at-rule-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"blockless-after-same-name-blockless",
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"after-comment"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"at-rule-name-case": "lower",
|
||||||
|
"at-rule-name-space-after": "always-single-line",
|
||||||
|
"at-rule-semicolon-newline-after": "always",
|
||||||
|
"block-closing-brace-empty-line-before": "never",
|
||||||
|
"block-closing-brace-newline-after": "always",
|
||||||
|
"block-closing-brace-newline-before": "always-multi-line",
|
||||||
|
"block-closing-brace-space-before": "always-single-line",
|
||||||
|
"block-opening-brace-newline-after": "always-multi-line",
|
||||||
|
"block-opening-brace-space-after": "always-single-line",
|
||||||
|
"block-opening-brace-space-before": "always",
|
||||||
|
"color-hex-case": "lower",
|
||||||
|
"color-hex-length": "short",
|
||||||
|
"comment-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"stylelint-commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"comment-whitespace-inside": "always",
|
||||||
|
"custom-property-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"after-custom-property",
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"after-comment",
|
||||||
|
"inside-single-line-block"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"declaration-bang-space-after": "never",
|
||||||
|
"declaration-bang-space-before": "always",
|
||||||
|
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||||
|
"declaration-block-semicolon-space-after": "always-single-line",
|
||||||
|
"declaration-block-semicolon-space-before": "never",
|
||||||
|
"declaration-block-single-line-max-declarations": 1,
|
||||||
|
"declaration-block-trailing-semicolon": "always",
|
||||||
|
"declaration-colon-newline-after": "always-multi-line",
|
||||||
|
"declaration-colon-space-after": "always-single-line",
|
||||||
|
"declaration-colon-space-before": "never",
|
||||||
|
"declaration-empty-line-before": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"after-declaration",
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"after-comment",
|
||||||
|
"inside-single-line-block"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"function-comma-newline-after": "always-multi-line",
|
||||||
|
"function-comma-space-after": "always-single-line",
|
||||||
|
"function-comma-space-before": "never",
|
||||||
|
"function-max-empty-lines": 0,
|
||||||
|
"function-name-case": "lower",
|
||||||
|
"function-parentheses-newline-inside": "always-multi-line",
|
||||||
|
"function-parentheses-space-inside": "never-single-line",
|
||||||
|
"function-whitespace-after": "always",
|
||||||
|
"indentation": 2,
|
||||||
|
"length-zero-no-unit": true,
|
||||||
|
"max-empty-lines": 1,
|
||||||
|
"media-feature-colon-space-after": "always",
|
||||||
|
"media-feature-colon-space-before": "never",
|
||||||
|
"media-feature-name-case": "lower",
|
||||||
|
"media-feature-parentheses-space-inside": "never",
|
||||||
|
"media-feature-range-operator-space-after": "always",
|
||||||
|
"media-feature-range-operator-space-before": "always",
|
||||||
|
"media-query-list-comma-newline-after": "always-multi-line",
|
||||||
|
"media-query-list-comma-space-after": "always-single-line",
|
||||||
|
"media-query-list-comma-space-before": "never",
|
||||||
|
"no-eol-whitespace": true,
|
||||||
|
"no-missing-end-of-source-newline": true,
|
||||||
|
"number-leading-zero": "always",
|
||||||
|
"number-no-trailing-zeros": true,
|
||||||
|
"property-case": "lower",
|
||||||
|
"rule-empty-line-before": [
|
||||||
|
"always-multi-line",
|
||||||
|
{
|
||||||
|
"except": [
|
||||||
|
"first-nested"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"after-comment"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selector-attribute-brackets-space-inside": "never",
|
||||||
|
"selector-attribute-operator-space-after": "never",
|
||||||
|
"selector-attribute-operator-space-before": "never",
|
||||||
|
"selector-combinator-space-after": "always",
|
||||||
|
"selector-combinator-space-before": "always",
|
||||||
|
"selector-descendant-combinator-no-non-space": true,
|
||||||
|
"selector-list-comma-newline-after": "always",
|
||||||
|
"selector-list-comma-space-before": "never",
|
||||||
|
"selector-max-empty-lines": 0,
|
||||||
|
"selector-pseudo-class-case": "lower",
|
||||||
|
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||||
|
"selector-pseudo-element-case": "lower",
|
||||||
|
"selector-pseudo-element-colon-notation": "double",
|
||||||
|
"selector-type-case": "lower",
|
||||||
|
"unit-case": "lower",
|
||||||
|
"value-list-comma-newline-after": "always-multi-line",
|
||||||
|
"value-list-comma-space-after": "always-single-line",
|
||||||
|
"value-list-comma-space-before": "never",
|
||||||
|
"value-list-max-empty-lines": 0
|
||||||
|
}
|
||||||
|
}
|
49
plugins/additionals/.travis.yml
Executable file
|
@ -0,0 +1,49 @@
|
||||||
|
language: ruby
|
||||||
|
|
||||||
|
rvm:
|
||||||
|
- 2.5.3
|
||||||
|
- 2.4.5
|
||||||
|
- 2.3.8
|
||||||
|
|
||||||
|
env:
|
||||||
|
- REDMINE_VER=4.0-stable DB=postgresql
|
||||||
|
- REDMINE_VER=3.4-stable DB=postgresql
|
||||||
|
- REDMINE_VER=4.0-stable DB=mysql
|
||||||
|
- REDMINE_VER=3.4-stable DB=mysql
|
||||||
|
|
||||||
|
sudo: true
|
||||||
|
|
||||||
|
addons:
|
||||||
|
postgresql: "9.6"
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- mysql-5.7-trusty
|
||||||
|
packages:
|
||||||
|
- mysql-server
|
||||||
|
- mysql-client
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- export PLUGIN_NAME=additionals
|
||||||
|
- export REDMINE_GIT_REPO=git://github.com/redmine/redmine.git
|
||||||
|
- export REDMINE_PATH=$HOME/redmine
|
||||||
|
- export BUNDLE_GEMFILE=$REDMINE_PATH/Gemfile
|
||||||
|
- export RAILS_ENV=test
|
||||||
|
- git clone $REDMINE_GIT_REPO $REDMINE_PATH
|
||||||
|
- cd $REDMINE_PATH
|
||||||
|
- if [[ "$REDMINE_VER" != "master" ]]; then git checkout -b $REDMINE_VER origin/$REDMINE_VER; fi
|
||||||
|
- ln -s $TRAVIS_BUILD_DIR $REDMINE_PATH/plugins/$PLUGIN_NAME
|
||||||
|
- cp $TRAVIS_BUILD_DIR/test/support/additional_environment.rb $REDMINE_PATH/config/
|
||||||
|
- cp $TRAVIS_BUILD_DIR/test/support/database-$DB-travis.yml $REDMINE_PATH/config/database.yml
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- if [[ "$DB" == "mysql" ]]; then mysql -e "use mysql; update user set authentication_string=PASSWORD('travis_ci_test') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"; fi
|
||||||
|
- if [[ "$DB" == "mysql" ]]; then mysql_upgrade -ptravis_ci_test; fi
|
||||||
|
- if [[ "$DB" == "mysql" ]]; then service mysql restart; fi
|
||||||
|
- bundle exec rake db:create db:migrate redmine:plugins:migrate
|
||||||
|
|
||||||
|
script:
|
||||||
|
- export SKIP_COVERAGE=1
|
||||||
|
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:units NAME=$PLUGIN_NAME; fi
|
||||||
|
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:functionals NAME=$PLUGIN_NAME; fi
|
||||||
|
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:integration NAME=$PLUGIN_NAME; fi
|
||||||
|
- if [[ "$REDMINE_VER" != "master" ]]; then bundle exec rake redmine:plugins:test NAME=$PLUGIN_NAME RUBYOPT="-W0"; fi
|
366
plugins/additionals/CHANGELOG.rst
Executable file
|
@ -0,0 +1,366 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
2.0.20
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- support single process rake installation #40
|
||||||
|
- FontAwesome 5.8.0 support
|
||||||
|
|
||||||
|
2.0.19
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- mermaid 8.0.0 support
|
||||||
|
- FontAwesome 5.7.1 support
|
||||||
|
- fixed close issue without permission
|
||||||
|
- create correct journal entry if issue status changed from sidebar #37
|
||||||
|
- create correct journal entry if issue has been 'assigned to me' from sidebar
|
||||||
|
|
||||||
|
2.0.18
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- Performance improvement (#36)
|
||||||
|
- FontAwesome 5.6.3 support
|
||||||
|
- Fix problem from migrating from Redmine 3.x to Redmine 4 with lost settings
|
||||||
|
|
||||||
|
2.0.17
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- Fix bug with undefined constant for tags
|
||||||
|
- add possibility to use custom date with date macro
|
||||||
|
- FontAwesome 5.6.0 support
|
||||||
|
|
||||||
|
2.0.16
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- CSS fix for project macro
|
||||||
|
- More compatibility for autocomplete_users with other plugins (like redmine_contacts_helpdesk)
|
||||||
|
- Compatibility to wiking plugin (macro list)
|
||||||
|
- Add Spanish translation, thanks to @dktcoding!
|
||||||
|
- Wiki button for available macros
|
||||||
|
- replace permission hide-in-memberbox with "hide" as role setting - check your roles, if you used this permission!
|
||||||
|
- replace multiple current_date macros with one macro called date
|
||||||
|
- cleanup macros: if no data exists, macros is displayed instead of data
|
||||||
|
(before some macros used error messages other hide message at all)
|
||||||
|
|
||||||
|
2.0.15
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- FontAwesome 5.5.0 support
|
||||||
|
- Usability improvement for change author in issue formular
|
||||||
|
- options and permission for issue requires timelog to use status
|
||||||
|
- New option to freeze issues on close. With permission "edit closed issue" user can break this rule.
|
||||||
|
- Fix problem with help menu and other redmine plugins (compatibility problem with other plugins)
|
||||||
|
- Fix problem with disabled users and changing author for issues
|
||||||
|
|
||||||
|
2.0.14
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- Change status is now compatible with redmine_agile
|
||||||
|
- Do not show sidebar for changing status, if edit_closed_issues permission is missing
|
||||||
|
- FontAwesome 5.3.1 support
|
||||||
|
- compatibility with plugin redmine_sudo and redmine_base_deface
|
||||||
|
- FontAwesome wiki macro has been added (called fa)
|
||||||
|
- Redmine.org issue and wiki page macro has been added
|
||||||
|
- Show macro list to all logged users at /help/macros
|
||||||
|
- Help menu, with more links to Redmine help pages (which can be used with other plugins, to assign additional entries)
|
||||||
|
|
||||||
|
2.0.13
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- FontAwesome 5.2.0 support
|
||||||
|
- smiley support for markdown text_formatting
|
||||||
|
- new_issue macro with i18n support
|
||||||
|
- updated bootstrap-datepicker to v1.8.0
|
||||||
|
- updated d3plus to v2.0.0-alpha.17
|
||||||
|
- ruby 2.2.0 is required
|
||||||
|
- Redmine 3.4 is required
|
||||||
|
- support sidebar with non default wiki titles (thanks to @danielvijge)
|
||||||
|
|
||||||
|
2.0.12
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- Provide d3 loader function
|
||||||
|
- More robust code for dealing with finding data
|
||||||
|
|
||||||
|
2.0.11
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- i18n methods
|
||||||
|
- FontAwesome 5.0.13 support
|
||||||
|
|
||||||
|
2.0.10
|
||||||
|
++++++
|
||||||
|
|
||||||
|
- Remove bootstrap library
|
||||||
|
- compatibility with https://www.redmine.org/plugins/issue_id
|
||||||
|
- bug fix: issue and user macro uses absolute url in mailer notification
|
||||||
|
- Updated marmaid library to version 8.0.0-rc8
|
||||||
|
- Updated d3 library to 3.5.17
|
||||||
|
- Updated nvd3 library to latest 1.8.6
|
||||||
|
- FontAwesome 5.0.12 support
|
||||||
|
- Set default values for ui-tooltip css class
|
||||||
|
- ZeroClipboard updated to 2.3.0
|
||||||
|
|
||||||
|
2.0.9
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Updated bootstrap library to 4.0.0
|
||||||
|
- Drop angular_gantt library
|
||||||
|
- enables deface overwrite directory for all installed plugins (not only additionals)
|
||||||
|
- Updated d3plus to version v2.0.0-alpha.16
|
||||||
|
- add "Assign to me" to issues
|
||||||
|
- add "Status on sidebar" for issues
|
||||||
|
- add link to create new issue on user profile
|
||||||
|
- FontAwesome 5.0.8 support
|
||||||
|
- Add marmaid library
|
||||||
|
|
||||||
|
2.0.8
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Provide XLSX helper (and drop XLS helper)
|
||||||
|
- FontAwesome 5.0.6 support
|
||||||
|
- add list support for rake task setting_set
|
||||||
|
|
||||||
|
2.0.7
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- FontAwesome 5.0.2 support
|
||||||
|
- Switching to SLIM template engine
|
||||||
|
|
||||||
|
2.0.6
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- add rake tasks: drop_settings, setting_get and setting_set
|
||||||
|
- Updated nvd3 library to 1.8.6
|
||||||
|
- Updated angularjs libraries to v2.0.0-rc.1
|
||||||
|
- Wiki iframe macro integration has been added
|
||||||
|
|
||||||
|
2.0.5
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Redmine 3.4 bug fixes
|
||||||
|
- Helper function fa_icon renamed to font_awesome_icon because of conflicts with redmine_bootstrap_kit
|
||||||
|
- Cleanups deface overwrites
|
||||||
|
- add hook for user show
|
||||||
|
- Traditional Chinese support has been added (thanks to @archonwang)
|
||||||
|
- Wiki macro for weather with meteoblue has been added
|
||||||
|
- Wiki macro for google maps has been added
|
||||||
|
- Wiki macro for issues now supports display a comment and detect issue id and comment id from URL
|
||||||
|
|
||||||
|
2.0.4
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Add group_users macro
|
||||||
|
- Fix bug with REST-API and assigned_id for issues
|
||||||
|
- Use user name setting for sort order in macros
|
||||||
|
- Add invisible_captcha spam protection on registration form
|
||||||
|
|
||||||
|
2.0.3
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Allow remove watchers without re-adding it (only if author or assigned_user changed)
|
||||||
|
- Fix sort order of users for change author
|
||||||
|
- Add uninstall documentation
|
||||||
|
- Add option to disable autowatch issue at user level
|
||||||
|
- Fixed bug with recurring_tasks plugin and autowatch issues
|
||||||
|
- Add more unit tests
|
||||||
|
|
||||||
|
2.0.2
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Add option to add involved issue users automatically
|
||||||
|
- Add change issue author feature
|
||||||
|
- Fixed bug with Redmine 3.4.x and default assignee settings
|
||||||
|
- Refactoring patch include and wiki macros
|
||||||
|
|
||||||
|
2.0.1
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Simplified Chinese support has been added (thanks to @archonwang)
|
||||||
|
- Helper function fa_icon has been added
|
||||||
|
- Help menu item and MyPage menu item does not require application server restart anymore
|
||||||
|
- Redmine 3.4.x compatibility
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Redmine Tweaks has been renamed to additionals, because to resolve loading order problem of Redmine plugins
|
||||||
|
- Merge common_libraries plugin into additionals plugin
|
||||||
|
- Fontawesome support
|
||||||
|
- Redmine 3.0.x required
|
||||||
|
|
||||||
|
1.0.3
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- TradingView macro support
|
||||||
|
- CryptoCompare macro support
|
||||||
|
- Reddit macro support
|
||||||
|
- Twitter macro improved with prefix image
|
||||||
|
|
||||||
|
1.0.2
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Smiley/Emoji legacy support
|
||||||
|
|
||||||
|
1.0.1
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Coding standard cleanups
|
||||||
|
- ruby 2.1.5 required or newer
|
||||||
|
- version bump
|
||||||
|
|
||||||
|
1.0.0
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- user group support for issue auto assign
|
||||||
|
- optimize deface overwrite path
|
||||||
|
- drop remove latest projects support (because Redmine 3.2 has dropped latest projects)
|
||||||
|
- add permission for log time on closed issues - make sure you adjust our permissions!
|
||||||
|
- code cleanups and bug fixes
|
||||||
|
- restructure settings
|
||||||
|
- wiki pdf settings has been added
|
||||||
|
- updated documentation
|
||||||
|
|
||||||
|
0.5.8
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Fixed top menu items permissions for anonymous and non member #29
|
||||||
|
- Fixed bug with overwriting application handler, which cases problem with other plugins
|
||||||
|
- Tweaks link added to admin menu
|
||||||
|
- replaced user macro with {{user}} syntax (old syntax user#id is not supported anymore)
|
||||||
|
- more formats for user macro and avatar support
|
||||||
|
- rename list_users to members
|
||||||
|
- rename list_projects to projects
|
||||||
|
- new documentation on https://redmine-tweaks.readthedocs.io
|
||||||
|
- updated bootstrap-datepicker and fixed zh locale problem
|
||||||
|
- html validation error has been fixed
|
||||||
|
- remove garfield support (because there is no image source server available)
|
||||||
|
- slideshare wiki macro has been added
|
||||||
|
- issue wiki macro has been added
|
||||||
|
- autoassign issue if no assignee is selected
|
||||||
|
- n+1 query optimization
|
||||||
|
|
||||||
|
0.5.7
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Custom source URL for Garfield source
|
||||||
|
- Wiki footer bug fixed with missing line break at the end of page
|
||||||
|
- date period support for calendar macro
|
||||||
|
- Code cleanups
|
||||||
|
|
||||||
|
0.5.6
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Redmine 3.2.x compatibility
|
||||||
|
- user macro has been added (user#1 or user:admin)
|
||||||
|
- recently_updated has been added
|
||||||
|
- lastupdated_by has been added
|
||||||
|
- lastupdated_at has been added
|
||||||
|
- calendar macro support
|
||||||
|
- NoReferrer support has been added
|
||||||
|
- system information uptime and uname have been added
|
||||||
|
- twitter macro support
|
||||||
|
- gist macro support
|
||||||
|
- vimeo macro support
|
||||||
|
|
||||||
|
0.5.5
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- dependency with deface (used to overview views)
|
||||||
|
- fixed garfield caching macro problem
|
||||||
|
- you can add content to overview page now (top and bottom)
|
||||||
|
- some content and view optimization (removed wiki_sidebar compatibility problems with other Redmine plugins)
|
||||||
|
- Code cleanups and refactoring
|
||||||
|
|
||||||
|
0.5.4
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- issue rule added for closing issue with open sub issues
|
||||||
|
- issue rule added for status change
|
||||||
|
- issue rule added for assigned_to change
|
||||||
|
|
||||||
|
0.5.3
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Redmine 3.0.x and 3.1.x supported
|
||||||
|
- "New issue" link with list_projects macro
|
||||||
|
- Parameter syntax changed for list_users and list_projects macros (sorry for that)
|
||||||
|
|
||||||
|
0.5.2
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- "Edit closed issue" permission has been added
|
||||||
|
- Permissions supported for top menu items
|
||||||
|
|
||||||
|
0.5.1
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- "Hide role in memberbox" has been added
|
||||||
|
|
||||||
|
0.5.0
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Redmine 2.6.x compatibility
|
||||||
|
- URL fixes
|
||||||
|
- Garfield macro has been added
|
||||||
|
|
||||||
|
0.4.9
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- added overview text field
|
||||||
|
- fix style for "goto top"
|
||||||
|
- added macro overview help page
|
||||||
|
- fix compatibility problems with sidebar and other plugins
|
||||||
|
|
||||||
|
0.4.8
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- added youtube macro
|
||||||
|
- project guide subject can be defined for project overview page
|
||||||
|
|
||||||
|
0.4.7
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- added jump to top link
|
||||||
|
- top menu item configuration has been added
|
||||||
|
- footer configuration (e.g. for imprint url) has been added
|
||||||
|
|
||||||
|
0.4.6
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- initialize plugins settings now works with other plugins
|
||||||
|
|
||||||
|
0.4.5
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- option to remove help menu item
|
||||||
|
- Redmine 2.4.1 required
|
||||||
|
|
||||||
|
0.4.4
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- installation error fixed
|
||||||
|
- description update for link handling
|
||||||
|
- help url now opens in new windows
|
||||||
|
- sidebar error has been fixed, if no wiki page already exist
|
||||||
|
|
||||||
|
0.4.3
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- global gantt and calendar bug fix
|
||||||
|
|
||||||
|
0.4.2
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- no requirements of Wiki extensions plugin anymore
|
||||||
|
|
||||||
|
0.4.1
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- Fix problem with my page permission
|
||||||
|
|
||||||
|
0.4.0
|
||||||
|
+++++
|
||||||
|
|
||||||
|
- First public release
|
16
plugins/additionals/Gemfile
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
gem 'deface', '>= 1.1.0'
|
||||||
|
gem 'gemoji', '~> 3.0.0'
|
||||||
|
gem 'invisible_captcha'
|
||||||
|
gem 'slim-rails'
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'brakeman', require: false
|
||||||
|
gem 'rubocop', require: false
|
||||||
|
gem 'slim_lint', require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'awesome_print', require: 'ap' # https://github.com/awesome-print/awesome_print
|
||||||
|
gem 'better_errors' # https://github.com/BetterErrors/better_errors
|
||||||
|
gem 'binding_of_caller' # better output of with variables for better_errors
|
||||||
|
end
|
339
plugins/additionals/LICENSE
Executable file
|
@ -0,0 +1,339 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
1
plugins/additionals/README.rst
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
docs/index.rst
|
39
plugins/additionals/app/controllers/additionals_assign_to_me_controller.rb
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
class AdditionalsAssignToMeController < ApplicationController
|
||||||
|
before_action :find_issue
|
||||||
|
helper :additionals_issues
|
||||||
|
|
||||||
|
def update
|
||||||
|
old_user = @issue.assigned_to
|
||||||
|
user_in_project = @project.assignable_users.detect { |u| u.id == User.current.id }
|
||||||
|
|
||||||
|
if old_user == User.current || user_in_project.nil?
|
||||||
|
redirect_to(issue_path(@issue))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@issue.init_journal(User.current)
|
||||||
|
@issue.assigned_to = User.current
|
||||||
|
|
||||||
|
if !@issue.save || old_user == @issue.assigned_to
|
||||||
|
flash[:error] = l(:error_issues_could_not_be_assigned_to_me)
|
||||||
|
return redirect_to(issue_path(@issue))
|
||||||
|
end
|
||||||
|
|
||||||
|
last_journal = @issue.journals.visible.order(:created_on).last
|
||||||
|
return redirect_to(issue_path(@issue)) if last_journal.nil?
|
||||||
|
|
||||||
|
last_journal = @issue.journals.visible.order(:created_on).last
|
||||||
|
redirect_to "#{issue_path(@issue)}#change-#{last_journal.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_issue
|
||||||
|
@issue = Issue.find(params[:issue_id])
|
||||||
|
raise Unauthorized unless @issue.visible? && @issue.editable?
|
||||||
|
|
||||||
|
@project = @issue.project
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
end
|
43
plugins/additionals/app/controllers/additionals_change_status_controller.rb
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
class AdditionalsChangeStatusController < ApplicationController
|
||||||
|
before_action :find_issue
|
||||||
|
helper :additionals_issues
|
||||||
|
|
||||||
|
def update
|
||||||
|
issue_old_status_id = @issue.status.id
|
||||||
|
issue_old_user = @issue.assigned_to
|
||||||
|
new_status_id = params[:new_status_id].to_i
|
||||||
|
allowed_status = @issue.sidbar_change_status_allowed_to(User.current, new_status_id)
|
||||||
|
|
||||||
|
if new_status_id < 1 || @issue.status_id == new_status_id || allowed_status.nil?
|
||||||
|
redirect_to(issue_path(@issue))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@issue.init_journal(User.current)
|
||||||
|
@issue.status_id = new_status_id
|
||||||
|
@issue.assigned_to = User.current if @issue.status_x_affected?(new_status_id) && issue_old_user != User.current
|
||||||
|
|
||||||
|
if !@issue.save || issue_old_status_id == @issue.status_id
|
||||||
|
flash[:error] = l(:error_issue_status_could_not_changed)
|
||||||
|
return redirect_to(issue_path(@issue))
|
||||||
|
end
|
||||||
|
|
||||||
|
last_journal = @issue.journals.visible.order(:created_on).last
|
||||||
|
|
||||||
|
return redirect_to(issue_path(@issue)) if last_journal.nil?
|
||||||
|
|
||||||
|
last_journal = @issue.journals.visible.order(:created_on).last
|
||||||
|
redirect_to "#{issue_path(@issue)}#change-#{last_journal.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_issue
|
||||||
|
@issue = Issue.find(params[:issue_id])
|
||||||
|
raise Unauthorized unless @issue.visible? && @issue.editable?
|
||||||
|
|
||||||
|
@project = @issue.project
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
end
|
7
plugins/additionals/app/controllers/additionals_macros_controller.rb
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
class AdditionalsMacrosController < ApplicationController
|
||||||
|
before_action :require_login
|
||||||
|
|
||||||
|
def show
|
||||||
|
@available_macros = AdditionalsMacro.all
|
||||||
|
end
|
||||||
|
end
|
46
plugins/additionals/app/helpers/additionals_fontawesome_helper.rb
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
module AdditionalsFontawesomeHelper
|
||||||
|
def fontawesome_info_url
|
||||||
|
s = []
|
||||||
|
s << l(:label_set_icon_from)
|
||||||
|
s << link_to('https://fontawesome.com/icons?m=free', 'https://fontawesome.com/icons?m=free', class: 'external')
|
||||||
|
safe_join(s, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
# name = TYPE-FA_NAME, eg. fas_car
|
||||||
|
# fas_cloud-upload-alt
|
||||||
|
# far_id-card
|
||||||
|
# fab_font-awesome
|
||||||
|
# options = class
|
||||||
|
# pre_text
|
||||||
|
# post_text
|
||||||
|
# title
|
||||||
|
def font_awesome_icon(name, options = {})
|
||||||
|
info = AdditionalsFontAwesome.value_info(name)
|
||||||
|
return '' if info.blank?
|
||||||
|
|
||||||
|
post_text = ''
|
||||||
|
options['aria-hidden'] = 'true'
|
||||||
|
options[:class] = if options[:class].present?
|
||||||
|
info[:classes] + ' ' + options[:class]
|
||||||
|
else
|
||||||
|
info[:classes]
|
||||||
|
end
|
||||||
|
|
||||||
|
s = []
|
||||||
|
if options[:pre_text].present?
|
||||||
|
s << options[:pre_text]
|
||||||
|
s << ' '
|
||||||
|
options.delete(:pre_text)
|
||||||
|
end
|
||||||
|
if options[:post_text].present?
|
||||||
|
post_text = options[:post_text]
|
||||||
|
options.delete(:post_text)
|
||||||
|
end
|
||||||
|
s << content_tag('span', '', options)
|
||||||
|
if post_text.present?
|
||||||
|
s << ' '
|
||||||
|
s << post_text
|
||||||
|
end
|
||||||
|
safe_join(s)
|
||||||
|
end
|
||||||
|
end
|
24
plugins/additionals/app/helpers/additionals_issues_helper.rb
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
module AdditionalsIssuesHelper
|
||||||
|
def issue_author_options_for_select(project, issue = nil)
|
||||||
|
authors = project.users.sorted
|
||||||
|
s = []
|
||||||
|
return s unless authors.any?
|
||||||
|
|
||||||
|
s << content_tag('option', "<< #{l(:label_me)} >>", value: User.current.id) if authors.include?(User.current)
|
||||||
|
|
||||||
|
if issue.nil?
|
||||||
|
s << options_from_collection_for_select(authors, 'id', 'name')
|
||||||
|
else
|
||||||
|
s << content_tag('option', issue.author, value: issue.author_id, selected: true) if issue.author && !authors.include?(issue.author)
|
||||||
|
s << options_from_collection_for_select(authors, 'id', 'name', issue.author_id)
|
||||||
|
end
|
||||||
|
safe_join(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_issue_change_author?(issue)
|
||||||
|
if issue.new_record? && User.current.allowed_to?(:change_new_issue_author, issue.project) ||
|
||||||
|
issue.persisted? && User.current.allowed_to?(:edit_issue_author, issue.project)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
214
plugins/additionals/app/helpers/additionals_menu_helper.rb
Executable file
|
@ -0,0 +1,214 @@
|
||||||
|
module AdditionalsMenuHelper
|
||||||
|
def additionals_top_menu_setup
|
||||||
|
return unless User.current.try(:hrm_user_type_id).nil?
|
||||||
|
|
||||||
|
if Additionals.setting?(:remove_mypage)
|
||||||
|
Redmine::MenuManager.map(:top_menu).delete(:my_page) if Redmine::MenuManager.map(:top_menu).exists?(:my_page)
|
||||||
|
else
|
||||||
|
handle_top_menu_item(:my_page, url: my_page_path, after: :home, if: proc { User.current.logged? })
|
||||||
|
end
|
||||||
|
|
||||||
|
if Additionals.setting?(:remove_help)
|
||||||
|
Redmine::MenuManager.map(:top_menu).delete(:help) if Redmine::MenuManager.map(:top_menu).exists?(:help)
|
||||||
|
elsif User.current.logged?
|
||||||
|
handle_top_menu_item(:help, url: '#', symbol: 'fas_question', last: true)
|
||||||
|
@additionals_help_items = additionals_help_menu_items
|
||||||
|
else
|
||||||
|
handle_top_menu_item(:help, url: Redmine::Info.help_url, symbol: 'fas_question', last: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_top_menu_item(menu_name, item)
|
||||||
|
Redmine::MenuManager.map(:top_menu).delete(menu_name.to_sym) if Redmine::MenuManager.map(:top_menu).exists?(menu_name.to_sym)
|
||||||
|
|
||||||
|
html_options = {}
|
||||||
|
html_options[:class] = 'external' if item[:url].include? '://'
|
||||||
|
html_options[:title] = item[:title] if item[:title].present?
|
||||||
|
|
||||||
|
menu_options = { parent: item[:parent].present? ? item[:parent].to_sym : nil,
|
||||||
|
html: html_options }
|
||||||
|
|
||||||
|
menu_options[:if] = menu_options[:if] if menu_options[:if].present?
|
||||||
|
|
||||||
|
menu_options[:caption] = if item[:symbol].present? && item[:name].present?
|
||||||
|
font_awesome_icon(item[:symbol], post_text: item[:name])
|
||||||
|
elsif item[:symbol].present?
|
||||||
|
font_awesome_icon(item[:symbol])
|
||||||
|
elsif item[:name].present?
|
||||||
|
item[:name].to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if item[:last].present? && item[:last]
|
||||||
|
menu_options[:last] = true
|
||||||
|
elsif item[:before].present?
|
||||||
|
menu_options[:before] = item[:before]
|
||||||
|
elsif item[:after].present?
|
||||||
|
menu_options[:after] = item[:after]
|
||||||
|
else
|
||||||
|
menu_options[:before] = :help
|
||||||
|
end
|
||||||
|
|
||||||
|
Redmine::MenuManager.map(:top_menu).push(menu_name, item[:url], menu_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_custom_top_menu_item
|
||||||
|
items = additionals_build_custom_items
|
||||||
|
return if items.empty?
|
||||||
|
|
||||||
|
user_roles = Role.givable
|
||||||
|
.joins(:members).where(members: { user_id: User.current.id })
|
||||||
|
.joins(members: :project).where(projects: { status: Project::STATUS_ACTIVE })
|
||||||
|
.distinct
|
||||||
|
.reorder(nil)
|
||||||
|
.pluck(:id)
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
additionals_custom_top_menu_item(item, user_roles)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_build_custom_items
|
||||||
|
items = []
|
||||||
|
Additionals::MAX_CUSTOM_MENU_ITEMS.times do |num|
|
||||||
|
menu_name = "custom_menu#{num}"
|
||||||
|
item = { menu_name: menu_name.to_sym,
|
||||||
|
url: Additionals.settings[menu_name + '_url'],
|
||||||
|
name: Additionals.settings[menu_name + '_name'],
|
||||||
|
title: Additionals.settings[menu_name + '_title'],
|
||||||
|
roles: Additionals.settings[menu_name + '_roles'] }
|
||||||
|
|
||||||
|
if item[:name].present? && item[:url].present? && item[:roles].present?
|
||||||
|
items << item
|
||||||
|
elsif Redmine::MenuManager.map(:top_menu).exists?(item[:menu_name])
|
||||||
|
Redmine::MenuManager.map(:top_menu).delete(item[:menu_name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
items
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_custom_top_menu_item(item, user_roles)
|
||||||
|
show_entry = false
|
||||||
|
item[:roles].each do |role|
|
||||||
|
if user_roles.empty? && role.to_i == Role::BUILTIN_ANONYMOUS
|
||||||
|
show_entry = true
|
||||||
|
break
|
||||||
|
elsif User.current.logged? && role.to_i == Role::BUILTIN_NON_MEMBER
|
||||||
|
# if user is logged in and non_member is active in item,
|
||||||
|
# always show it
|
||||||
|
show_entry = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
user_roles.each do |user_role|
|
||||||
|
if role.to_i == user_role
|
||||||
|
show_entry = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break if show_entry == true
|
||||||
|
end
|
||||||
|
|
||||||
|
if show_entry
|
||||||
|
handle_top_menu_item(item[:menu_name], item)
|
||||||
|
elsif Redmine::MenuManager.map(:top_menu).exists?(item[:menu_name])
|
||||||
|
Redmine::MenuManager.map(:top_menu).delete(item[:menu_name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def addtionals_help_plugin_items
|
||||||
|
user_items = [{ title: 'Redmine Guide', url: Redmine::Info.help_url },
|
||||||
|
{ title: "Redmine #{l(:label_macro_plural)}", url: additionals_macros_path }]
|
||||||
|
|
||||||
|
admin_items = [{ title: 'Additionals', url: 'https://additionals.readthedocs.io/en/latest/manual/', manual: true },
|
||||||
|
{ title: 'Redmine Changelog', url: 'https://www.redmine.org/projects/redmine/wiki/Changelog_3_4' },
|
||||||
|
{ title: 'Redmine Upgrade', url: 'https://www.redmine.org/projects/redmine/wiki/RedmineUpgrade' },
|
||||||
|
{ title: 'Redmine Security Advisories', url: 'https://www.redmine.org/projects/redmine/wiki/Security_Advisories' }]
|
||||||
|
|
||||||
|
Redmine::Plugin.all.each do |plugin|
|
||||||
|
next if plugin.id == :additionals
|
||||||
|
|
||||||
|
plugin_item_base = nil
|
||||||
|
|
||||||
|
begin
|
||||||
|
plugin_item_base = plugin.id.to_s.camelize.constantize
|
||||||
|
rescue LoadError
|
||||||
|
Rails.logger.debug "Ignore plugin #{plugin.id} for help integration"
|
||||||
|
rescue StandardError => e
|
||||||
|
raise e unless e.class.to_s == 'NameError'
|
||||||
|
end
|
||||||
|
|
||||||
|
plugin_item = plugin_item_base.try(:additionals_help_items) unless plugin_item_base.nil?
|
||||||
|
plugin_item = additionals_help_items_fallbacks(plugin.id) if plugin_item.nil?
|
||||||
|
|
||||||
|
next if plugin_item.nil?
|
||||||
|
|
||||||
|
plugin_item.each do |temp_item|
|
||||||
|
u_items = if !temp_item[:manual].nil? && temp_item[:manual]
|
||||||
|
{ title: "#{temp_item[:title]} #{l(:label_help_manual)}", url: temp_item[:url] }
|
||||||
|
else
|
||||||
|
{ title: temp_item[:title], url: temp_item[:url] }
|
||||||
|
end
|
||||||
|
|
||||||
|
if !temp_item[:admin].nil? && temp_item[:admin]
|
||||||
|
admin_items << u_items
|
||||||
|
else
|
||||||
|
user_items << u_items
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{ user: user_items, admin: admin_items }
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_help_menu_items
|
||||||
|
plugin_items = addtionals_help_plugin_items
|
||||||
|
pages = plugin_items[:user].sort_by { |k| k[:title] }
|
||||||
|
|
||||||
|
if User.current.admin?
|
||||||
|
pages << { title: '-' }
|
||||||
|
pages += plugin_items[:admin].sort_by { |k| k[:title] }
|
||||||
|
end
|
||||||
|
|
||||||
|
s = []
|
||||||
|
pages.each_with_index do |item, idx|
|
||||||
|
s << if item[:title] == '-'
|
||||||
|
content_tag(:li, tag(:hr))
|
||||||
|
else
|
||||||
|
html_options = { class: 'help_item_' + idx.to_s }
|
||||||
|
if item[:url].include? '://'
|
||||||
|
html_options[:class] << ' external'
|
||||||
|
html_options[:target] = '_blank'
|
||||||
|
end
|
||||||
|
content_tag(:li,
|
||||||
|
link_to(item[:title], item[:url], html_options))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
safe_join(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plugin help items definition for plugins,
|
||||||
|
# which do not have additionals_help_menu_items integration
|
||||||
|
def additionals_help_items_fallbacks(plugin_id)
|
||||||
|
plugins = { redmine_wiki_lists: [{ title: 'Wiki Lists Macros',
|
||||||
|
url: 'https://www.r-labs.org/projects/wiki_lists/wiki/Wiki_Lists_en' }],
|
||||||
|
redmine_wiki_extensions: [{ title: 'Wiki Extensions',
|
||||||
|
url: 'https://www.r-labs.org/projects/r-labs/wiki/Wiki_Extensions_en' }],
|
||||||
|
redmine_git_hosting: [{ title: 'Redmine Git Hosting',
|
||||||
|
url: 'http://redmine-git-hosting.io/get_started/',
|
||||||
|
admin: true }],
|
||||||
|
redmine_contacts: [{ title: 'Redmine CRM',
|
||||||
|
url: 'https://www.redmineup.com/pages/help/crm',
|
||||||
|
admin: true }],
|
||||||
|
redmine_contacts_helpdesk: [{ title: 'Redmine Helpdesk',
|
||||||
|
url: 'https://www.redmineup.com/pages/help/helpdesk',
|
||||||
|
admin: true }],
|
||||||
|
redmine_ldap_sync: [{ title: 'Redmine LDAP',
|
||||||
|
url: 'https://www.redmine.org/projects/redmine/wiki/RedmineLDAP',
|
||||||
|
admin: true },
|
||||||
|
{ title: 'Redmine LDAP Sync',
|
||||||
|
url: 'https://github.com/thorin/redmine_ldap_sync/blob/master/README.md',
|
||||||
|
admin: true }] }
|
||||||
|
plugins[plugin_id]
|
||||||
|
end
|
||||||
|
end
|
261
plugins/additionals/app/helpers/additionals_queries_helper.rb
Executable file
|
@ -0,0 +1,261 @@
|
||||||
|
module AdditionalsQueriesHelper
|
||||||
|
def additionals_query_session_key(object_type)
|
||||||
|
"#{object_type}_query".to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_retrieve_query(object_type, options = {})
|
||||||
|
session_key = additionals_query_session_key(object_type)
|
||||||
|
query_class = Object.const_get("#{object_type.camelcase}Query")
|
||||||
|
if params[:query_id].present?
|
||||||
|
additionals_load_query_id(query_class, session_key, params[:query_id], options, object_type)
|
||||||
|
elsif api_request? ||
|
||||||
|
params[:set_filter] ||
|
||||||
|
session[session_key].nil? ||
|
||||||
|
session[session_key][:project_id] != (@project ? @project.id : nil)
|
||||||
|
# Give it a name, required to be valid
|
||||||
|
@query = query_class.new(name: '_')
|
||||||
|
@query.project = @project
|
||||||
|
@query.user_filter = options[:user_filter] if options[:user_filter]
|
||||||
|
@query.build_from_params(params)
|
||||||
|
session[session_key] = { project_id: @query.project_id }
|
||||||
|
# session has a limit to 4k, we have to use a cache for it for larger data
|
||||||
|
Rails.cache.write(additionals_query_cache_key(object_type),
|
||||||
|
filters: @query.filters,
|
||||||
|
group_by: @query.group_by,
|
||||||
|
column_names: @query.column_names,
|
||||||
|
totalable_names: @query.totalable_names,
|
||||||
|
sort_criteria: params[:sort].presence || @query.sort_criteria.to_a)
|
||||||
|
else
|
||||||
|
# retrieve from session
|
||||||
|
@query = query_class.find_by(id: session[session_key][:id]) if session[session_key][:id]
|
||||||
|
session_data = Rails.cache.read(additionals_query_cache_key(object_type))
|
||||||
|
@query ||= query_class.new(name: '_',
|
||||||
|
filters: session_data.nil? ? nil : session_data[:filters],
|
||||||
|
group_by: session_data.nil? ? nil : session_data[:group_by],
|
||||||
|
column_names: session_data.nil? ? nil : session_data[:column_names],
|
||||||
|
totalable_names: session_data.nil? ? nil : session_data[:totalable_names],
|
||||||
|
sort_criteria: params[:sort].presence || (session_data.nil? ? nil : session_data[:sort_criteria]))
|
||||||
|
@query.project = @project
|
||||||
|
if params[:sort].present?
|
||||||
|
@query.sort_criteria = params[:sort]
|
||||||
|
# we have to write cache for sort order
|
||||||
|
Rails.cache.write(additionals_query_cache_key(object_type),
|
||||||
|
filters: @query.filters,
|
||||||
|
group_by: @query.group_by,
|
||||||
|
column_names: @query.column_names,
|
||||||
|
totalable_names: @query.totalable_names,
|
||||||
|
sort_criteria: params[:sort])
|
||||||
|
elsif session_data.present?
|
||||||
|
@query.sort_criteria = session_data[:sort_criteria]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_load_query_id(query_class, session_key, query_id, options, object_type)
|
||||||
|
cond = 'project_id IS NULL'
|
||||||
|
cond << " OR project_id = #{@project.id}" if @project
|
||||||
|
@query = query_class.where(cond).find(query_id)
|
||||||
|
raise ::Unauthorized unless @query.visible?
|
||||||
|
|
||||||
|
@query.project = @project
|
||||||
|
@query.user_filter = options[:user_filter] if options[:user_filter]
|
||||||
|
session[session_key] = { id: @query.id, project_id: @query.project_id }
|
||||||
|
|
||||||
|
@query.sort_criteria = params[:sort] if params[:sort].present?
|
||||||
|
# we have to write cache for sort order
|
||||||
|
Rails.cache.write(additionals_query_cache_key(object_type),
|
||||||
|
filters: @query.filters,
|
||||||
|
group_by: @query.group_by,
|
||||||
|
column_names: @query.column_names,
|
||||||
|
totalable_names: @query.totalable_names,
|
||||||
|
sort_criteria: @query.sort_criteria)
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_query_cache_key(object_type)
|
||||||
|
project_id = @project.nil? ? 0 : @project.id
|
||||||
|
"#{object_type}_query_data_#{session.id}_#{project_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_select2_search_users(where_filter = '', where_params = {})
|
||||||
|
q = params[:q].to_s.strip
|
||||||
|
exclude_id = params[:user_id].to_i
|
||||||
|
scope = User.active.where(type: 'User')
|
||||||
|
scope = scope.where.not(id: exclude_id) if exclude_id > 0
|
||||||
|
scope = scope.where(where_filter, where_params) if where_filter.present?
|
||||||
|
scope = scope.like(q) if q.present?
|
||||||
|
scope = scope.order(last_login_on: :desc)
|
||||||
|
.limit(params[:limit] || Additionals::SELECT2_INIT_ENTRIES)
|
||||||
|
@users = scope.to_a.sort! { |x, y| x.name <=> y.name }
|
||||||
|
render layout: false, partial: 'auto_completes/additionals_users'
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_query_to_xlsx(items, query, options = {})
|
||||||
|
require 'write_xlsx'
|
||||||
|
columns = if options[:columns].present? || options[:c].present?
|
||||||
|
query.available_columns
|
||||||
|
else
|
||||||
|
query.columns
|
||||||
|
end
|
||||||
|
|
||||||
|
stream = StringIO.new('')
|
||||||
|
export_to_xlsx(items, columns, filename: stream)
|
||||||
|
stream.string
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_result_to_xlsx(items, columns, options = {})
|
||||||
|
raise 'option filename is mission' if options[:filename].blank?
|
||||||
|
|
||||||
|
require 'write_xlsx'
|
||||||
|
export_to_xlsx(items, columns, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_to_xlsx(items, columns, options)
|
||||||
|
workbook = WriteXLSX.new(options[:filename])
|
||||||
|
worksheet = workbook.add_worksheet
|
||||||
|
|
||||||
|
# Freeze header row and # column.
|
||||||
|
freeze_row = options[:freeze_first_row].nil? || options[:freeze_first_row] ? 1 : 0
|
||||||
|
freeze_column = options[:freeze_first_column].nil? || options[:freeze_first_column] ? 1 : 0
|
||||||
|
worksheet.freeze_panes(freeze_row, freeze_column)
|
||||||
|
|
||||||
|
options[:columns_width] = if options[:xlsx_write_header_row].present?
|
||||||
|
send(options[:xlsx_write_header_row], workbook, worksheet, columns)
|
||||||
|
else
|
||||||
|
xlsx_write_header_row(workbook, worksheet, columns)
|
||||||
|
end
|
||||||
|
options[:columns_width] = if options[:xlsx_write_item_rows].present?
|
||||||
|
send(options[:xlsx_write_item_rows], workbook, worksheet, columns, items, options)
|
||||||
|
else
|
||||||
|
xlsx_write_item_rows(workbook, worksheet, columns, items, options)
|
||||||
|
end
|
||||||
|
columns.size.times do |index|
|
||||||
|
worksheet.set_column(index, index, options[:columns_width][index])
|
||||||
|
end
|
||||||
|
|
||||||
|
workbook.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def xlsx_write_header_row(workbook, worksheet, columns)
|
||||||
|
columns_width = []
|
||||||
|
columns.each_with_index do |c, index|
|
||||||
|
value = if c.class.name == 'String'
|
||||||
|
c
|
||||||
|
else
|
||||||
|
c.caption.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
worksheet.write(0, index, value, workbook.add_format(xlsx_cell_format(:header)))
|
||||||
|
columns_width << xlsx_get_column_width(value)
|
||||||
|
end
|
||||||
|
columns_width
|
||||||
|
end
|
||||||
|
|
||||||
|
def xlsx_write_item_rows(workbook, worksheet, columns, items, options = {})
|
||||||
|
hyperlink_format = workbook.add_format(xlsx_cell_format(:link))
|
||||||
|
even_text_format = workbook.add_format(xlsx_cell_format(:text, '', 0))
|
||||||
|
even_text_format.set_num_format(0x31)
|
||||||
|
odd_text_format = workbook.add_format(xlsx_cell_format(:text, '', 1))
|
||||||
|
odd_text_format.set_num_format(0x31)
|
||||||
|
|
||||||
|
items.each_with_index do |line, line_index|
|
||||||
|
columns.each_with_index do |c, column_index|
|
||||||
|
value = csv_content(c, line)
|
||||||
|
if c.name == :id # ID
|
||||||
|
link = url_for(controller: line.class.name.underscore.pluralize, action: 'show', id: line.id)
|
||||||
|
worksheet.write(line_index + 1, column_index, link, hyperlink_format, value)
|
||||||
|
elsif xlsx_hyperlink_cell?(value)
|
||||||
|
worksheet.write(line_index + 1, column_index, value, hyperlink_format, value)
|
||||||
|
elsif !c.inline?
|
||||||
|
# block column can be multiline strings
|
||||||
|
value.gsub!("\r\n", "\n")
|
||||||
|
text_format = line_index.even? ? even_text_format : odd_text_format
|
||||||
|
worksheet.write_rich_string(line_index + 1, column_index, value, text_format)
|
||||||
|
else
|
||||||
|
worksheet.write(line_index + 1,
|
||||||
|
column_index,
|
||||||
|
value,
|
||||||
|
workbook.add_format(xlsx_cell_format(:cell, value, line_index)))
|
||||||
|
end
|
||||||
|
|
||||||
|
width = xlsx_get_column_width(value)
|
||||||
|
options[:columns_width][column_index] = width if options[:columns_width][column_index] < width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
options[:columns_width]
|
||||||
|
end
|
||||||
|
|
||||||
|
def xlsx_get_column_width(value)
|
||||||
|
value_str = value.to_s
|
||||||
|
|
||||||
|
# 1.1: margin
|
||||||
|
width = (value_str.length + value_str.chars.reject(&:ascii_only?).length) * 1.1 + 1
|
||||||
|
# 30: max width
|
||||||
|
width > 30 ? 30 : width
|
||||||
|
end
|
||||||
|
|
||||||
|
def xlsx_cell_format(type, value = 0, index = 0)
|
||||||
|
format = { border: 1, text_wrap: 1, valign: 'top' }
|
||||||
|
case type
|
||||||
|
when :header
|
||||||
|
format[:bold] = 1
|
||||||
|
format[:color] = 'white'
|
||||||
|
format[:bg_color] = 'gray'
|
||||||
|
when :link
|
||||||
|
format[:color] = 'blue'
|
||||||
|
format[:underline] = 1
|
||||||
|
format[:bg_color] = 'silver' unless index.even?
|
||||||
|
else
|
||||||
|
format[:bg_color] = 'silver' unless index.even?
|
||||||
|
format[:color] = 'red' if value.is_a?(Numeric) && value < 0
|
||||||
|
end
|
||||||
|
|
||||||
|
format
|
||||||
|
end
|
||||||
|
|
||||||
|
def xlsx_hyperlink_cell?(token)
|
||||||
|
# Match http, https or ftp URL
|
||||||
|
if token =~ %r{\A[fh]tt?ps?://}
|
||||||
|
true
|
||||||
|
# Match mailto:
|
||||||
|
elsif token.present? && token.start_with?('mailto:')
|
||||||
|
true
|
||||||
|
# Match internal or external sheet link
|
||||||
|
elsif token =~ /\A(?:in|ex)ternal:/
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the query definition as hidden field tags
|
||||||
|
# columns in ignored_column_names are skipped (names as symbols)
|
||||||
|
# TODO: this is a temporary fix and should be removed
|
||||||
|
# after https://www.redmine.org/issues/29830 is in Redmine core.
|
||||||
|
def query_as_hidden_field_tags(query, ignored_column_names = nil)
|
||||||
|
tags = hidden_field_tag('set_filter', '1', id: nil)
|
||||||
|
|
||||||
|
if query.filters.present?
|
||||||
|
query.filters.each do |field, filter|
|
||||||
|
tags << hidden_field_tag('f[]', field, id: nil)
|
||||||
|
tags << hidden_field_tag("op[#{field}]", filter[:operator], id: nil)
|
||||||
|
filter[:values].each do |value|
|
||||||
|
tags << hidden_field_tag("v[#{field}][]", value, id: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tags << hidden_field_tag('f[]', '', id: nil)
|
||||||
|
end
|
||||||
|
query.columns.each do |column|
|
||||||
|
next if ignored_column_names.present? && ignored_column_names.include?(column.name)
|
||||||
|
|
||||||
|
tags << hidden_field_tag('c[]', column.name, id: nil)
|
||||||
|
end
|
||||||
|
if query.totalable_names.present?
|
||||||
|
query.totalable_names.each do |name|
|
||||||
|
tags << hidden_field_tag('t[]', name, id: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tags << hidden_field_tag('group_by', query.group_by, id: nil) if query.group_by.present?
|
||||||
|
tags << hidden_field_tag('sort', query.sort_criteria.to_param, id: nil) if query.sort_criteria.present?
|
||||||
|
|
||||||
|
tags
|
||||||
|
end
|
||||||
|
end
|
125
plugins/additionals/app/helpers/additionals_tag_helper.rb
Executable file
|
@ -0,0 +1,125 @@
|
||||||
|
require 'digest/md5'
|
||||||
|
|
||||||
|
module AdditionalsTagHelper
|
||||||
|
# deprecated: this will removed after a while
|
||||||
|
def render_additionals_tags_list(tags, options = {})
|
||||||
|
additionals_tag_cloud(tags, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# deprecated: this will removed after a while
|
||||||
|
def render_additionals_tag_link_line(tag_list)
|
||||||
|
additionals_tag_links(tag_list)
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_cloud(tags, options = {})
|
||||||
|
return if tags.blank?
|
||||||
|
|
||||||
|
options[:show_count] = true
|
||||||
|
|
||||||
|
# prevent ActsAsTaggableOn::TagsHelper from calling `all`
|
||||||
|
# otherwise we will need sort tags after `tag_cloud`
|
||||||
|
tags = tags.all if tags.respond_to?(:all)
|
||||||
|
|
||||||
|
s = []
|
||||||
|
tags.each do |tag|
|
||||||
|
s << additionals_tag_link(tag, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
sep = if options[:tags_without_color]
|
||||||
|
', '
|
||||||
|
else
|
||||||
|
' '
|
||||||
|
end
|
||||||
|
|
||||||
|
content_tag(:div, safe_join(s, sep), class: 'tags')
|
||||||
|
end
|
||||||
|
|
||||||
|
# plain list of tags
|
||||||
|
def render_additionals_tags(tags, sep = ' ')
|
||||||
|
s = if tags.blank?
|
||||||
|
['']
|
||||||
|
else
|
||||||
|
tags.map(&:name)
|
||||||
|
end
|
||||||
|
s.join(sep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_links(tag_list, options = {})
|
||||||
|
return unless tag_list
|
||||||
|
|
||||||
|
sep = if options[:tags_without_color]
|
||||||
|
', '
|
||||||
|
else
|
||||||
|
' '
|
||||||
|
end
|
||||||
|
|
||||||
|
safe_join(tag_list.map do |tag|
|
||||||
|
additionals_tag_link(tag, options)
|
||||||
|
end, sep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_link(tag, options = {})
|
||||||
|
tag_name = []
|
||||||
|
tag_name << tag.name
|
||||||
|
|
||||||
|
unless options[:tags_without_color]
|
||||||
|
tag_bg_color = additionals_tag_color(tag.name)
|
||||||
|
tag_fg_color = additionals_tag_fg_color(tag_bg_color)
|
||||||
|
tag_style = "background-color: #{tag_bg_color}; color: #{tag_fg_color}"
|
||||||
|
end
|
||||||
|
|
||||||
|
tag_name << content_tag('span', "(#{tag.count})", class: 'tag-count') if options[:show_count]
|
||||||
|
|
||||||
|
if options[:tags_without_color]
|
||||||
|
content_tag('span',
|
||||||
|
link_to(safe_join(tag_name), additionals_tag_url(tag.name, options)),
|
||||||
|
class: 'tag-label')
|
||||||
|
else
|
||||||
|
content_tag('span',
|
||||||
|
link_to(safe_join(tag_name),
|
||||||
|
additionals_tag_url(tag.name, options),
|
||||||
|
style: tag_style),
|
||||||
|
class: 'additionals-tag-label-color',
|
||||||
|
style: tag_style)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_url(tag_name, options = {})
|
||||||
|
action = options[:tag_action].presence || (controller_name == 'hrm_user_resources' ? 'show' : 'index')
|
||||||
|
|
||||||
|
{ controller: options[:tag_controller].presence || controller_name,
|
||||||
|
action: action,
|
||||||
|
set_filter: 1,
|
||||||
|
project_id: @project,
|
||||||
|
fields: [:tags],
|
||||||
|
values: { tags: [tag_name] },
|
||||||
|
operators: { tags: '=' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def tag_cloud(tags, classes)
|
||||||
|
return [] if tags.empty?
|
||||||
|
|
||||||
|
max_count = tags.max_by(&:count).count.to_f
|
||||||
|
|
||||||
|
tags.each do |tag|
|
||||||
|
index = ((tag.count / max_count) * (classes.size - 1))
|
||||||
|
yield tag, classes[index.nan? ? 0 : index.round]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_color(tag_name)
|
||||||
|
"##{Digest::MD5.hexdigest(tag_name)[0..5]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def additionals_tag_fg_color(bg_color)
|
||||||
|
# calculate contrast text color according to YIQ method
|
||||||
|
# https://24ways.org/2010/calculating-color-contrast/
|
||||||
|
# https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
||||||
|
r = bg_color[1..2].hex
|
||||||
|
g = bg_color[3..4].hex
|
||||||
|
b = bg_color[5..6].hex
|
||||||
|
(r * 299 + g * 587 + b * 114) >= 128_000 ? 'black' : 'white'
|
||||||
|
end
|
||||||
|
end
|
35
plugins/additionals/app/helpers/additionals_wiki_pdf_helper.rb
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
module AdditionalsWikiPdfHelper
|
||||||
|
include Redmine::Export::PDF
|
||||||
|
|
||||||
|
def wiki_page_to_pdf(page, project)
|
||||||
|
pdf = ITCPDF.new(current_language)
|
||||||
|
pdf.set_title("#{project} - #{page.title}")
|
||||||
|
pdf.alias_nb_pages
|
||||||
|
pdf.footer_date = format_date(User.current.today)
|
||||||
|
pdf.add_page
|
||||||
|
unless Additionals.setting?(:wiki_pdf_remove_title)
|
||||||
|
pdf.SetFontStyle('B', 11)
|
||||||
|
pdf.RDMMultiCell(190, 5,
|
||||||
|
"#{project} - #{page.title} - # #{page.content.version}")
|
||||||
|
end
|
||||||
|
pdf.ln
|
||||||
|
# Set resize image scale
|
||||||
|
pdf.set_image_scale(1.6)
|
||||||
|
pdf.SetFontStyle('', 9)
|
||||||
|
if Additionals.setting?(:wiki_pdf_remove_attachments)
|
||||||
|
pdf.RDMwriteFormattedCell(190,
|
||||||
|
5,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
textilizable(page.content,
|
||||||
|
:text,
|
||||||
|
only_path: false,
|
||||||
|
edit_section_links: false,
|
||||||
|
headings: false,
|
||||||
|
inline_attachments: false), page.attachments)
|
||||||
|
else
|
||||||
|
write_wiki_page(pdf, page)
|
||||||
|
end
|
||||||
|
pdf.output
|
||||||
|
end
|
||||||
|
end
|
118
plugins/additionals/app/models/additionals_font_awesome.rb
Executable file
|
@ -0,0 +1,118 @@
|
||||||
|
class AdditionalsFontAwesome
|
||||||
|
include Redmine::I18n
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def load_icons(type)
|
||||||
|
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins',
|
||||||
|
'additionals',
|
||||||
|
'config',
|
||||||
|
'fontawesome_icons.yml'))).result) || {}
|
||||||
|
icons = {}
|
||||||
|
data.each do |key, values|
|
||||||
|
icons[key] = { unicode: values['unicode'], label: values['label'] } if values['styles'].include?(convert_type2style(type))
|
||||||
|
end
|
||||||
|
icons
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_type2style(type)
|
||||||
|
case type
|
||||||
|
when :fab
|
||||||
|
'brands'
|
||||||
|
when :far
|
||||||
|
'regular'
|
||||||
|
else
|
||||||
|
'solid'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def font_weight(key)
|
||||||
|
case key
|
||||||
|
when :fas
|
||||||
|
900
|
||||||
|
else
|
||||||
|
'normal'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def font_family(key)
|
||||||
|
case key
|
||||||
|
when :fab
|
||||||
|
'Font Awesome\ 5 Brands'
|
||||||
|
else
|
||||||
|
'Font Awesome\ 5 Free'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def key2value(key, type)
|
||||||
|
"fa#{type}_" + key
|
||||||
|
end
|
||||||
|
|
||||||
|
def classes(value)
|
||||||
|
info = value_info(value)
|
||||||
|
return '' if info.blank?
|
||||||
|
|
||||||
|
info[:classes]
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_values(type)
|
||||||
|
FONTAWESOME_ICONS[type].collect { |fa_symbol, values| { id: key2value(fa_symbol, type[-1]), text: values[:label] } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_values(type)
|
||||||
|
FONTAWESOME_ICONS[type].collect { |fa_symbol, values| [values[:label], key2value(fa_symbol, type[-1])] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_for_select
|
||||||
|
[{ text: l(:label_fontawesome_regular), children: json_values(:far) },
|
||||||
|
{ text: l(:label_fontawesome_solid), children: json_values(:fas) },
|
||||||
|
{ text: l(:label_fontawesome_brands), children: json_values(:fab) }].to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
# show only one value as current selected
|
||||||
|
# (all other options are retrieved by select2
|
||||||
|
def active_option_for_select(selected)
|
||||||
|
info = value_info(selected, with_details: true)
|
||||||
|
return [] if info.blank?
|
||||||
|
|
||||||
|
[[info[:label], selected]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def options_for_select
|
||||||
|
[[l(:label_fontawesome_regular), select_values(:far)],
|
||||||
|
[l(:label_fontawesome_solid), select_values(:fas)],
|
||||||
|
[l(:label_fontawesome_brands), select_values(:fab)]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_info(value, options = {})
|
||||||
|
return {} if value.blank?
|
||||||
|
|
||||||
|
values = value.split('_')
|
||||||
|
return {} unless values.count == 2
|
||||||
|
|
||||||
|
info = { type: values[0].to_sym,
|
||||||
|
name: "fa-#{values[1]}" }
|
||||||
|
|
||||||
|
info[:classes] = "#{info[:type]} #{info[:name]}"
|
||||||
|
info[:font_weight] = font_weight(info[:type])
|
||||||
|
info[:font_family] = font_family(info[:type])
|
||||||
|
|
||||||
|
if options[:with_details]
|
||||||
|
info.merge!(load_details(info[:type], values[1]))
|
||||||
|
return {} if info[:unicode].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
info
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_details(type, name)
|
||||||
|
return {} unless FONTAWESOME_ICONS.key?(type)
|
||||||
|
|
||||||
|
values = FONTAWESOME_ICONS[type][name]
|
||||||
|
return {} if values.blank?
|
||||||
|
|
||||||
|
{ unicode: "&#x#{values[:unicode]};".html_safe, label: values[:label] } # rubocop:disable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
43
plugins/additionals/app/models/additionals_import.rb
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
class AdditionalsImport < Import
|
||||||
|
class_attribute :import_class
|
||||||
|
|
||||||
|
# Returns the objects that were imported
|
||||||
|
def saved_objects
|
||||||
|
object_ids = saved_items.pluck(:obj_id)
|
||||||
|
import_class.where(id: object_ids).order(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def project=(project)
|
||||||
|
settings['project'] = project
|
||||||
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
settings['project']
|
||||||
|
end
|
||||||
|
|
||||||
|
def mappable_custom_fields
|
||||||
|
object = import_class.new
|
||||||
|
@custom_fields = object.custom_field_values.map(&:custom_field)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_custom_field_attributes(object, row)
|
||||||
|
object.custom_field_values.each_with_object({}) do |v, h|
|
||||||
|
value = case v.custom_field.field_format
|
||||||
|
when 'date'
|
||||||
|
row_date(row, "cf_#{v.custom_field.id}")
|
||||||
|
when 'list'
|
||||||
|
row_value(row, "cf_#{v.custom_field.id}").try(:split, ',')
|
||||||
|
else
|
||||||
|
row_value(row, "cf_#{v.custom_field.id}")
|
||||||
|
end
|
||||||
|
next unless value
|
||||||
|
|
||||||
|
h[v.custom_field.id.to_s] =
|
||||||
|
if value.is_a?(Array)
|
||||||
|
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.compact.flatten
|
||||||
|
else
|
||||||
|
v.custom_field.value_from_keyword(value, object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
75
plugins/additionals/app/models/additionals_macro.rb
Executable file
|
@ -0,0 +1,75 @@
|
||||||
|
class AdditionalsMacro
|
||||||
|
def self.all(options = {})
|
||||||
|
all = Redmine::WikiFormatting::Macros.available_macros
|
||||||
|
macros = {}
|
||||||
|
macro_list = []
|
||||||
|
|
||||||
|
# needs to run every request (for each user once)
|
||||||
|
permissions = build_permissions(options)
|
||||||
|
|
||||||
|
if options[:filtered].present?
|
||||||
|
options[:filtered] << 'hello_world'
|
||||||
|
else
|
||||||
|
options[:filtered] = ['hello_world']
|
||||||
|
end
|
||||||
|
|
||||||
|
all.each do |macro, macro_options|
|
||||||
|
next if options[:filtered].include?(macro.to_s)
|
||||||
|
next unless macro_allowed(macro, permissions)
|
||||||
|
|
||||||
|
macro_list << macro.to_s
|
||||||
|
macros[macro] = macro_options
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:only_names]
|
||||||
|
macro_list.sort
|
||||||
|
else
|
||||||
|
macros.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.macro_allowed(macro, permissions)
|
||||||
|
permissions.each do |permission|
|
||||||
|
next if permission[:list].exclude?(macro)
|
||||||
|
return false unless permission[:access]
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.build_permissions(options)
|
||||||
|
gpermission = []
|
||||||
|
macro_permissions.each do |permission|
|
||||||
|
permission[:access] = if options[:controller_only] &&
|
||||||
|
permission[:controller].present? &&
|
||||||
|
options[:controller_only].to_sym != permission[:controller]
|
||||||
|
false
|
||||||
|
else
|
||||||
|
User.current.allowed_to?(permission[:permission], nil, global: true)
|
||||||
|
end
|
||||||
|
gpermission << permission
|
||||||
|
end
|
||||||
|
|
||||||
|
gpermission
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.macro_permissions
|
||||||
|
[{ list: %i[issue issue_name_link],
|
||||||
|
permission: :view_issues },
|
||||||
|
{ list: %i[password password_query password_tag password_tag_count],
|
||||||
|
permission: :view_passwords },
|
||||||
|
{ list: %i[contact deal contact_avatar contact_note contact_plain],
|
||||||
|
permission: :view_contacts },
|
||||||
|
{ list: %i[db db_query db_tag db_tag_count],
|
||||||
|
permission: :view_db_entries },
|
||||||
|
{ list: %i[child_pages calendar last_updated_at last_updated_by lastupdated_at lastupdated_by
|
||||||
|
new_page recently_updated recent comments comment_form tags taggedpages tagcloud
|
||||||
|
show_count count vote show_vote terms_accept terms_reject],
|
||||||
|
permission: :view_wiki_pages,
|
||||||
|
controller: :wiki },
|
||||||
|
{ list: %i[mail send_file],
|
||||||
|
permission: :view_helpdesk_tickets },
|
||||||
|
{ list: %i[kb article_id article category],
|
||||||
|
permission: :view_kb_articles }]
|
||||||
|
end
|
||||||
|
end
|
150
plugins/additionals/app/models/additionals_query.rb
Executable file
|
@ -0,0 +1,150 @@
|
||||||
|
module AdditionalsQuery
|
||||||
|
def self.included(base)
|
||||||
|
base.send :include, InstanceMethods
|
||||||
|
end
|
||||||
|
|
||||||
|
module InstanceMethods
|
||||||
|
def initialize_ids_filter(options = {})
|
||||||
|
if options[:label]
|
||||||
|
add_available_filter 'ids', type: :integer, label: options[:label]
|
||||||
|
else
|
||||||
|
add_available_filter 'ids', type: :integer, name: '#'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_for_ids_field(_field, operator, value)
|
||||||
|
if operator == '='
|
||||||
|
# accepts a comma separated list of ids
|
||||||
|
ids = value.first.to_s.scan(/\d+/).map(&:to_i)
|
||||||
|
if ids.present?
|
||||||
|
"#{queried_table_name}.id IN (#{ids.join(',')})"
|
||||||
|
else
|
||||||
|
'1=0'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sql_for_field('id', operator, value, queried_table_name, 'id')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_project_filter(options = {})
|
||||||
|
if project.nil?
|
||||||
|
add_available_filter('project_id', order: options[:position],
|
||||||
|
type: :list,
|
||||||
|
values: -> { project_values })
|
||||||
|
end
|
||||||
|
return if project.nil? || project.leaf? || subproject_values.empty?
|
||||||
|
|
||||||
|
add_available_filter('subproject_id', order: options[:position],
|
||||||
|
type: :list_subprojects,
|
||||||
|
values: -> { subproject_values })
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_created_filter(options = {})
|
||||||
|
add_available_filter 'created_on', order: options[:position],
|
||||||
|
type: :date_past,
|
||||||
|
label: options[:label].presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_updated_filter(options = {})
|
||||||
|
add_available_filter 'updated_on', order: options[:position],
|
||||||
|
type: :date_past,
|
||||||
|
label: options[:label].presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_tags_filter(options = {})
|
||||||
|
values = if project
|
||||||
|
queried_class.available_tags(project: project.id)
|
||||||
|
else
|
||||||
|
queried_class.available_tags
|
||||||
|
end
|
||||||
|
return if values.blank?
|
||||||
|
|
||||||
|
add_available_filter 'tags', order: options[:position],
|
||||||
|
type: :list,
|
||||||
|
values: values.collect { |t| [t.name, t.name] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_author_filter(options = {})
|
||||||
|
return if author_values.empty?
|
||||||
|
|
||||||
|
add_available_filter('author_id', order: options[:position],
|
||||||
|
type: :list_optional,
|
||||||
|
values: options[:no_lambda].nil? ? author_values : -> { author_values })
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_assignee_filter(options = {})
|
||||||
|
return if author_values.empty?
|
||||||
|
|
||||||
|
add_available_filter('assigned_to_id', order: options[:position],
|
||||||
|
type: :list_optional,
|
||||||
|
values: options[:no_lambda] ? author_values : -> { author_values })
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_watcher_filter(options = {})
|
||||||
|
return if watcher_values.empty? || !User.current.logged?
|
||||||
|
|
||||||
|
add_available_filter('watcher_id', order: options[:position],
|
||||||
|
type: :list,
|
||||||
|
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
|
||||||
|
end
|
||||||
|
|
||||||
|
def watcher_values
|
||||||
|
watcher_values = [["<< #{l(:label_me)} >>", 'me']]
|
||||||
|
watcher_values += users.collect { |s| [s.name, s.id.to_s] } if User.current.allowed_to?(:manage_public_queries, project, global: true)
|
||||||
|
watcher_values
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_for_watcher_id_field(field, operator, value)
|
||||||
|
watchable_type = queried_class == User ? 'Principal' : queried_class.to_s
|
||||||
|
|
||||||
|
db_table = Watcher.table_name
|
||||||
|
"#{queried_table_name}.id #{operator == '=' ? 'IN' : 'NOT IN'}
|
||||||
|
(SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='#{watchable_type}' AND " +
|
||||||
|
sql_for_field(field, '=', value, db_table, 'user_id') + ')'
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_for_tags_field(field, _operator, value)
|
||||||
|
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sql_for_is_private_field(_field, operator, value)
|
||||||
|
if bool_operator(operator, value)
|
||||||
|
return '1=1' if value.count > 1
|
||||||
|
|
||||||
|
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
|
||||||
|
else
|
||||||
|
return '1=0' if value.count > 1
|
||||||
|
|
||||||
|
"#{queried_table_name}.is_private = #{self.class.connection.quoted_false}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# use for list fields with to values 1 (true) and 0 (false)
|
||||||
|
def bool_operator(operator, values)
|
||||||
|
operator == '=' && values.first == '1' || operator != '=' && values.first != '1'
|
||||||
|
end
|
||||||
|
|
||||||
|
# use for list
|
||||||
|
def bool_values
|
||||||
|
[[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_count
|
||||||
|
objects_scope.count
|
||||||
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
|
raise StatementInvalid, e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def results_scope(options = {})
|
||||||
|
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
|
||||||
|
|
||||||
|
objects_scope(options)
|
||||||
|
.order(order_option)
|
||||||
|
.joins(joins_for_order_statement(order_option.join(',')))
|
||||||
|
.limit(options[:limit])
|
||||||
|
.offset(options[:offset])
|
||||||
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
|
raise StatementInvalid, e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
70
plugins/additionals/app/models/additionals_tag.rb
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
class AdditionalsTag
|
||||||
|
TAG_TABLE_NAME = RedmineCrm::Tag.table_name if defined? RedmineCrm
|
||||||
|
TAGGING_TABLE_NAME = RedmineCrm::Tagging.table_name if defined? RedmineCrm
|
||||||
|
PROJECT_TABLE_NAME = Project.table_name
|
||||||
|
|
||||||
|
def self.get_available_tags(klass, options = {})
|
||||||
|
scope = RedmineCrm::Tag.where({})
|
||||||
|
scope = scope.where("#{PROJECT_TABLE_NAME}.id = ?", options[:project]) if options[:project]
|
||||||
|
if options[:permission]
|
||||||
|
scope = scope.where(tag_access(options[:permission]))
|
||||||
|
elsif options[:visible_condition]
|
||||||
|
scope = scope.where(klass.visible_condition(User.current))
|
||||||
|
end
|
||||||
|
scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||||
|
scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
|
||||||
|
scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
|
||||||
|
scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
|
||||||
|
|
||||||
|
scope = scope.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count")
|
||||||
|
scope = scope.joins(tag_joins(klass, options))
|
||||||
|
scope = scope.group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0')
|
||||||
|
scope = scope.order("#{TAG_TABLE_NAME}.name")
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.tag_joins(klass, options = {})
|
||||||
|
table_name = klass.table_name
|
||||||
|
|
||||||
|
joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
|
||||||
|
joins << "JOIN #{table_name} " \
|
||||||
|
"ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
|
||||||
|
|
||||||
|
if options[:project_join]
|
||||||
|
joins << options[:project_join]
|
||||||
|
elsif options[:project] || !options[:without_projects]
|
||||||
|
joins << "JOIN #{PROJECT_TABLE_NAME} ON #{table_name}.project_id = #{PROJECT_TABLE_NAME}.id"
|
||||||
|
end
|
||||||
|
|
||||||
|
joins
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.tag_access(permission)
|
||||||
|
projects_allowed = if permission.nil?
|
||||||
|
Project.visible.pluck(:id)
|
||||||
|
else
|
||||||
|
Project.where(Project.allowed_to_condition(User.current, permission)).pluck(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if projects_allowed.present?
|
||||||
|
"#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join(',')})" unless projects_allowed.empty?
|
||||||
|
else
|
||||||
|
'1=0'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.remove_unused_tags
|
||||||
|
unused = RedmineCrm::Tag.find_by_sql(<<-SQL)
|
||||||
|
SELECT * FROM tags WHERE id NOT IN (
|
||||||
|
SELECT DISTINCT tag_id FROM taggings
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
unused.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sql_for_tags_field(klass, operator, value)
|
||||||
|
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
|
||||||
|
ids_list = klass.tagged_with(value).collect(&:id).push(0).join(',')
|
||||||
|
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
|
||||||
|
end
|
||||||
|
end
|
5
plugins/additionals/app/overrides/account/register.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'account/register',
|
||||||
|
name: 'add-invisble-captcha',
|
||||||
|
insert_top: 'div.box',
|
||||||
|
original: 'e64d82c46cc3322e4d953aa119d1e71e81854158',
|
||||||
|
partial: 'account/invisible_captcha'
|
5
plugins/additionals/app/overrides/admin/info.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'admin/info',
|
||||||
|
name: 'add-system_info',
|
||||||
|
insert_after: 'table.list',
|
||||||
|
original: '73b55ca692bcf4db9ecb7a16ec6d6f9e46f08a90',
|
||||||
|
partial: 'admin/system_info'
|
5
plugins/additionals/app/overrides/issues/show.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'issues/_action_menu',
|
||||||
|
name: 'add-issue-assign-to-me',
|
||||||
|
insert_bottom: 'div.contextual',
|
||||||
|
original: 'c0a30490bb9ac5c5644e674319f17e40c57034d8',
|
||||||
|
partial: 'issues/additionals_action_menu'
|
5
plugins/additionals/app/overrides/layouts/base.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'layouts/base',
|
||||||
|
name: 'add-body-header',
|
||||||
|
insert_before: 'div#wrapper',
|
||||||
|
original: '4af81ed701989727953cea2e376c9d83665d7eb2',
|
||||||
|
partial: 'additionals/global_body_header'
|
5
plugins/additionals/app/overrides/roles/form.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'roles/_form',
|
||||||
|
name: 'roles-form-hide',
|
||||||
|
insert_before: 'p.manage_members_shown',
|
||||||
|
original: 'b2a317f49e0b65ae506c8871f0c2bcc3e8098766',
|
||||||
|
partial: 'roles/additionals_form'
|
10
plugins/additionals/app/overrides/users/show.rb
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
Deface::Override.new virtual_path: 'users/show',
|
||||||
|
name: 'user-show-info-hook',
|
||||||
|
insert_top: 'div.splitcontentleft ul:first-child',
|
||||||
|
original: 'aff8d775275e3f33cc45d72b8e2896144be4beff',
|
||||||
|
partial: 'hooks/view_users_show'
|
||||||
|
Deface::Override.new virtual_path: 'users/show',
|
||||||
|
name: 'user-contextual-hook',
|
||||||
|
insert_bottom: 'div.contextual',
|
||||||
|
original: '9d6a7ad6ba0addc68c6b4f6c3b868511bc8eb542',
|
||||||
|
partial: 'hooks/view_users_contextual'
|
15
plugins/additionals/app/overrides/welcome/index.rb
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
Deface::Override.new virtual_path: 'welcome/index',
|
||||||
|
name: 'add-welcome-bottom-content',
|
||||||
|
insert_after: 'div.splitcontentright',
|
||||||
|
original: 'dd470844bcaa4d7c9dc66e70e6c0c843d42969bf',
|
||||||
|
partial: 'welcome/overview_bottom'
|
||||||
|
Deface::Override.new virtual_path: 'welcome/index',
|
||||||
|
name: 'add-welcome-top-content',
|
||||||
|
insert_before: 'div.splitcontentleft',
|
||||||
|
original: 'e7de0a2e88c5ccb4d1feb7abac239e4b669babed',
|
||||||
|
partial: 'welcome/overview_top'
|
||||||
|
Deface::Override.new virtual_path: 'welcome/index',
|
||||||
|
name: 'remove-welcome-news',
|
||||||
|
replace: 'div.news',
|
||||||
|
original: '163f5df8f0cb2d5009d7f57ad38174ed29201a1a',
|
||||||
|
partial: 'welcome/overview_news'
|
5
plugins/additionals/app/overrides/wiki/show.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'wiki/show',
|
||||||
|
name: 'addto-wiki-show',
|
||||||
|
insert_before: 'div.contextual',
|
||||||
|
original: '6b0cb1646d5e2cb23feee1805949e266036581e6',
|
||||||
|
partial: 'wiki/show_additionals'
|
5
plugins/additionals/app/overrides/wiki/sidebar.rb
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
Deface::Override.new virtual_path: 'wiki/_sidebar',
|
||||||
|
name: 'addto-wiki-sidebar',
|
||||||
|
insert_after: 'ul',
|
||||||
|
original: '07a5375c015a7d96826c9977c4d8889c4a98bb49',
|
||||||
|
partial: 'wiki/global_sidebar'
|
2
plugins/additionals/app/views/account/_invisible_captcha.html.slim
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
- if Additionals.setting?(:invisible_captcha)
|
||||||
|
= invisible_captcha
|
5
plugins/additionals/app/views/account/_login_text.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- login_text = Additionals.settings[:account_login_bottom]
|
||||||
|
- if login_text.present?
|
||||||
|
br
|
||||||
|
.login-additionals
|
||||||
|
= textilizable(login_text)
|
9
plugins/additionals/app/views/additionals/_body_bottom.html.slim
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
- footer = Additionals.settings[:global_footer]
|
||||||
|
- if footer.present?
|
||||||
|
.additionals-footer
|
||||||
|
= textilizable(footer)
|
||||||
|
- if @additionals_help_items.present?
|
||||||
|
javascript:
|
||||||
|
$(function() {
|
||||||
|
$('a.help').parent().append("<ul class=\"menu-children\">#{escape_javascript(@additionals_help_items)}</ul>");
|
||||||
|
});
|
5
plugins/additionals/app/views/additionals/_content.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- unless (controller_name == 'account' && action_name == 'login') || \
|
||||||
|
(controller_name == 'my') || \
|
||||||
|
(controller_name == 'account' && action_name == 'lost_password')
|
||||||
|
- if Additionals.setting?(:add_go_to_top)
|
||||||
|
a.gototop[href="#gototop"] = l(:label_go_to_top)
|
27
plugins/additionals/app/views/additionals/_export_options.html.slim
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
div id="#{export_format}-export-options" style="display: none"
|
||||||
|
h3.title = l(:label_export_options, export_format: export_format.upcase)
|
||||||
|
|
||||||
|
= form_tag(url, method: :get, id: "#{export_format}-export-form") do
|
||||||
|
- if @query.available_filters.key?('description')
|
||||||
|
= query_as_hidden_field_tags @query, [:description]
|
||||||
|
else
|
||||||
|
= query_as_hidden_field_tags @query
|
||||||
|
p
|
||||||
|
label
|
||||||
|
= radio_button_tag 'c[]', '', true
|
||||||
|
= l(:description_selected_columns)
|
||||||
|
br
|
||||||
|
label
|
||||||
|
= radio_button_tag 'c[]', 'all_inline'
|
||||||
|
= l(:description_all_columns)
|
||||||
|
- if @query.available_filters.key?('description')
|
||||||
|
p
|
||||||
|
label
|
||||||
|
= check_box_tag 'c[]', 'description', @query.has_column?(:description)
|
||||||
|
= l(:field_description)
|
||||||
|
- if Rails.version >= '5.2'
|
||||||
|
= export_csv_encoding_select_tag
|
||||||
|
p.buttons
|
||||||
|
= submit_tag l(:button_export), name: nil, onclick: 'hideModal(this);'
|
||||||
|
'
|
||||||
|
= link_to_function l(:button_cancel), 'hideModal(this);'
|
2
plugins/additionals/app/views/additionals/_global_body_header.slim
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
- if Additionals.setting?(:add_go_to_top)
|
||||||
|
a#gototop
|
5
plugins/additionals/app/views/additionals/_global_sidebar.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- sidebar = Additionals.settings[:global_sidebar]
|
||||||
|
- if sidebar.present?
|
||||||
|
br
|
||||||
|
.sidebar-additionals
|
||||||
|
= textilizable(sidebar)
|
|
@ -0,0 +1,7 @@
|
||||||
|
= render(partial: 'additionals/live_search_ajax_call.js', layout: false, formats: [:js])
|
||||||
|
- unless defined? classes
|
||||||
|
- classes = 'title'
|
||||||
|
h2 class="#{classes}"
|
||||||
|
= @query.new_record? ? l(title) : h(@query.name)
|
||||||
|
span.additionals-live-search
|
||||||
|
= text_field_tag(:search, q, autocomplete: 'off', class: 'live-search-field', placeholder: l(placeholder))
|
9
plugins/additionals/app/views/additionals/_html_head.html.slim
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
- additionals_top_menu_setup
|
||||||
|
- if Additionals.settings[:external_urls].to_i > 0
|
||||||
|
= javascript_include_tag('redirect', plugin: 'additionals')
|
||||||
|
- if Additionals.settings[:external_urls].to_i == 2
|
||||||
|
= javascript_include_tag('noreferrer', plugin: 'additionals')
|
||||||
|
= additionals_library_load(:font_awesome)
|
||||||
|
= stylesheet_link_tag 'additionals', plugin: 'additionals'
|
||||||
|
- if User.current.try(:hrm_user_type_id).nil?
|
||||||
|
- render_custom_top_menu_item
|
18
plugins/additionals/app/views/additionals/_live_search_ajax_call.js.slim
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
javascript:
|
||||||
|
$(function() {
|
||||||
|
// when the #search field changes
|
||||||
|
$('#search').live_observe_field(2, function() {
|
||||||
|
var form = $('#query_form'); // grab the form wrapping the search bar.
|
||||||
|
var url = form.attr('action');
|
||||||
|
form.find('[name="c[]"] option').each(function(i, elem) {
|
||||||
|
$(elem).attr('selected', true)
|
||||||
|
})
|
||||||
|
var formData = form.serialize();
|
||||||
|
form.find('[name="c[]"] option').each(function(i, elem) {
|
||||||
|
$(elem).attr('selected', false)
|
||||||
|
})
|
||||||
|
$.get(url, formData, function(data) { // perform an AJAX get, the trailing function is what happens on successful get.
|
||||||
|
$("#query-result-list").html(data); // replace the "results" div with the result of action taken
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
31
plugins/additionals/app/views/additionals/_select2_ajax_call.js.slim
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
- options = {} if options.nil?
|
||||||
|
javascript:
|
||||||
|
$("##{field_id}").select2({
|
||||||
|
ajax: {
|
||||||
|
url: "#{ajax_url}",
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function(params) {
|
||||||
|
return {
|
||||||
|
q: params.term
|
||||||
|
};
|
||||||
|
},
|
||||||
|
processResults: function(data, params) {
|
||||||
|
return {
|
||||||
|
results: data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cache: true
|
||||||
|
},
|
||||||
|
placeholder: "#{options[:placeholder].presence}",
|
||||||
|
allowClear: #{options[:allow_clear].present? && options[:allow_clear] ? 'true' : 'false'},
|
||||||
|
minimumInputLength: 0,
|
||||||
|
width: '60%',
|
||||||
|
templateResult: formatState
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatState(opt) {
|
||||||
|
if (opt.loading) return opt.name;
|
||||||
|
var $opt = $('<span>' + opt.name_with_icon + '</span>');
|
||||||
|
return $opt;
|
||||||
|
};
|
27
plugins/additionals/app/views/additionals/_settings_list_defaults.html.slim
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
fieldset.box
|
||||||
|
legend = l(:additionals_query_list_defaults)
|
||||||
|
- setting_name_columns = "#{query_type}_list_defaults"
|
||||||
|
- query = query_class.new(@settings[setting_name_columns.to_sym])
|
||||||
|
- if Redmine::VERSION.to_s >= '4'
|
||||||
|
.default-query-settings-label-redmine4
|
||||||
|
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
|
||||||
|
- else
|
||||||
|
.default-query-settings-label
|
||||||
|
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
|
||||||
|
|
||||||
|
- columns = query_class.new.available_totalable_columns
|
||||||
|
- if columns.count > 0
|
||||||
|
fieldset.box
|
||||||
|
legend = l(:additionals_query_list_default_totals)
|
||||||
|
|
||||||
|
.default-query-settings-totals
|
||||||
|
- setting_name_totals = "#{query_type}_list_default_totals"
|
||||||
|
= hidden_field_tag("settings[#{setting_name_totals}][]", '')
|
||||||
|
- columns.each do |s|
|
||||||
|
label.inline
|
||||||
|
- value = @settings[setting_name_totals.to_sym].present? ? @settings[setting_name_totals.to_sym].include?(s.name.to_s) : false
|
||||||
|
= check_box_tag("settings[#{setting_name_totals}][]",
|
||||||
|
s.name,
|
||||||
|
value,
|
||||||
|
id: nil)
|
||||||
|
= s.caption
|
26
plugins/additionals/app/views/additionals/_tag_list.html.slim
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
- if defined?(show_always) && show_always || entry.tag_list.present?
|
||||||
|
.tags.attribute
|
||||||
|
- unless defined? hide_label
|
||||||
|
span.label
|
||||||
|
= l(:field_tag_list)
|
||||||
|
' :
|
||||||
|
- if defined?(editable) && editable
|
||||||
|
#tags-data
|
||||||
|
= additionals_tag_links(entry.tags, tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
||||||
|
'
|
||||||
|
span.contextual
|
||||||
|
= link_to l(:label_edit_tags),
|
||||||
|
{},
|
||||||
|
onclick: "$('#edit_tags_form').show(); $('#tags-data').hide(); return false;",
|
||||||
|
id: 'edit_tags_link'
|
||||||
|
|
||||||
|
#edit_tags_form style="display: none;"
|
||||||
|
= form_tag(update_url, method: :put, multipart: true ) do
|
||||||
|
= render partial: 'tags_form'
|
||||||
|
'
|
||||||
|
= submit_tag l(:button_save), class: 'button-small'
|
||||||
|
'
|
||||||
|
= link_to l(:button_cancel), {}, onclick: "$('#edit_tags_form').hide(); $('#tags-data').show(); return false;"
|
||||||
|
|
||||||
|
- else
|
||||||
|
= additionals_tag_links(entry.tags, tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
|
@ -0,0 +1,8 @@
|
||||||
|
- @settings = ActionController::Parameters.new(@settings) unless Rails.version >= '5.2'
|
||||||
|
' Need Help? :
|
||||||
|
= link_to(l(:label_additionals_doc),
|
||||||
|
'https://additionals.readthedocs.io/en/latest/',
|
||||||
|
class: 'external',
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener')
|
||||||
|
= render_tabs additionals_settings_tabs
|
39
plugins/additionals/app/views/additionals/settings/_general.html.slim
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
br
|
||||||
|
h3 = l(:label_content_plural)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_account_login))
|
||||||
|
= text_area_tag 'settings[account_login_bottom]', @settings[:account_login_bottom], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:account_login_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_global_sidebar))
|
||||||
|
= text_area_tag 'settings[global_sidebar]', @settings[:global_sidebar], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:global_sidebar_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_global_footer))
|
||||||
|
= text_area_tag 'settings[global_footer]', @settings[:global_footer], class: 'wiki-edit', rows: 5
|
||||||
|
em.info
|
||||||
|
= l(:global_footer_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_setting_plural)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_external_urls))
|
||||||
|
= select_tag 'settings[external_urls]',
|
||||||
|
options_for_select({ l(:external_url_default) => '0',
|
||||||
|
l(:external_url_new_window) => '1',
|
||||||
|
l(:external_url_noreferrer) => '2' }, @settings['external_urls'])
|
||||||
|
em.info
|
||||||
|
= t(:external_urls_info_html)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_add_go_to_top))
|
||||||
|
= check_box_tag 'settings[add_go_to_top]', 1, @settings[:add_go_to_top].to_i == 1
|
||||||
|
em.info
|
||||||
|
= t(:add_go_to_top_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_legacy_smiley_support))
|
||||||
|
= check_box_tag 'settings[legacy_smiley_support]', 1, @settings[:legacy_smiley_support].to_i == 1
|
||||||
|
em.info
|
||||||
|
= t(:legacy_smiley_support_info_html)
|
112
plugins/additionals/app/views/additionals/settings/_issues.html.slim
Executable file
|
@ -0,0 +1,112 @@
|
||||||
|
br
|
||||||
|
h3 = l(:label_content_plural)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_new_ticket_message))
|
||||||
|
= text_area_tag 'settings[new_ticket_message]', @settings[:new_ticket_message], class: 'wiki-edit', rows: 10
|
||||||
|
em.info = l(:new_ticket_message_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
hr
|
||||||
|
|
||||||
|
h3 = l(:label_setting_plural)
|
||||||
|
.info = t(:top_rules_help)
|
||||||
|
|
||||||
|
br
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_new_issue_on_profile))
|
||||||
|
= check_box_tag 'settings[new_issue_on_profile]', 1, @settings[:new_issue_on_profile].to_i == 1
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_issue_assign_to_me))
|
||||||
|
= check_box_tag 'settings[issue_assign_to_me]', 1, @settings[:issue_assign_to_me].to_i == 1
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_issue_change_status_in_sidebar))
|
||||||
|
= check_box_tag 'settings[issue_change_status_in_sidebar]', 1, @settings[:issue_change_status_in_sidebar].to_i == 1
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_issue_autowatch_involved))
|
||||||
|
= check_box_tag 'settings[issue_autowatch_involved]', 1, @settings[:issue_autowatch_involved].to_i == 1
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_close_with_open_children))
|
||||||
|
= check_box_tag 'settings[issue_close_with_open_children]', 1, @settings[:issue_close_with_open_children].to_i == 1
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_freezed_with_close))
|
||||||
|
= check_box_tag 'settings[issue_freezed_with_close]', 1, @settings[:issue_freezed_with_close].to_i == 1
|
||||||
|
em.info = t(:rule_issue_freezed_with_close_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
|
||||||
|
- rule_status = IssueStatus.sorted
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_status_change))
|
||||||
|
= check_box_tag 'settings[issue_status_change]', 1, @settings[:issue_status_change].to_i == 1
|
||||||
|
span[style="vertical-align: top; margin-left: 15px;"]
|
||||||
|
= l(:field_status)
|
||||||
|
| x:
|
||||||
|
= select_tag 'settings[issue_status_x]',
|
||||||
|
options_for_select(rule_status.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_status_x]),
|
||||||
|
multiple: true, size: 6, style: 'width:150px'
|
||||||
|
'
|
||||||
|
= l(:field_status)
|
||||||
|
| y:
|
||||||
|
= select_tag 'settings[issue_status_y]',
|
||||||
|
options_for_select(rule_status.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_status_y]),
|
||||||
|
multiple: false, style: 'width:150px; vertical-align: top'
|
||||||
|
em.info = t(:rule_issue_status_change_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_current_user_status))
|
||||||
|
= check_box_tag 'settings[issue_current_user_status]', 1, @settings[:issue_current_user_status].to_i == 1
|
||||||
|
span[style="vertical-align: top; margin-left: 15px;"]
|
||||||
|
= l(:field_status)
|
||||||
|
| x:
|
||||||
|
= select_tag 'settings[issue_assign_to_x]',
|
||||||
|
options_for_select(rule_status.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_assign_to_x]),
|
||||||
|
multiple: true, size: 6, style: 'width:150px'
|
||||||
|
em.info = t(:rule_issue_current_user_status_info_html)
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_auto_assign))
|
||||||
|
= check_box_tag 'settings[issue_auto_assign]', 1, @settings[:issue_auto_assign].to_i == 1
|
||||||
|
span[style="vertical-align: top; margin-left: 15px;"]
|
||||||
|
= l(:field_status)
|
||||||
|
| x:
|
||||||
|
= select_tag 'settings[issue_auto_assign_status]',
|
||||||
|
options_for_select(rule_status.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_auto_assign_status]),
|
||||||
|
multiple: true, size: 6, style: 'width:150px'
|
||||||
|
'
|
||||||
|
= l(:label_role)
|
||||||
|
| :
|
||||||
|
= select_tag 'settings[issue_auto_assign_role]',
|
||||||
|
options_from_collection_for_select(Role.givable.sorted, :id, :name, @settings[:issue_auto_assign_role]),
|
||||||
|
multiple: false, style: 'width:150px; vertical-align: top'
|
||||||
|
em.info = t(:rule_issue_auto_assign_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_rule_issue_timelog_required))
|
||||||
|
= check_box_tag 'settings[issue_timelog_required]', 1, @settings[:issue_timelog_required].to_i == 1
|
||||||
|
span[style="vertical-align: top; margin-left: 15px;"]
|
||||||
|
= l(:label_tracker_plural)
|
||||||
|
| :
|
||||||
|
= select_tag 'settings[issue_timelog_required_tracker]',
|
||||||
|
options_for_select(Tracker.all.sorted.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_timelog_required_tracker]),
|
||||||
|
multiple: true, size: 6, style: 'width:150px'
|
||||||
|
'
|
||||||
|
= l(:field_status)
|
||||||
|
| :
|
||||||
|
= select_tag 'settings[issue_timelog_required_status]',
|
||||||
|
options_for_select(rule_status.collect { |column| [column.name, column.id] },
|
||||||
|
@settings[:issue_timelog_required_status]),
|
||||||
|
multiple: true, size: 6, style: 'width:150px'
|
||||||
|
em.info
|
||||||
|
= t(:rule_issue_timelog_required_info_html)
|
15
plugins/additionals/app/views/additionals/settings/_macros.html.slim
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
em.info
|
||||||
|
= l(:hidden_macros_in_toolbar_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_hidden_macros_in_toolbar))
|
||||||
|
= hidden_field_tag('settings[hidden_macros_in_toolbar][]', '')
|
||||||
|
- @available_macros = AdditionalsMacro.all(only_names: true).each do |m|
|
||||||
|
label.block
|
||||||
|
- value = @settings[:hidden_macros_in_toolbar].present? ? @settings[:hidden_macros_in_toolbar].include?(m) : false
|
||||||
|
= check_box_tag('settings[hidden_macros_in_toolbar][]', m, value, id: nil)
|
||||||
|
= m
|
||||||
|
|
||||||
|
br
|
44
plugins/additionals/app/views/additionals/settings/_menu.html.slim
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
.info = t(:label_top_menu_help_html)
|
||||||
|
br
|
||||||
|
h3 = l(:label_custom_menu_items)
|
||||||
|
|
||||||
|
- 5.times do |i|
|
||||||
|
fieldset
|
||||||
|
legend
|
||||||
|
b = "#{l(:label_menu_entry)} ##{i + 1}"
|
||||||
|
div
|
||||||
|
p
|
||||||
|
label = h l(:field_name)
|
||||||
|
= text_field_tag('settings[custom_menu' + i.to_s + '_name]', @settings['custom_menu' + i.to_s + '_name'], size: 40)
|
||||||
|
p
|
||||||
|
label = h l(:field_url)
|
||||||
|
= text_field_tag('settings[custom_menu' + i.to_s + '_url]', @settings['custom_menu' + i.to_s + '_url'], size: 80)
|
||||||
|
p
|
||||||
|
label = h l(:field_title)
|
||||||
|
= text_field_tag('settings[custom_menu' + i.to_s + '_title]', @settings['custom_menu' + i.to_s + '_title'], size: 80)
|
||||||
|
i
|
||||||
|
| (
|
||||||
|
= l(:label_optional)
|
||||||
|
| )
|
||||||
|
p
|
||||||
|
label = h l(:label_permissions)
|
||||||
|
- permission_field = 'custom_menu' + i.to_s + '_roles'
|
||||||
|
- menu_roles = Struct.new(:id, :name)
|
||||||
|
= select_tag('settings[' + permission_field + ']',
|
||||||
|
options_from_collection_for_select(Role.sorted.collect { |m| menu_roles.new(m.id, m.name) },
|
||||||
|
:id,
|
||||||
|
:name,
|
||||||
|
@settings[permission_field]),
|
||||||
|
multiple: true, style: 'height: 100px;')
|
||||||
|
em.info = l(:menu_roles_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_setting_plural)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_remove_help))
|
||||||
|
= check_box_tag 'settings[remove_help]', 1, @settings[:remove_help].to_i == 1
|
||||||
|
em.info = l(:remove_help_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_remove_mypage))
|
||||||
|
= check_box_tag 'settings[remove_mypage]', 1, @settings[:remove_mypage].to_i == 1
|
||||||
|
em.info = l(:remove_mypage_info)
|
29
plugins/additionals/app/views/additionals/settings/_overview.html.slim
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
.info = t(:top_overview_help)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_content_plural)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_overview_right))
|
||||||
|
= text_area_tag 'settings[overview_right]', @settings[:overview_right], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:overview_right_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_overview_top))
|
||||||
|
= text_area_tag 'settings[overview_top]', @settings[:overview_top], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:overview_top_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_overview_bottom))
|
||||||
|
= text_area_tag 'settings[overview_bottom]', @settings[:overview_bottom], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:overview_bottom_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_setting_plural)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_remove_news))
|
||||||
|
= check_box_tag 'settings[remove_news]', 1, @settings[:remove_news].to_i == 1
|
||||||
|
em.info
|
||||||
|
= l(:remove_news_info)
|
26
plugins/additionals/app/views/additionals/settings/_projects.html.slim
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
.info = t(:top_projects_help)
|
||||||
|
br
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_project_overview_content))
|
||||||
|
= text_area_tag 'settings[project_overview_content]',
|
||||||
|
@settings[:project_overview_content],
|
||||||
|
class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:project_overview_content_info)
|
||||||
|
|
||||||
|
hr
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_disabled_modules))
|
||||||
|
= hidden_field_tag('settings[disabled_modules][]', '')
|
||||||
|
- Redmine::AccessControl.available_project_modules_all.each do |m|
|
||||||
|
label.block
|
||||||
|
- value = @settings[:disabled_modules].present? ? @settings[:disabled_modules].include?(m.to_s) : false
|
||||||
|
= check_box_tag('settings[disabled_modules][]', m, value, id: nil)
|
||||||
|
= l_or_humanize(m, prefix: 'project_module_')
|
||||||
|
|
||||||
|
br
|
||||||
|
|
||||||
|
em.info
|
||||||
|
= l(:disabled_modules_info)
|
10
plugins/additionals/app/views/additionals/settings/_users.html.slim
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
br
|
||||||
|
h3 = l(:label_user_plural)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_invisible_captcha))
|
||||||
|
= check_box_tag 'settings[invisible_captcha]',
|
||||||
|
1,
|
||||||
|
@settings[:invisible_captcha].to_i == 1,
|
||||||
|
disabled: (true unless Setting.self_registration?)
|
||||||
|
em.info
|
||||||
|
= t(:invisible_captcha_info_html)
|
7
plugins/additionals/app/views/additionals/settings/_web_apis.html.slim
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
br
|
||||||
|
h3 = l(:label_web_apis)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_google_maps_embed_api))
|
||||||
|
= text_field_tag('settings[google_maps_api_key]', @settings[:google_maps_api_key], size: 60)
|
||||||
|
em.info = t(:google_maps_embed_api_html)
|
||||||
|
= call_hook(:additionals_settings_web_apis, settings: @settings)
|
34
plugins/additionals/app/views/additionals/settings/_wiki.html.slim
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
.info = t(:top_wiki_help)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_content_plural)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_global_wiki_sidebar))
|
||||||
|
= text_area_tag 'settings[global_wiki_sidebar]', @settings[:global_wiki_sidebar], class: 'wiki-edit', rows: 10
|
||||||
|
em.info
|
||||||
|
= l(:global_wiki_sidebar_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_global_wiki_header))
|
||||||
|
= text_area_tag 'settings[global_wiki_header]', @settings[:global_wiki_header], class: 'wiki-edit', rows: 5
|
||||||
|
em.info
|
||||||
|
= l(:global_wiki_header_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_global_wiki_footer))
|
||||||
|
= text_area_tag 'settings[global_wiki_footer]', @settings[:global_wiki_footer], class: 'wiki-edit', rows: 5
|
||||||
|
em.info
|
||||||
|
= l(:global_wiki_footer_info)
|
||||||
|
|
||||||
|
br
|
||||||
|
h3 = l(:label_pdf_wiki_settings)
|
||||||
|
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_wiki_pdf_remove_title))
|
||||||
|
= check_box_tag 'settings[wiki_pdf_remove_title]', 1, @settings[:wiki_pdf_remove_title].to_i == 1
|
||||||
|
em.info
|
||||||
|
= l(:wiki_pdf_remove_title_info)
|
||||||
|
p
|
||||||
|
= content_tag(:label, l(:label_wiki_pdf_remove_attachments))
|
||||||
|
= check_box_tag 'settings[wiki_pdf_remove_attachments]', 1, @settings[:wiki_pdf_remove_attachments].to_i == 1
|
||||||
|
em.info
|
||||||
|
= l(:wiki_pdf_remove_attachments_info)
|
11
plugins/additionals/app/views/additionals_macros/show.html.slim
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
h2 = l(:label_settings_macros) + " (#{@available_macros.count})"
|
||||||
|
|
||||||
|
.info = t(:label_top_macros_help_html)
|
||||||
|
br
|
||||||
|
.box
|
||||||
|
- @available_macros.each do |macro, options|
|
||||||
|
.macro-box
|
||||||
|
.macro-title
|
||||||
|
= macro.to_s
|
||||||
|
.macro-desc
|
||||||
|
pre = options[:desc]
|
11
plugins/additionals/app/views/admin/_system_info.html.slim
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
table.list
|
||||||
|
tr
|
||||||
|
td.name
|
||||||
|
= "#{l(:label_system_info)}:"
|
||||||
|
td.name
|
||||||
|
= system_info
|
||||||
|
tr
|
||||||
|
td.name
|
||||||
|
= "#{l(:label_uptime)}:"
|
||||||
|
td.name
|
||||||
|
= system_uptime
|
|
@ -0,0 +1 @@
|
||||||
|
== @tags.collect { |tag| { 'id' => tag.name, 'text' => tag.name } }.to_json
|
9
plugins/additionals/app/views/auto_completes/_additionals_users.html.erb
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
<%= raw @users.map { |user| {
|
||||||
|
'id' => user.id,
|
||||||
|
'text' => user.name,
|
||||||
|
'name' => user.name,
|
||||||
|
'name_with_icon' => user_with_avatar(user, no_link: true),
|
||||||
|
'value' => user.id
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
%>
|
|
@ -0,0 +1,11 @@
|
||||||
|
- if Additionals.setting?(:issue_freezed_with_close) && !User.current.allowed_to?(:edit_closed_issues, project)
|
||||||
|
- if @issues.detect(&:closed?)
|
||||||
|
ruby:
|
||||||
|
@safe_attributes = []
|
||||||
|
@can[:edit] = false
|
||||||
|
@can[:edit] = false
|
||||||
|
@allowed_statuses = nil
|
||||||
|
@trackers = nil
|
||||||
|
@can[:add_watchers] = nil
|
||||||
|
@can[:delete] = nil
|
||||||
|
@options_by_custom_field = []
|
1
plugins/additionals/app/views/hooks/_view_users_contextual.html.slim
Executable file
|
@ -0,0 +1 @@
|
||||||
|
= call_hook(:view_users_show_contextual, user: @user)
|
1
plugins/additionals/app/views/hooks/_view_users_show.html.slim
Executable file
|
@ -0,0 +1 @@
|
||||||
|
= call_hook(:view_users_show_info, user: @user)
|
5
plugins/additionals/app/views/issues/_additionals_action_menu.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- if User.current.logged? && @issue.editable? && Additionals.setting?(:issue_assign_to_me) && \
|
||||||
|
@issue.assigned_to_id != User.current.id && @project.assignable_users.detect { |u| u.id == User.current.id }
|
||||||
|
= link_to(font_awesome_icon('far_user-circle', post_text: l(:button_assign_to_me)),
|
||||||
|
issue_assign_to_me_path(@issue), method: :put,
|
||||||
|
class: 'assign-to-me')
|
21
plugins/additionals/app/views/issues/_additionals_sidebar.html.slim
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
- if Additionals.setting?(:issue_change_status_in_sidebar) && \
|
||||||
|
@issue && \
|
||||||
|
User.current.allowed_to?(:edit_issues, @project) && \
|
||||||
|
(!@issue.closed? || User.current.allowed_to?(:edit_closed_issues, @project))
|
||||||
|
- statuses = @issue.sidbar_change_status_allowed_to(User.current)
|
||||||
|
- if statuses.present?
|
||||||
|
h3 = l(:label_issue_change_status)
|
||||||
|
ul.issue-status-change-sidebar
|
||||||
|
- statuses.each do |s|
|
||||||
|
- if s != @issue.status
|
||||||
|
li
|
||||||
|
- if s.is_closed?
|
||||||
|
= link_to(font_awesome_icon('fas_caret-square-left', post_text: s.name),
|
||||||
|
issue_change_status_path(@issue, new_status_id: s.id),
|
||||||
|
method: :put, class: "status-switch status-#{s.id}")
|
||||||
|
- else
|
||||||
|
= link_to(font_awesome_icon('far_caret-square-left', post_text: s.name),
|
||||||
|
issue_change_status_path(@issue, new_status_id: s.id),
|
||||||
|
method: :put, class: "status-switch status-#{s.id}")
|
||||||
|
|
||||||
|
h3 = l(:label_planning)
|
9
plugins/additionals/app/views/issues/_change_author.html.slim
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
- if show_issue_change_author?(issue) && issue.safe_attribute?('author_id')
|
||||||
|
- author_options = issue_author_options_for_select(issue.project, issue)
|
||||||
|
- if author_options.present?
|
||||||
|
p#change_author
|
||||||
|
= form.label_for_field :author_id
|
||||||
|
= link_to_function content_tag(:span, l(:button_edit), class: 'icon icon-edit'), '$(this).hide(); $("#issue_author_id").show()'
|
||||||
|
= form.select :author_id, author_options, { required: true, no_label: true }, style: 'display: none'
|
||||||
|
javascript:
|
||||||
|
$('#change_author').insertBefore($('#issue_tracker_id').parent());
|
7
plugins/additionals/app/views/issues/_change_author_bulk.html.slim
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
- if @project && User.current.allowed_to?(:edit_issue_author, @project)
|
||||||
|
- author_options = issue_author_options_for_select(@project)
|
||||||
|
- if author_options.present?
|
||||||
|
p#change_author
|
||||||
|
= label_tag('issue[author_id]', l(:field_author))
|
||||||
|
= select_tag('issue[author_id]',
|
||||||
|
content_tag('option', l(:label_no_change_option), value: '') + author_options)
|
3
plugins/additionals/app/views/issues/_new_ticket_message.html.slim
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
- if @issue.new_ticket_message.present?
|
||||||
|
.nodata.nodata-left
|
||||||
|
= textilizable(@issue.new_ticket_message).html_safe
|
4
plugins/additionals/app/views/projects/_project_overview.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- project_overview_content = Additionals.settings[:project_overview_content]
|
||||||
|
- if project_overview_content.present?
|
||||||
|
.project-content.wiki.box
|
||||||
|
= textilizable(project_overview_content)
|
3
plugins/additionals/app/views/queries/_additionals_description.html.slim
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
- if @query.description?
|
||||||
|
.query-description
|
||||||
|
= textilizable @query, :description
|
17
plugins/additionals/app/views/roles/_additionals_form.html.slim
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
p
|
||||||
|
= f.check_box :hide, disabled: @role.users_visibility != 'members_of_visible_projects'
|
||||||
|
em.info
|
||||||
|
= t(:info_hidden_roles_html)
|
||||||
|
|
||||||
|
javascript:
|
||||||
|
$(function() {
|
||||||
|
$('#role_users_visibility').change(function() {
|
||||||
|
var uv = $("#role_users_visibility").val();
|
||||||
|
if (uv == 'members_of_visible_projects') {
|
||||||
|
$("#role_hide").prop('disabled', false);
|
||||||
|
} else {
|
||||||
|
$("#role_hide").prop('checked', false);
|
||||||
|
$("#role_hide").prop('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
4
plugins/additionals/app/views/users/_additionals_contextual.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- if Additionals.setting?(:new_issue_on_profile) && @memberships.present?
|
||||||
|
- project_url = memberships_new_issue_project_url(user, @memberships)
|
||||||
|
- if project_url.present?
|
||||||
|
= link_to(l(:label_issue_new), project_url, class: 'user-new-issue icon icon-add')
|
4
plugins/additionals/app/views/users/_autowatch_involved_issue.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- if Additionals.setting?(:issue_autowatch_involved) && User.current.allowed_to?(:view_issues, nil, global: true)
|
||||||
|
= labelled_fields_for :pref, @user.pref do |pref_fields|
|
||||||
|
p
|
||||||
|
= pref_fields.check_box :autowatch_involved_issue
|
5
plugins/additionals/app/views/welcome/_overview_bottom.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- overview_bottom = Additionals.settings[:overview_bottom]
|
||||||
|
- if overview_bottom.present?
|
||||||
|
.clear-both
|
||||||
|
.overview-bottom.wiki.box
|
||||||
|
= textilizable(overview_bottom)
|
5
plugins/additionals/app/views/welcome/_overview_news.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- unless Additionals.setting?(:remove_news)
|
||||||
|
.news.box
|
||||||
|
h3 = l(:label_news_latest)
|
||||||
|
= render partial: 'news/news', collection: @news
|
||||||
|
= link_to l(:label_news_view_all), news_index_path
|
4
plugins/additionals/app/views/welcome/_overview_right.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- overview_right = Additionals.settings[:overview_right]
|
||||||
|
- if overview_right.present?
|
||||||
|
.overview-right.wiki.box
|
||||||
|
= textilizable(overview_right)
|
4
plugins/additionals/app/views/welcome/_overview_top.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- overview_top = Additionals.settings[:overview_top]
|
||||||
|
- if overview_top.present?
|
||||||
|
.overview-top.wiki.box
|
||||||
|
= textilizable(overview_top)
|
17
plugins/additionals/app/views/wiki/_calendar_macros.html.slim
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
.month-calendar id="month-calendar-#{id}"
|
||||||
|
javascript:
|
||||||
|
$("#month-calendar-#{id}").datepicker({
|
||||||
|
language: "#{locale}",
|
||||||
|
calendarWeeks: #{options[:show_weeks]},
|
||||||
|
todayHighlight: true,
|
||||||
|
multidate: true,
|
||||||
|
disableTouchKeyboard: true,
|
||||||
|
defaultViewDate: {
|
||||||
|
year: #{options[:year]},
|
||||||
|
month: #{options[:month]},
|
||||||
|
day: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
- unless selected.empty?
|
||||||
|
javascript:
|
||||||
|
$('#month-calendar-#{id}').datepicker('setDates', #{selected});
|
16
plugins/additionals/app/views/wiki/_cryptocompare.html.slim
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
.cryptocompare
|
||||||
|
javascript:
|
||||||
|
var scripts = document.getElementsByTagName("script");
|
||||||
|
var embedder = scripts[scripts.length - 1];
|
||||||
|
(function() {
|
||||||
|
var appName = encodeURIComponent(window.location.hostname);
|
||||||
|
if (appName == "") {
|
||||||
|
appName = "local";
|
||||||
|
}
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.type = "text/javascript";
|
||||||
|
s.async = true;
|
||||||
|
var theUrl = "#{raw url}";
|
||||||
|
s.src = theUrl + (theUrl.indexOf("?") >= 0 ? "&" : "?") + "app=" + appName;
|
||||||
|
embedder.parentNode.appendChild(s);
|
||||||
|
})();
|
5
plugins/additionals/app/views/wiki/_global_sidebar.html.slim
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
- sidebar = Additionals.settings[:global_sidebar]
|
||||||
|
- if sidebar.present?
|
||||||
|
.sidebar-additionals
|
||||||
|
= textilizable(sidebar)
|
||||||
|
br
|
15
plugins/additionals/app/views/wiki/_project_macros.html.slim
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
.additionals-projects.box
|
||||||
|
- if list_title
|
||||||
|
h3 = list_title
|
||||||
|
ul
|
||||||
|
- @projects.each do |project|
|
||||||
|
li.project class="#{cycle('odd', 'even')}"
|
||||||
|
span[style='font-weight: bold;']
|
||||||
|
= link_to_project(project)
|
||||||
|
- if project.homepage?
|
||||||
|
' :
|
||||||
|
= link_to(project.homepage, project.homepage, @html_options)
|
||||||
|
- if with_create_issue && User.current.allowed_to?(:add_issues, project)
|
||||||
|
= link_to('',
|
||||||
|
new_project_issue_path(project_id: project),
|
||||||
|
class: 'icon icon-add', title: l(:label_issue_new))
|
4
plugins/additionals/app/views/wiki/_show_additionals.html.slim
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
- content_for :header_tags do
|
||||||
|
= stylesheet_link_tag 'bootstrap-datepicker3.standalone.min', plugin: 'additionals'
|
||||||
|
= javascript_include_tag('bootstrap-datepicker.min', plugin: 'additionals')
|
||||||
|
= bootstrap_datepicker_locale
|
6
plugins/additionals/app/views/wiki/_tradingview.html.slim
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
/! TradingView Widget BEGIN
|
||||||
|
= javascript_include_tag 'https://s3.tradingview.com/tv.js'
|
||||||
|
div[style="display: inline-block;"]
|
||||||
|
javascript:
|
||||||
|
new TradingView.widget(#{raw options.to_json});
|
||||||
|
/! TradingView Widget END
|
23
plugins/additionals/app/views/wiki/_user_macros.html.slim
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
.users.box
|
||||||
|
- if list_title
|
||||||
|
h3 = list_title
|
||||||
|
- users.each do |user|
|
||||||
|
.user.box class="#{cycle('odd', 'even')}"
|
||||||
|
div[style="float: left; display: block; margin-right: 5px;"]
|
||||||
|
= avatar(user, size: 50)
|
||||||
|
.user.line[style="font-weight: bold;"]
|
||||||
|
= link_to_user(user)
|
||||||
|
- if !user_roles.nil? && user_roles[user.id]
|
||||||
|
.user.line
|
||||||
|
= l(:field_role)
|
||||||
|
' :
|
||||||
|
= user_roles[user.id].join(', ').html_safe
|
||||||
|
.user.line
|
||||||
|
= l(:field_login)
|
||||||
|
' :
|
||||||
|
= link_to user.login, '/users/' + user.id.to_s
|
||||||
|
- unless user.pref.hide_mail
|
||||||
|
.user.line
|
||||||
|
= l(:field_mail)
|
||||||
|
' :
|
||||||
|
= mail_to(user.mail, nil, encode: 'javascript')
|
BIN
plugins/additionals/assets/images/ZeroClipboard.swf
Executable file
BIN
plugins/additionals/assets/images/jstoolbar/bt_macros.png
Executable file
After Width: | Height: | Size: 524 B |
BIN
plugins/additionals/assets/images/redmine-link.png
Executable file
After Width: | Height: | Size: 317 B |
BIN
plugins/additionals/assets/images/smileys/check.png
Executable file
After Width: | Height: | Size: 248 B |
BIN
plugins/additionals/assets/images/smileys/exclamation-red-frame.png
Executable file
After Width: | Height: | Size: 874 B |
BIN
plugins/additionals/assets/images/smileys/failure-frame.png
Executable file
After Width: | Height: | Size: 759 B |
BIN
plugins/additionals/assets/images/smileys/question-frame.png
Executable file
After Width: | Height: | Size: 925 B |
BIN
plugins/additionals/assets/images/smileys/rose.png
Executable file
After Width: | Height: | Size: 1,000 B |
BIN
plugins/additionals/assets/images/smileys/smiley-angel.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
plugins/additionals/assets/images/smileys/smiley-annoyed.png
Executable file
After Width: | Height: | Size: 862 B |
BIN
plugins/additionals/assets/images/smileys/smiley-confuse.png
Executable file
After Width: | Height: | Size: 790 B |
BIN
plugins/additionals/assets/images/smileys/smiley-cool.png
Executable file
After Width: | Height: | Size: 869 B |
BIN
plugins/additionals/assets/images/smileys/smiley-cry.png
Executable file
After Width: | Height: | Size: 820 B |
BIN
plugins/additionals/assets/images/smileys/smiley-eek.png
Executable file
After Width: | Height: | Size: 856 B |
BIN
plugins/additionals/assets/images/smileys/smiley-evil.png
Executable file
After Width: | Height: | Size: 863 B |