diff --git a/plugins/additionals/.github/workflows/linters.yml b/plugins/additionals/.github/workflows/linters.yml new file mode 100644 index 0000000..265dab6 --- /dev/null +++ b/plugins/additionals/.github/workflows/linters.yml @@ -0,0 +1,47 @@ +name: Run Linters +on: + push: + pull_request: + schedule: + - cron: '30 5 * * *' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + + - name: Set Gemfile + run: | + echo "">> Gemfile + echo "group :test do">> Gemfile + echo " gem 'pandoc-ruby', require: false" >> Gemfile + echo " gem 'rubocop', require: false" >> Gemfile + echo " gem 'rubocop-performance', require: false" >> Gemfile + echo " gem 'rubocop-rails', require: false" >> Gemfile + echo " gem 'slim_lint', require: false" >> Gemfile + echo "end">> Gemfile + + - name: Setup gems + run: | + bundle install --jobs 4 --retry 3 + + - name: Run RuboCop + run: | + bundle exec rubocop -S + + - name: Run Slim-Lint + run: | + bundle exec slim-lint app/views + if: always() + + - name: Run Brakeman + run: | + bundle exec brakeman -5 diff --git a/plugins/additionals/.github/workflows/tests.yml b/plugins/additionals/.github/workflows/tests.yml new file mode 100644 index 0000000..a24e8a5 --- /dev/null +++ b/plugins/additionals/.github/workflows/tests.yml @@ -0,0 +1,124 @@ +name: Tests +on: + push: + pull_request: + schedule: + - cron: '0 5 * * *' + +jobs: + test: + name: ${{ matrix.redmine }} ${{ matrix.db }} ruby-${{ matrix.ruby }} + runs-on: ubuntu-latest + + strategy: + matrix: + ruby: ['2.6', '2.4'] + redmine: ['4.1-stable', 'master'] + db: ['postgres', 'mysql'] + fail-fast: false + + services: + postgres: + image: postgres:13 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: 'BestPasswordEver' + ports: + # will assign a random free host port + - 3306/tcp + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - name: Verify MySQL connection from host + run: | + mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports[3306] }} -uroot -pBestPasswordEver -e "SHOW DATABASES" + if: matrix.db == 'mysql' + + - name: Checkout Redmine + uses: actions/checkout@v2 + with: + repository: redmine/redmine + ref: ${{ matrix.redmine }} + path: redmine + + - name: Checkout additionals + uses: actions/checkout@v2 + with: + repository: AlphaNodes/additionals + path: redmine/plugins/additionals + + - name: Update package archives + run: sudo apt-get update --yes --quiet + + - name: Install package dependencies + run: > + sudo apt-get install --yes --quiet + build-essential + cmake + libicu-dev + libpq-dev + libmysqlclient-dev + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - name: Prepare Redmine source + working-directory: redmine + run: | + sed -i '/rubocop/d' Gemfile + rm -f .rubocop* + cp plugins/additionals/test/support/database-${{ matrix.db }}.yml config/database.yml + cp plugins/additionals/test/support/configuration.yml config/configuration.yml + + - name: Install Ruby dependencies + working-directory: redmine + run: | + bundle install --jobs=4 --retry=3 --without development + + - name: Run Redmine rake tasks + env: + RAILS_ENV: test + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + working-directory: redmine + run: | + bundle exec rake generate_secret_token + bundle exec rake db:create db:migrate redmine:plugins:migrate + bundle exec rake db:test:prepare + + - name: Run tests + env: + RAILS_ENV: test + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + working-directory: redmine + run: bundle exec rake redmine:plugins:test NAME=additionals RUBYOPT="-W0" + + - name: Run uninstall test + env: + RAILS_ENV: test + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + working-directory: redmine + run: bundle exec rake redmine:plugins:migrate NAME=additionals VERSION=0 + + - name: Run build gem file + working-directory: redmine/plugins/additionals + run: gem build additionals.gemspec diff --git a/plugins/additionals/.gitignore b/plugins/additionals/.gitignore index a889413..2469bbe 100755 --- a/plugins/additionals/.gitignore +++ b/plugins/additionals/.gitignore @@ -1,8 +1,11 @@ .DS_Store .buildpath +*.gem coverage/ tmp/ +Gemfile.lock .project +.vscode .settings/ docs/_build docs/_static diff --git a/plugins/additionals/.rubocop.yml b/plugins/additionals/.rubocop.yml index 51b9c7e..5282b1c 100755 --- a/plugins/additionals/.rubocop.yml +++ b/plugins/additionals/.rubocop.yml @@ -70,3 +70,12 @@ Style/HashTransformKeys: Style/HashTransformValues: Enabled: false + +Naming/VariableNumber: + Enabled: true + Exclude: + - 'test/**/*' + +Style/StringConcatenation: + Exclude: + - 'app/views/additionals/_select2_ajax_call.*' diff --git a/plugins/additionals/.slim-lint.yml b/plugins/additionals/.slim-lint.yml index 54689e6..6579ae1 100755 --- a/plugins/additionals/.slim-lint.yml +++ b/plugins/additionals/.slim-lint.yml @@ -4,15 +4,10 @@ linters: RuboCop: ignored_cops: - Layout/ArgumentAlignment - - Layout/ArrayAlignment - Layout/BlockEndNewline - Layout/EmptyLineAfterGuardClause - Layout/HashAlignment - - Layout/IndentationConsistency - Layout/IndentationWidth - - Layout/IndentFirstArgument - - Layout/IndentFirstArrayElement - - Layout/IndentFirstHashElement - Layout/MultilineArrayBraceLayout - Layout/MultilineAssignmentLayout - Layout/MultilineBlockLayout @@ -21,19 +16,11 @@ linters: - Layout/MultilineMethodCallIndentation - Layout/MultilineMethodDefinitionBraceLayout - Layout/MultilineOperationIndentation - - Layout/TrailingBlankLines + - Layout/SpaceBeforeBrackets - Layout/TrailingEmptyLines - - 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 diff --git a/plugins/additionals/.travis.yml b/plugins/additionals/.travis.yml deleted file mode 100755 index f826712..0000000 --- a/plugins/additionals/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: ruby -os: linux -dist: xenial - -rvm: - - 2.6.6 - - 2.5.8 - - 2.4.10 - -services: - - mysql - - postgresql - -env: - - REDMINE_VER=4.1-stable DB=postgresql - - REDMINE_VER=master DB=postgresql - - REDMINE_VER=4.1-stable DB=mysql - - REDMINE_VER=master DB=mysql - -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 - - sed -i '/rubocop/d' $REDMINE_PATH/Gemfile - - rm -f $REDMINE_PATH/.rubocop* - - cp $TRAVIS_BUILD_DIR/test/support/Gemfile.local $REDMINE_PATH - - 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: - - bundle exec rake db:create db:migrate redmine:plugins:migrate - -script: - - if [[ "$REDMINE_VER" != "master" ]] && [[ "$DB" == "postgresql" ]]; then brakeman plugins/$PLUGIN_NAME; fi - - if [[ "$REDMINE_VER" != "master" ]] && [[ "$DB" == "postgresql" ]]; then rubocop plugins/$PLUGIN_NAME; fi - - bundle exec rake redmine:plugins:test NAME=$PLUGIN_NAME RUBYOPT="-W0" - - bundle exec rake redmine:plugins:migrate NAME=$PLUGIN_NAME VERSION=0 diff --git a/plugins/additionals/CHANGELOG.rst b/plugins/additionals/CHANGELOG.rst index 7e6afa6..ff3b968 100755 --- a/plugins/additionals/CHANGELOG.rst +++ b/plugins/additionals/CHANGELOG.rst @@ -1,6 +1,33 @@ Changelog ========= +3.0.2 ++++++ + +- d3plus to v2.0.0-alpha.30 support +- Mermaid 8.9.1 support +- Bug fix for select2 loading without named field +- FontAwesome 5.15.2 support +- D3 6.6.0 support +- Fix news limit for welcome dashboard block +- Frensh translation updated, thanks to Brice BEAUMESNIL! +- clipboard.js updated to v2.0.8 +- +3.0.1 ++++++ + +- Do not show "Assign to me" if assigned_to is disabled for tracker +- FontAwesome 5.15.1 support +- D3 6.3.1 support +- Mermaid 8.8.4 support +- add current_user as special login name for user macro (which shows current login user) +- add text parameter to user macro (which disable link to user) +- add asynchronous text block +- gemify plugin to use it with Gemfile.local or other plugins +- remove spam protection functionality +- Chart.js 2.9.4 support +- Allow overwrite mermaid theme and variables + 3.0.0 +++++ diff --git a/plugins/additionals/Gemfile b/plugins/additionals/Gemfile index 4288b35..96be6e8 100755 --- a/plugins/additionals/Gemfile +++ b/plugins/additionals/Gemfile @@ -1,13 +1,4 @@ source 'https://rubygems.org' -gem 'deface', '1.5.3' -gem 'gemoji', '~> 3.0.0' -gem 'invisible_captcha' -gem 'render_async' -gem 'rss' -gem 'slim-rails' - -group :development, :test do - gem 'brakeman', require: false - gem 'slim_lint', require: false -end +# Specify your gem's dependencies in additionals.gemspec +gemspec diff --git a/plugins/additionals/README.rst b/plugins/additionals/README.rst deleted file mode 100644 index d46ade8..0000000 --- a/plugins/additionals/README.rst +++ /dev/null @@ -1 +0,0 @@ -docs/index.rst \ No newline at end of file diff --git a/plugins/additionals/Rakefile b/plugins/additionals/Rakefile new file mode 100644 index 0000000..39a5d23 --- /dev/null +++ b/plugins/additionals/Rakefile @@ -0,0 +1,11 @@ +require 'bundler/gem_tasks' +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs << 'test' + files = FileList['test/**/*test.rb'] + t.test_files = files + t.verbose = true +end + +task default: :test diff --git a/plugins/additionals/additionals.gemspec b/plugins/additionals/additionals.gemspec new file mode 100644 index 0000000..9c5d6ca --- /dev/null +++ b/plugins/additionals/additionals.gemspec @@ -0,0 +1,30 @@ +lib = File.expand_path '../lib', __FILE__ +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'additionals/version' + +Gem::Specification.new do |spec| + spec.name = 'additionals' + spec.version = Additionals::VERSION + spec.authors = ['AlphaNodes'] + spec.email = ['alex@alphanodes.com'] + + spec.summary = 'Redmine plugin for adding dashboard functionality, wiki macros and libraries for other Redmine plugins' + spec.description = 'Redmine plugin for adding dashboard functionality, wiki macros and libraries for other Redmine plugins' + spec.homepage = 'https://github.com/alphanodes/alphanodes' + spec.license = 'GPL-2.0' + + spec.files = Dir['**/*'] - Dir['test/**/*'] - Dir['Gemfile', 'Gemfile.lock', 'README.rst'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.4' + + spec.add_runtime_dependency 'deface', '1.5.3' + spec.add_runtime_dependency 'gemoji', '~> 3.0.0' + spec.add_runtime_dependency 'render_async' + spec.add_runtime_dependency 'rss' + spec.add_runtime_dependency 'slim-rails' + + spec.add_development_dependency 'brakeman' + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'slim_lint' +end diff --git a/plugins/additionals/app/controllers/additionals_change_status_controller.rb b/plugins/additionals/app/controllers/additionals_change_status_controller.rb index ed1a481..f82c420 100755 --- a/plugins/additionals/app/controllers/additionals_change_status_controller.rb +++ b/plugins/additionals/app/controllers/additionals_change_status_controller.rb @@ -13,7 +13,7 @@ class AdditionalsChangeStatusController < ApplicationController return end - @issue.init_journal(User.current) + @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 diff --git a/plugins/additionals/app/controllers/dashboard_async_blocks_controller.rb b/plugins/additionals/app/controllers/dashboard_async_blocks_controller.rb index d5e7f6a..fb98457 100644 --- a/plugins/additionals/app/controllers/dashboard_async_blocks_controller.rb +++ b/plugins/additionals/app/controllers/dashboard_async_blocks_controller.rb @@ -6,7 +6,6 @@ class DashboardAsyncBlocksController < ApplicationController helper :additionals_routes helper :additionals_queries - helper :additionals_tag helper :queries helper :issues helper :activities @@ -14,6 +13,12 @@ class DashboardAsyncBlocksController < ApplicationController include DashboardsHelper + # support for redmine_contacts_helpdesk plugin + if Redmine::Plugin.installed? 'redmine_contacts_helpdesk' + include HelpdeskHelper + helper :helpdesk + end + rescue_from Query::StatementInvalid, with: :query_statement_invalid rescue_from StandardError, with: :dashboard_with_invalid_block diff --git a/plugins/additionals/app/controllers/dashboards_controller.rb b/plugins/additionals/app/controllers/dashboards_controller.rb index 4ab51b8..cef7616 100644 --- a/plugins/additionals/app/controllers/dashboards_controller.rb +++ b/plugins/additionals/app/controllers/dashboards_controller.rb @@ -17,7 +17,6 @@ class DashboardsController < ApplicationController helper :dashboards helper :additionals_issues helper :additionals_queries - helper :additionals_tag include AdditionalsRoutesHelper include AdditionalsQueriesHelper @@ -92,7 +91,7 @@ class DashboardsController < ApplicationController respond_to do |format| format.html - format.xml {} + format.api end end diff --git a/plugins/additionals/app/helpers/additionals_chartjs_helper.rb b/plugins/additionals/app/helpers/additionals_chartjs_helper.rb index 6eee28b..bb3f41b 100644 --- a/plugins/additionals/app/helpers/additionals_chartjs_helper.rb +++ b/plugins/additionals/app/helpers/additionals_chartjs_helper.rb @@ -6,7 +6,7 @@ module AdditionalsChartjsHelper end def select_options_for_chartjs_colorscheme(selected) - data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/config/colorschemes.yml'))).result) || {} + data = YAML.safe_load(ERB.new(IO.read(File.join(Additionals.plugin_dir, 'config', 'colorschemes.yml'))).result) || {} grouped_options_for_select(data, selected) end end diff --git a/plugins/additionals/app/helpers/additionals_clipboardjs_helper.rb b/plugins/additionals/app/helpers/additionals_clipboardjs_helper.rb index 61fe55c..b49cc1d 100644 --- a/plugins/additionals/app/helpers/additionals_clipboardjs_helper.rb +++ b/plugins/additionals/app/helpers/additionals_clipboardjs_helper.rb @@ -3,20 +3,34 @@ module AdditionalsClipboardjsHelper render_clipboardjs_button(target, clipboard_text_from_button) + render_clipboardjs_javascript(target) end + def render_text_with_clipboardjs(text) + return if text.blank? + + tag.acronym text, + class: 'clipboard-text', + title: l(:label_copy_to_clipboard), + data: clipboardjs_data(text: text) + end + + def clipboardjs_data(clipboard_data) + data = { 'label-copied' => l(:label_copied_to_clipboard), + 'label-to-copy' => l(:label_copy_to_clipboard) } + + clipboard_data.each do |key, value| + data["clipboard-#{key}"] = value if value.present? + end + + data + end + private def render_clipboardjs_button(target, clipboard_text_from_button) - data = { 'clipboard-target' => "##{target}", - 'label-copied' => l(:label_copied_to_clipboard), - 'label-to-copy' => l(:label_copy_to_clipboard) } - - data['clipboard-text'] = clipboard_text_from_button if clipboard_text_from_button.present? - tag.button id: "zc_#{target}", - class: 'clipboard_button far fa-copy', + class: 'clipboard-button far fa-copy', type: 'button', title: l(:label_copy_to_clipboard), - data: data + data: clipboardjs_data(target: "##{target}", text: clipboard_text_from_button) end def render_clipboardjs_javascript(target) diff --git a/plugins/additionals/app/helpers/additionals_menu_helper.rb b/plugins/additionals/app/helpers/additionals_menu_helper.rb index fabdf9f..ceee70f 100755 --- a/plugins/additionals/app/helpers/additionals_menu_helper.rb +++ b/plugins/additionals/app/helpers/additionals_menu_helper.rb @@ -100,12 +100,9 @@ module AdditionalsMenuHelper 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 + if user_roles.empty? && role.to_i == Role::BUILTIN_ANONYMOUS || + # if user is logged in and non_member is active in item, always show it + User.current.logged? && role.to_i == Role::BUILTIN_NON_MEMBER show_entry = true break end diff --git a/plugins/additionals/app/helpers/additionals_queries_helper.rb b/plugins/additionals/app/helpers/additionals_queries_helper.rb index 3b0c4df..4c357af 100755 --- a/plugins/additionals/app/helpers/additionals_queries_helper.rb +++ b/plugins/additionals/app/helpers/additionals_queries_helper.rb @@ -4,8 +4,8 @@ module AdditionalsQueriesHelper 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") + 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? || @@ -28,7 +28,7 @@ module AdditionalsQueriesHelper 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)) + 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], @@ -79,11 +79,11 @@ module AdditionalsQueriesHelper def additionals_select2_search_users(options = {}) q = params[:q].to_s.strip exclude_id = params[:user_id].to_i - scope = User.active.where(type: 'User') + scope = User.active.where type: 'User' scope = scope.visible unless options[:all_visible] scope = scope.where.not(id: exclude_id) if exclude_id.positive? scope = scope.where(options[:where_filter], options[:where_params]) if options[:where_filter] - q.split(' ').map { |search_string| scope = scope.like(search_string) } if q.present? + q.split.map { |search_string| scope = scope.like(search_string) } if q.present? scope = scope.order(last_login_on: :desc) .limit(Additionals::SELECT2_INIT_ENTRIES) @users = scope.to_a.sort! { |x, y| x.name <=> y.name } @@ -140,13 +140,13 @@ module AdditionalsQueriesHelper def xlsx_write_header_row(workbook, worksheet, columns) columns_width = [] columns.each_with_index do |c, index| - value = if c.class.name == 'String' + value = if c.is_a? String c else c.caption.to_s end - worksheet.write(0, index, value, workbook.add_format(xlsx_cell_format(:header))) + worksheet.write 0, index, value, workbook.add_format(xlsx_cell_format(:header)) columns_width << xlsx_get_column_width(value) end columns_width @@ -224,13 +224,11 @@ module AdditionalsQueriesHelper def xlsx_hyperlink_cell?(token) # Match http, https or ftp URL - if %r{\A[fh]tt?ps?://}.match?(token) - true - # Match mailto: - elsif token.present? && token.start_with?('mailto:') - true - # Match internal or external sheet link - elsif /\A(?:in|ex)ternal:/.match?(token) + if %r{\A[fh]tt?ps?://}.match?(token) || + # Match mailto: + token.present? && token.start_with?('mailto:') || + # Match internal or external sheet link + /\A(?:in|ex)ternal:/.match?(token) true end end diff --git a/plugins/additionals/app/helpers/additionals_select2_helper.rb b/plugins/additionals/app/helpers/additionals_select2_helper.rb index 5ad1931..562c3e6 100644 --- a/plugins/additionals/app/helpers/additionals_select2_helper.rb +++ b/plugins/additionals/app/helpers/additionals_select2_helper.rb @@ -1,9 +1,10 @@ module AdditionalsSelect2Helper def additionals_select2_tag(name, option_tags = nil, options = {}) s = select_tag(name, option_tags, options) + id = options.delete(:id) || sanitize_to_id(name) s << hidden_field_tag("#{name}[]", '') if options[:multiple] && options.fetch(:include_hidden, true) - s + javascript_tag("select2Tag('#{sanitize_to_id name}', #{options.to_json});") + s + javascript_tag("select2Tag('#{id}', #{options.to_json});") end # Transforms select filters of +type+ fields into select2 diff --git a/plugins/additionals/app/helpers/additionals_settings_helper.rb b/plugins/additionals/app/helpers/additionals_settings_helper.rb index d3e01c4..07b1f26 100644 --- a/plugins/additionals/app/helpers/additionals_settings_helper.rb +++ b/plugins/additionals/app/helpers/additionals_settings_helper.rb @@ -4,7 +4,6 @@ module AdditionalsSettingsHelper { name: 'wiki', partial: 'additionals/settings/wiki', label: :label_wiki }, { name: 'macros', partial: 'additionals/settings/macros', label: :label_macro_plural }, { name: 'rules', partial: 'additionals/settings/issues', label: :label_issue_plural }, - { name: 'users', partial: 'additionals/settings/users', label: :label_user_plural }, { name: 'web', partial: 'additionals/settings/web_apis', label: :label_web_apis }] unless Redmine::Plugin.installed? 'redmine_hrm' @@ -46,6 +45,22 @@ module AdditionalsSettingsHelper text_field_tag("settings[#{name}]", value, options)] end + def additionals_settings_passwordfield(name, options = {}) + label_title = options.delete(:label).presence || l("label_#{name}") + value = options.delete(:value).presence || @settings[name] + + safe_join [label_tag("settings[#{name}]", label_title), + password_field_tag("settings[#{name}]", value, options)] + end + + def additionals_settings_urlfield(name, options = {}) + label_title = options.delete(:label).presence || l("label_#{name}") + value = options.delete(:value).presence || @settings[name] + + safe_join [label_tag("settings[#{name}]", label_title), + url_field_tag("settings[#{name}]", value, options)] + end + def additionals_settings_textarea(name, options = {}) label_title = options.delete(:label).presence || l("label_#{name}") value = options.delete(:value).presence || @settings[name] diff --git a/plugins/additionals/app/helpers/additionals_tag_helper.rb b/plugins/additionals/app/helpers/additionals_tag_helper.rb deleted file mode 100755 index 6c5c8d0..0000000 --- a/plugins/additionals/app/helpers/additionals_tag_helper.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'digest/md5' - -module AdditionalsTagHelper - 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 - - 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_object, options = {}) - tag_name = [] - tag_name << tag_object.name - - unless options[:tags_without_color] - tag_bg_color = additionals_tag_color tag_object.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 << tag.span("(#{tag_object.count})", class: 'tag-count') if options[:show_count] - - if options[:tags_without_color] - tag.span link_to(safe_join(tag_name), additionals_tag_url(tag_object.name, options)), - class: 'tag-label' - else - tag.span link_to(safe_join(tag_name), - additionals_tag_url(tag_object.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: options[: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 diff --git a/plugins/additionals/app/helpers/dashboards_helper.rb b/plugins/additionals/app/helpers/dashboards_helper.rb index 17a54a5..937e676 100644 --- a/plugins/additionals/app/helpers/dashboards_helper.rb +++ b/plugins/additionals/app/helpers/dashboards_helper.rb @@ -86,10 +86,10 @@ module DashboardsHelper tag.div class: 'active-dashboards' do out = [tag.h3(l(:label_active_dashboard)), tag.ul do - concat tag.ul "#{l :field_name}: #{dashboard.name}" - concat tag.ul safe_join([l(:field_author), link_to_user(dashboard.author)], ': ') - concat tag.ul "#{l :field_created_on}: #{format_time dashboard.created_at}" - concat tag.ul "#{l :field_updated_on}: #{format_time dashboard.updated_at}" + concat tag.li "#{l :field_name}: #{dashboard.name}" + concat tag.li safe_join([l(:field_author), link_to_user(dashboard.author)], ': ') + concat tag.li "#{l :field_created_on}: #{format_time dashboard.created_at}" + concat tag.li "#{l :field_updated_on}: #{format_time dashboard.updated_at}" end] if dashboard.description.present? @@ -105,7 +105,7 @@ module DashboardsHelper return '' unless dashboards.any? tag.h3(title, class: 'dashboards') + - tag.ul do + tag.ul(class: 'dashboards') do dashboards.each do |dashboard| selected = dashboard.id == if params[:dashboard_id].present? params[:dashboard_id].to_i @@ -115,19 +115,24 @@ module DashboardsHelper css = 'dashboard' css << ' selected' if selected + li_class = nil + link = [dashboard_link(dashboard, project, class: css)] if dashboard.system_default? link << if dashboard.project_id.nil? - font_awesome_icon('fas_cube', + li_class = 'global' + font_awesome_icon 'fas_cube', title: l(:field_system_default), - class: 'dashboard-system-default global') + class: "dashboard-system-default #{li_class}" else - font_awesome_icon('fas_cube', + li_class = 'project' + font_awesome_icon 'fas_cube', title: l(:field_project_system_default), - class: 'dashboard-system-default project') + class: "dashboard-system-default #{li_class}" end end - concat tag.li safe_join(link) + + concat tag.li safe_join(link), class: li_class end end end @@ -325,7 +330,7 @@ module DashboardsHelper max_entries = settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES news = if dashboard.content_project.nil? - News.latest User.current + News.latest User.current, max_entries else dashboard.content_project .news @@ -378,7 +383,7 @@ module DashboardsHelper max_entries = max_entries.present? ? max_entries.to_i : 10 begin - URI.open(url) do |rss_feed| + URI.parse(url).open do |rss_feed| rss = RSS::Parser.parse(rss_feed) rss.items.each do |item| cnt += 1 diff --git a/plugins/additionals/app/jobs/additionals_remove_unused_tag_job.rb b/plugins/additionals/app/jobs/additionals_remove_unused_tag_job.rb deleted file mode 100644 index 7453c76..0000000 --- a/plugins/additionals/app/jobs/additionals_remove_unused_tag_job.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AdditionalsRemoveUnusedTagJob < AdditionalsJob - def perform - AdditionalsTag.remove_unused_tags - end -end diff --git a/plugins/additionals/app/models/additionals_chart.rb b/plugins/additionals/app/models/additionals_chart.rb index ed1fdc5..84a27ad 100644 --- a/plugins/additionals/app/models/additionals_chart.rb +++ b/plugins/additionals/app/models/additionals_chart.rb @@ -1,4 +1,5 @@ -class AdditionalsChart < ActiveRecord::Base +class AdditionalsChart + include ActiveRecord::Sanitization include Redmine::I18n CHART_DEFAULT_HEIGHT = 350 diff --git a/plugins/additionals/app/models/additionals_font_awesome.rb b/plugins/additionals/app/models/additionals_font_awesome.rb index ead1529..a1e1622 100755 --- a/plugins/additionals/app/models/additionals_font_awesome.rb +++ b/plugins/additionals/app/models/additionals_font_awesome.rb @@ -6,7 +6,7 @@ class AdditionalsFontAwesome class << self def load_icons(type) - data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/config/fontawesome_icons.yml'))).result) || {} + data = YAML.safe_load(ERB.new(IO.read(File.join(Additionals.plugin_dir, '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)) diff --git a/plugins/additionals/app/models/additionals_import.rb b/plugins/additionals/app/models/additionals_import.rb index 0b4c406..f8d31a8 100755 --- a/plugins/additionals/app/models/additionals_import.rb +++ b/plugins/additionals/app/models/additionals_import.rb @@ -31,7 +31,7 @@ class AdditionalsImport < Import next unless value h[v.custom_field.id.to_s] = - if value.is_a?(Array) + if value.is_a? Array value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.flatten!&.compact else v.custom_field.value_from_keyword(value, object) diff --git a/plugins/additionals/app/models/additionals_info.rb b/plugins/additionals/app/models/additionals_info.rb new file mode 100644 index 0000000..b4691b3 --- /dev/null +++ b/plugins/additionals/app/models/additionals_info.rb @@ -0,0 +1,73 @@ +class AdditionalsInfo + include Redmine::I18n + + class << self + def system_infos + { system_info: { label: l(:label_system_info), value: system_info }, + system_uptime: { label: l(:label_uptime), value: system_uptime } } + end + + def system_info + if windows_platform? + win_info = `wmic os get Caption,CSDVersion,BuildNumber /value` + return 'unknown' if win_info.blank? + + windows_version = '' + windows_build = '' + build_names = %w[BuildNumber CSDVersion] + win_info.split(/\n+/).each do |line| + line_info = line.split '=' + if line_info[0] == 'Caption' + windows_version = line_info[1] + elsif build_names.include?(line_info[0]) && line_info[1]&.present? + windows_build = line_info[1] + end + end + "#{windows_version} build #{windows_build}" + else + `uname -a` + end + end + + def system_uptime(format: :time_tag) + if windows_platform? + `net stats srv | find "Statist"` + elsif File.exist? '/proc/uptime' + secs = `cat /proc/uptime`.to_i + min = 0 + hours = 0 + days = 0 + if secs.positive? + min = (secs / 60).round + hours = (secs / 3_600).round + days = (secs / 86_400).round + end + if days >= 1 + "#{days} #{l(:days, count: days)}" + elsif hours >= 1 + "#{hours} #{l(:hours, count: hours)}" + else + "#{min} #{l(:minutes, count: min)}" + end + else + # this should be work on macOS + seconds = `sysctl -n kern.boottime | awk '{print $4}'`.tr ',', '' + so = DateTime.strptime seconds.strip, '%s' + if so.present? + if format == :datetime + so + else + ApplicationController.helpers.time_tag so + end + else + days = `uptime | awk '{print $3}'`.to_i.round + "#{days} #{l(:days, count: days)}" + end + end + end + + def windows_platform? + /cygwin|mswin|mingw|bccwin|wince|emx/.match? RUBY_PLATFORM + end + end +end diff --git a/plugins/additionals/app/models/additionals_query.rb b/plugins/additionals/app/models/additionals_query.rb index 6b389ff..e9d773e 100755 --- a/plugins/additionals/app/models/additionals_query.rb +++ b/plugins/additionals/app/models/additionals_query.rb @@ -22,6 +22,17 @@ module AdditionalsQuery sql.join(' AND ') end + def fix_sql_for_text_field(field, operator, value, table_name = nil, target_field = nil) + table_name = queried_table_name if table_name.blank? + target_field = field if target_field.blank? + + sql = [] + sql << "(#{sql_for_field(field, operator, value, table_name, target_field)})" + sql << "#{table_name}.#{target_field} != ''" if operator == '*' + + sql.join(' AND ') + end + def initialize_ids_filter(options = {}) if options[:label] add_available_filter 'ids', type: :integer, label: options[:label] @@ -44,30 +55,44 @@ module AdditionalsQuery end end + def sql_for_project_identifier_field(field, operator, values) + value = values.first + values = value.split(',').map(&:strip) if ['=', '!'].include?(operator) && value.include?(',') + sql_for_field field, operator, values, Project.table_name, 'identifier' + end + def sql_for_project_status_field(field, operator, value) sql_for_field field, operator, value, Project.table_name, 'status' end - def initialize_project_status_filter - return if project&.leaf? + def initialize_project_identifier_filter + return if project - add_available_filter('project.status', + add_available_filter 'project.identifier', + type: :string, + name: l(:label_attribute_of_project, name: l(:field_identifier)) + end + + def initialize_project_status_filter + return if project + + add_available_filter 'project.status', type: :list, name: l(:label_attribute_of_project, name: l(:field_status)), - values: -> { project_statuses_values }) + values: -> { project_statuses_values } end def initialize_project_filter(options = {}) if project.nil? || options[:always] - add_available_filter('project_id', order: options[:position], + add_available_filter 'project_id', order: options[:position], type: :list, - values: -> { project_values }) + values: -> { project_values } end return if project.nil? || project.leaf? || subproject_values.empty? - add_available_filter('subproject_id', order: options[:position], + add_available_filter 'subproject_id', order: options[:position], type: :list_subprojects, - values: -> { subproject_values }) + values: -> { subproject_values } end def initialize_created_filter(options = {}) @@ -83,16 +108,9 @@ module AdditionalsQuery 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] } + type: :list_optional, + values: -> { tag_values(project) } end def initialize_approved_filter @@ -106,27 +124,35 @@ module AdditionalsQuery end def initialize_author_filter(options = {}) - return if author_values.empty? - - add_available_filter('author_id', order: options[:position], + add_available_filter 'author_id', order: options[:position], type: :list_optional, - values: options[:no_lambda].nil? ? author_values : -> { author_values }) + values: -> { author_values } end def initialize_assignee_filter(options = {}) - return if author_values.empty? - - add_available_filter('assigned_to_id', order: options[:position], + add_available_filter 'assigned_to_id', order: options[:position], type: :list_optional, - values: options[:no_lambda] ? assigned_to_all_values : -> { assigned_to_all_values }) + values: -> { assigned_to_all_values } end def initialize_watcher_filter(options = {}) - return if watcher_values.empty? || !User.current.logged? + return unless User.current.logged? - add_available_filter('watcher_id', order: options[:position], + add_available_filter 'watcher_id', order: options[:position], type: :list, - values: options[:no_lambda] ? watcher_values : -> { watcher_values }) + values: -> { watcher_values } + end + + def tag_values(project) + values = if project + queried_class.available_tags project: project.id + else + queried_class.available_tags + end + + return [] if values.blank? + + values.collect { |t| [t.name, t.name] } end # issue independend values. Use assigned_to_values from Redmine, if you want it only for issues @@ -154,7 +180,7 @@ module AdditionalsQuery end def sql_for_tags_field(field, _operator, value) - AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value) + AdditionalTags.sql_for_tags_field queried_class, operator_for(field), value end def sql_for_is_private_field(_field, operator, value) diff --git a/plugins/additionals/app/models/additionals_tag.rb b/plugins/additionals/app/models/additionals_tag.rb deleted file mode 100755 index db9c893..0000000 --- a/plugins/additionals/app/models/additionals_tag.rb +++ /dev/null @@ -1,76 +0,0 @@ -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 - - class << self - def all_type_tags(klass, options = {}) - RedmineCrm::Tag.where({}) - .joins(tag_joins(klass, options)) - .distinct - .order("#{TAG_TABLE_NAME}.name") - end - - def 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.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count") - .joins(tag_joins(klass, options)) - .group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0') - .order("#{TAG_TABLE_NAME}.name") - end - - def remove_unused_tags - RedmineCrm::Tag.where.not(id: RedmineCrm::Tagging.select(:tag_id).distinct) - .each(&:destroy) - end - - def 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 - - private - - def tag_access(permission) - projects_allowed = if permission.nil? - Project.visible.ids - else - Project.where(Project.allowed_to_condition(User.current, permission)).ids - end - - if projects_allowed.present? - "#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join ','})" unless projects_allowed.empty? - else - '1=0' - end - end - - def 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 - end -end diff --git a/plugins/additionals/app/models/dashboard.rb b/plugins/additionals/app/models/dashboard.rb index ab4f9d7..41b5efe 100644 --- a/plugins/additionals/app/models/dashboard.rb +++ b/plugins/additionals/app/models/dashboard.rb @@ -4,6 +4,7 @@ class Dashboard < ActiveRecord::Base include Additionals::EntityMethods class SystemDefaultChangeException < StandardError; end + class ProjectSystemDefaultChangeException < StandardError; end belongs_to :project @@ -302,7 +303,10 @@ class Dashboard < ActiveRecord::Base config = { dashboard_id: id, block: block } - settings[:user_id] = User.current.id if !options.key?(:skip_user_id) || !options[:skip_user_id] + if !options.key?(:skip_user_id) || !options[:skip_user_id] + settings[:user_id] = User.current.id + settings[:user_is_admin] = User.current.admin? + end if settings.present? settings.each do |key, setting| @@ -321,11 +325,11 @@ class Dashboard < ActiveRecord::Base unique_params = settings.flatten unique_params += options[:unique_params].reject(&:blank?) if options[:unique_params].present? - Rails.logger.debug "debug async_params for #{block}: unique_params=#{unique_params.inspect}" + # Rails.logger.debug "debug async_params for #{block}: unique_params=#{unique_params.inspect}" config[:unique_key] = Digest::SHA256.hexdigest(unique_params.join('_')) end - Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}" + # Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}" config end @@ -358,7 +362,10 @@ class Dashboard < ActiveRecord::Base end def validate_system_default - return if new_record? || system_default_was == system_default || system_default? + return if new_record? || + system_default_was == system_default || + system_default? || + project_id.present? raise SystemDefaultChangeException end diff --git a/plugins/additionals/app/models/dashboard_content.rb b/plugins/additionals/app/models/dashboard_content.rb index a03c93c..43005d7 100644 --- a/plugins/additionals/app/models/dashboard_content.rb +++ b/plugins/additionals/app/models/dashboard_content.rb @@ -41,9 +41,13 @@ class DashboardContent with_project: true }, max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS }, - 'text' => { label: l(:label_text), + 'text' => { label: l(:label_text_sync), max_occurs: MAX_MULTIPLE_OCCURS, partial: 'dashboards/blocks/text' }, + 'text_async' => { label: l(:label_text_async), + max_occurs: MAX_MULTIPLE_OCCURS, + async: { required_settings: %i[text], + partial: 'dashboards/blocks/text_async' } }, 'news' => { label: l(:label_news_latest), permission: :view_news }, 'documents' => { label: l(:label_document_plural), diff --git a/plugins/additionals/app/models/dashboard_role.rb b/plugins/additionals/app/models/dashboard_role.rb index 9ecfff7..f174f50 100644 --- a/plugins/additionals/app/models/dashboard_role.rb +++ b/plugins/additionals/app/models/dashboard_role.rb @@ -3,4 +3,7 @@ class DashboardRole < ActiveRecord::Base belongs_to :dashboard belongs_to :role + + validates :dashboard, :role, + presence: true end diff --git a/plugins/additionals/app/overrides/account/register.rb b/plugins/additionals/app/overrides/account/register.rb deleted file mode 100755 index 419a744..0000000 --- a/plugins/additionals/app/overrides/account/register.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new virtual_path: 'account/register', - name: 'add-invisble-captcha', - insert_top: 'div.box', - original: 'a9c303821376a8d83cba32654629d71cc3926a1d', - partial: 'account/invisible_captcha' diff --git a/plugins/additionals/app/overrides/contacts/form.rb b/plugins/additionals/app/overrides/contacts/form.rb index a1e8a9b..364e0bf 100644 --- a/plugins/additionals/app/overrides/contacts/form.rb +++ b/plugins/additionals/app/overrides/contacts/form.rb @@ -1,8 +1,15 @@ unless Redmine::Plugin.installed? 'redmine_servicedesk' - # redmine_contacts does not provide hook - Deface::Override.new virtual_path: 'contacts/_form', - name: 'contacts-form-hook', - insert_bottom: 'div#contact_data', - original: 'df6cae24cfd26e5299c45c427fbbd4e5f23c313e', - partial: 'hooks/view_contacts_form' + if defined?(CONTACTS_VERSION_TYPE) && CONTACTS_VERSION_TYPE == 'PRO version' + Deface::Override.new virtual_path: 'contacts/_form', + name: 'contacts-pro-form-hook', + insert_bottom: 'div#contact_data', + original: 'df6cae24cfd26e5299c45c427fbbd4e5f23c313e', + partial: 'hooks/view_contacts_form' + else + Deface::Override.new virtual_path: 'contacts/_form', + name: 'contacts-form-hook', + insert_bottom: 'div#contact_data', + original: '217049684a0bcd7e404dc6b5b2348aae47ac8a72', + partial: 'hooks/view_contacts_form' + end end diff --git a/plugins/additionals/app/overrides/wiki/edit.rb b/plugins/additionals/app/overrides/wiki/edit.rb new file mode 100644 index 0000000..ae9be74 --- /dev/null +++ b/plugins/additionals/app/overrides/wiki/edit.rb @@ -0,0 +1,5 @@ +Deface::Override.new virtual_path: 'wiki/edit', + name: 'wiki-edit-bottom', + insert_before: 'fieldset', + original: 'ededb6cfd5adfe8a9723d00ce0ee23575c7cc44c', + partial: 'hooks/view_wiki_form_bottom' diff --git a/plugins/additionals/app/overrides/wiki/show.rb b/plugins/additionals/app/overrides/wiki/show.rb new file mode 100644 index 0000000..bb13ae0 --- /dev/null +++ b/plugins/additionals/app/overrides/wiki/show.rb @@ -0,0 +1,5 @@ +Deface::Override.new virtual_path: 'wiki/show', + name: 'wiki-show-bottom', + insert_before: 'p.wiki-update-info', + original: 'd9f52aa98f1cb335314570d3f5403690f1b29145', + partial: 'hooks/view_wiki_show_bottom' diff --git a/plugins/additionals/app/views/account/_invisible_captcha.html.slim b/plugins/additionals/app/views/account/_invisible_captcha.html.slim deleted file mode 100755 index e22a941..0000000 --- a/plugins/additionals/app/views/account/_invisible_captcha.html.slim +++ /dev/null @@ -1,2 +0,0 @@ -- if Additionals.setting?(:invisible_captcha) - = f.invisible_captcha :url, autocomplete: 'off' diff --git a/plugins/additionals/app/views/additionals/_body_bottom.html.slim b/plugins/additionals/app/views/additionals/_body_bottom.html.slim index b045725..6c4fcd1 100755 --- a/plugins/additionals/app/views/additionals/_body_bottom.html.slim +++ b/plugins/additionals/app/views/additionals/_body_bottom.html.slim @@ -1,7 +1,7 @@ -- footer = Additionals.setting(:global_footer) +- footer = Additionals.setting :global_footer - if footer.present? .additionals-footer - = textilizable(footer) + = textilizable footer - if @additionals_help_items.present? javascript: @@ -9,7 +9,7 @@ $('a.help').parent().append("
"); }); -- if Additionals.setting?(:open_external_urls) +- if Additionals.setting? :open_external_urls javascript: $(function() { $('a.external').attr({ 'target': '_blank', diff --git a/plugins/additionals/app/views/additionals/_body_top.slim b/plugins/additionals/app/views/additionals/_body_top.slim index 207c34a..f1e843e 100644 --- a/plugins/additionals/app/views/additionals/_body_top.slim +++ b/plugins/additionals/app/views/additionals/_body_top.slim @@ -1,2 +1,2 @@ -- if Additionals.setting?(:add_go_to_top) +- if Additionals.setting? :add_go_to_top a#gototop diff --git a/plugins/additionals/app/views/additionals/_chart_table_values.html.slim b/plugins/additionals/app/views/additionals/_chart_table_values.html.slim index 286d5d0..61193e8 100644 --- a/plugins/additionals/app/views/additionals/_chart_table_values.html.slim +++ b/plugins/additionals/app/views/additionals/_chart_table_values.html.slim @@ -7,8 +7,7 @@ table.list.issue-report.table-of-values tbody - options = { set_filter: 1 } - @chart[:filters].each do |line| - - if line[:filter] - - options.merge! line[:filter] + - options.merge! line[:filter] if line[:filter] tr class="#{cycle 'odd', 'even'}" td.name class="#{line[:id].to_s == '0' ? 'summary' : ''}" - if line[:filter].nil? diff --git a/plugins/additionals/app/views/additionals/_h2_with_query_search.html.slim b/plugins/additionals/app/views/additionals/_h2_with_query_search.html.slim index 3af60ee..d09b8ee 100755 --- a/plugins/additionals/app/views/additionals/_h2_with_query_search.html.slim +++ b/plugins/additionals/app/views/additionals/_h2_with_query_search.html.slim @@ -1,11 +1,13 @@ -= render(partial: 'additionals/live_search_ajax_call.js', layout: false, formats: [:js]) -- unless defined? classes - - classes = 'title' +- classes = 'title' unless defined? classes h2 class="#{classes}" = @query.new_record? ? l(title) : h(@query.name) span.additionals-live-search - = text_field_tag(:search, + = text_field_tag :search, q, autocomplete: 'off', class: 'live-search-field', - placeholder: l(:label_query_name_search)) + placeholder: defined?(placeholder) ? placeholder : l(:label_query_name_search) + + javascript: + observeLiveSearchField('search', + 'query-result-list') diff --git a/plugins/additionals/app/views/additionals/_html_head.html.slim b/plugins/additionals/app/views/additionals/_html_head.html.slim index 1680cb6..7d33104 100755 --- a/plugins/additionals/app/views/additionals/_html_head.html.slim +++ b/plugins/additionals/app/views/additionals/_html_head.html.slim @@ -2,5 +2,4 @@ = additionals_library_load :font_awesome = stylesheet_link_tag 'additionals', plugin: 'additionals' = javascript_include_tag 'additionals', plugin: 'additionals' -- unless Redmine::Plugin.installed? 'redmine_hrm' - - render_custom_top_menu_item +- render_custom_top_menu_item unless Redmine::Plugin.installed? 'redmine_hrm' diff --git a/plugins/additionals/app/views/additionals/_live_search_ajax_call.js.slim b/plugins/additionals/app/views/additionals/_live_search_ajax_call.js.slim deleted file mode 100755 index 295d6be..0000000 --- a/plugins/additionals/app/views/additionals/_live_search_ajax_call.js.slim +++ /dev/null @@ -1,18 +0,0 @@ -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 - }); - }); - }); diff --git a/plugins/additionals/app/views/additionals/_select2_ajax_call.js.slim b/plugins/additionals/app/views/additionals/_select2_ajax_call.js.slim index 20d3e87..0a87881 100755 --- a/plugins/additionals/app/views/additionals/_select2_ajax_call.js.slim +++ b/plugins/additionals/app/views/additionals/_select2_ajax_call.js.slim @@ -21,7 +21,7 @@ javascript: placeholder: "#{options[:placeholder].presence}", allowClear: #{options[:allow_clear].present? && options[:allow_clear] ? 'true' : 'false'}, minimumInputLength: 0, - width: "#{options[:width].presence || '60%'}", + width: "#{options[:width].presence || '90%'}", templateResult: #{options[:template_result].presence || 'formatNameWithIcon'}, #{options[:template_selection].present? ? ('templateSelection: ' + options[:template_selection]) : nil} }) diff --git a/plugins/additionals/app/views/additionals/_settings_list_defaults.html.slim b/plugins/additionals/app/views/additionals/_settings_list_defaults.html.slim index b21b5c9..bbde541 100755 --- a/plugins/additionals/app/views/additionals/_settings_list_defaults.html.slim +++ b/plugins/additionals/app/views/additionals/_settings_list_defaults.html.slim @@ -16,8 +16,8 @@ fieldset.box - 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}][]", + = check_box_tag "settings[#{setting_name_totals}][]", s.name, value, - id: nil) + id: nil = s.caption diff --git a/plugins/additionals/app/views/additionals/_tag_list.html.slim b/plugins/additionals/app/views/additionals/_tag_list.html.slim deleted file mode 100755 index ec9858c..0000000 --- a/plugins/additionals/app/views/additionals/_tag_list.html.slim +++ /dev/null @@ -1,30 +0,0 @@ -- 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, - project: @project, - 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, - project: @project, - tags_without_color: defined?(tags_without_color) ? tags_without_color : false) diff --git a/plugins/additionals/app/views/additionals/settings/_additionals.html.slim b/plugins/additionals/app/views/additionals/settings/_additionals.html.slim index ab3d683..e7a92de 100755 --- a/plugins/additionals/app/views/additionals/settings/_additionals.html.slim +++ b/plugins/additionals/app/views/additionals/settings/_additionals.html.slim @@ -1,7 +1,3 @@ ' Need Help? : -= link_to(l(:label_additionals_doc), - 'https://additionals.readthedocs.io/en/latest/', - class: 'external', - target: '_blank', - rel: 'noopener') += link_to_external l(:label_additionals_doc), 'https://additionals.readthedocs.io/en/latest/' = render_tabs additionals_settings_tabs diff --git a/plugins/additionals/app/views/additionals/settings/_users.html.slim b/plugins/additionals/app/views/additionals/settings/_users.html.slim deleted file mode 100755 index 60bf889..0000000 --- a/plugins/additionals/app/views/additionals/settings/_users.html.slim +++ /dev/null @@ -1,5 +0,0 @@ -p - = additionals_settings_checkbox :invisible_captcha, - disabled: (true unless Setting.self_registration?) - em.info - = t(:invisible_captcha_info_html) diff --git a/plugins/additionals/app/views/additionals_macros/show.html.slim b/plugins/additionals/app/views/additionals_macros/show.html.slim index 9bb2744..f99555e 100755 --- a/plugins/additionals/app/views/additionals_macros/show.html.slim +++ b/plugins/additionals/app/views/additionals_macros/show.html.slim @@ -1,6 +1,6 @@ h2 = l(:label_settings_macros) + " (#{@available_macros.count})" -.info = t(:label_top_macros_help_html) +.info = t :label_top_macros_help_html br .box - @available_macros.each do |macro, options| diff --git a/plugins/additionals/app/views/admin/_system_info.html.slim b/plugins/additionals/app/views/admin/_system_info.html.slim index 11022e9..a2178d5 100755 --- a/plugins/additionals/app/views/admin/_system_info.html.slim +++ b/plugins/additionals/app/views/admin/_system_info.html.slim @@ -1,13 +1,6 @@ table.list - tr - td.name - = l :label_system_info - | : - td.name - = system_info - tr - td.name - = l :label_uptime - | : - td.name - = system_uptime + - AdditionalsInfo.system_infos.each_value do |system_info| + tr + td.name + = "#{system_info[:label]}:" + td.name = system_info[:value] diff --git a/plugins/additionals/app/views/auto_completes/_additionals_tag_list.html.slim b/plugins/additionals/app/views/auto_completes/_additionals_tag_list.html.slim deleted file mode 100755 index 7007689..0000000 --- a/plugins/additionals/app/views/auto_completes/_additionals_tag_list.html.slim +++ /dev/null @@ -1 +0,0 @@ -== @tags.collect { |tag| { 'id' => tag.name, 'text' => tag.name } }.to_json diff --git a/plugins/additionals/app/views/dashboards/_form.html.slim b/plugins/additionals/app/views/dashboards/_form.html.slim index 2ac2c01..230b427 100644 --- a/plugins/additionals/app/views/dashboards/_form.html.slim +++ b/plugins/additionals/app/views/dashboards/_form.html.slim @@ -9,10 +9,7 @@ .splitcontent .splitcontentleft - - - if @dashboard.new_record? - = hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type - + = hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type if @dashboard.new_record? - if @project && @allowed_projects.present? && @allowed_projects.count > 1 p = f.select :project_id, diff --git a/plugins/additionals/app/views/dashboards/blocks/_activity.html.slim b/plugins/additionals/app/views/dashboards/blocks/_activity.html.slim index 4d0f70f..71d56ca 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_activity.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_activity.html.slim @@ -6,6 +6,6 @@ - events_by_day = activity_dashboard_data settings, dashboard - title = Additionals.true?(settings[:me_only]) ? l(:label_my_activity) : l(:label_activity) h3 = link_to title, activity_path(user_id: User.current, - from: events_by_day.keys.first) + from: events_by_day.keys.first) = render partial: 'activities/activities', locals: { events_by_day: events_by_day } diff --git a/plugins/additionals/app/views/dashboards/blocks/_activity_settings.html.slim b/plugins/additionals/app/views/dashboards/blocks/_activity_settings.html.slim index b22be45..67f32af 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_activity_settings.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_activity_settings.html.slim @@ -1,17 +1,17 @@ -- max_entries = settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES div id="#{block}-settings" style="#{'display: none;' if hide}" = form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do = hidden_field_tag "settings[#{block}][me_only]", '0' .box p label - = l(:label_max_entries) + = l :label_max_entries ' : - = number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true - + = number_field_tag "settings[#{block}][max_entries]", + settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES, + min: 1, max: 1000, required: true p label - = l(:label_only_my_activities) + = l :label_only_my_activities ' : = check_box_tag "settings[#{block}][me_only]", '1', Additionals.true?(settings[:me_only]) diff --git a/plugins/additionals/app/views/dashboards/blocks/_feed.html.slim b/plugins/additionals/app/views/dashboards/blocks/_feed.html.slim index 7494ecd..af2b485 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_feed.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_feed.html.slim @@ -11,10 +11,7 @@ - if feed[:items].count.positive? ul.reporting-list.feed - feed[:items].each do |item| - li - = link_to item[:title], - item[:link], - class: 'external', rel: 'noopener noreferrer', target: '_blank' + li = link_to_external item[:title], item[:link] - else p.nodata = l :label_no_data - elsif settings[:url].blank? diff --git a/plugins/additionals/app/views/dashboards/blocks/_news.html.slim b/plugins/additionals/app/views/dashboards/blocks/_news.html.slim index aa7238d..ce223a1 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_news.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_news.html.slim @@ -6,7 +6,7 @@ h3.icon.icon-news = l :label_news_latest .box p label - = l(:label_max_entries) + = l :label_max_entries ' : = number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true p diff --git a/plugins/additionals/app/views/dashboards/blocks/_project_subprojects.html.slim b/plugins/additionals/app/views/dashboards/blocks/_project_subprojects.html.slim index bb8025a..e169418 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_project_subprojects.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_project_subprojects.html.slim @@ -1,7 +1,7 @@ - if @subprojects.any? h3.icon.icon-projects - = l(:label_subproject_plural) + = l :label_subproject_plural ul.subprojects - @subprojects.each do |project| li - = link_to(project.name, project_path(project), class: project.css_classes).html_safe + = link_to project.name, project_path(project), class: project.css_classes diff --git a/plugins/additionals/app/views/dashboards/blocks/_query_list.html.slim b/plugins/additionals/app/views/dashboards/blocks/_query_list.html.slim index 3372c23..1f7dc6e 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_query_list.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_query_list.html.slim @@ -22,6 +22,7 @@ - if count.positive? / required by some helpers of other plugins - @query = query + = render partial: query_block[:list_partial], locals: { query_block[:entities_var] => query.send(query_block[:entries_method], limit: settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES), diff --git a/plugins/additionals/app/views/dashboards/blocks/_text.html.slim b/plugins/additionals/app/views/dashboards/blocks/_text.html.slim index 22e4756..8469bb4 100644 --- a/plugins/additionals/app/views/dashboards/blocks/_text.html.slim +++ b/plugins/additionals/app/views/dashboards/blocks/_text.html.slim @@ -1,26 +1,12 @@ -ruby: - title = settings[:title] || l(:label_text) - text = settings[:text] - -- if title.present? - h3 = title +- if settings[:text].nil? + h3 + = l :label_text_sync +- elsif settings[:title].present? + h3 + = settings[:title] - if @can_edit - div id="#{block}-settings" style='display: none;' - = form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do - .box - p - label - = l :field_title - ' : - = text_field_tag "settings[#{block}][title]", title - p - = text_area_tag "settings[#{block}][text]", text, rows: addtionals_textarea_cols(text), class: 'wiki-edit' - = wikitoolbar_for "settings_#{block}_text" - p - = submit_tag l(:button_save) - ' - = link_to_function l(:button_cancel), "$('##{block}-settings').toggle();" + = render partial: 'dashboards/blocks/text_async_settings', locals: { block: block, settings: settings } .wiki - = textilizable text + = textilizable settings[:text] diff --git a/plugins/additionals/app/views/dashboards/blocks/_text_async.html.slim b/plugins/additionals/app/views/dashboards/blocks/_text_async.html.slim new file mode 100644 index 0000000..db9a050 --- /dev/null +++ b/plugins/additionals/app/views/dashboards/blocks/_text_async.html.slim @@ -0,0 +1,10 @@ +- cache render_async_cache_key(_dashboard_async_blocks_path(@project, + dashboard.async_params(block, async, settings))), + expires_in: async[:cache_expires_in] || DashboardContent::RENDER_ASYNC_CACHE_EXPIRES_IN, + skip_digest: true do + + - if settings[:title].present? + h3 = settings[:title] + + .wiki + = textilizable settings[:text] diff --git a/plugins/additionals/app/views/dashboards/blocks/_text_async_settings.html.slim b/plugins/additionals/app/views/dashboards/blocks/_text_async_settings.html.slim new file mode 100644 index 0000000..c0cfc5d --- /dev/null +++ b/plugins/additionals/app/views/dashboards/blocks/_text_async_settings.html.slim @@ -0,0 +1,15 @@ +div id="#{block}-settings" style='display: none;' + = form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do + .box + p + label + = l :field_title + ' : + = text_field_tag "settings[#{block}][title]", (settings[:title] || l(:label_text_sync)) + p + = text_area_tag "settings[#{block}][text]", settings[:text], rows: addtionals_textarea_cols(settings[:text]), class: 'wiki-edit' + = wikitoolbar_for "settings_#{block}_text" + p + = submit_tag l :button_save + ' + = link_to_function l(:button_cancel), "$('##{block}-settings').toggle();" diff --git a/plugins/additionals/app/views/dashboards/edit.html.slim b/plugins/additionals/app/views/dashboards/edit.html.slim index 5b6ecca..acfe00a 100644 --- a/plugins/additionals/app/views/dashboards/edit.html.slim +++ b/plugins/additionals/app/views/dashboards/edit.html.slim @@ -1,7 +1,6 @@ h2 = l(:button_dashboard_edit) = labelled_form_for :dashboard, @dashboard, - url: { action: 'update', id: @dashboard.id }, - html: { multipart: true, method: :put, id: 'dashboard-form' } do |f| + html: { multipart: true, id: 'dashboard-form' } do |f| = render partial: 'form', locals: { f: f } = submit_tag l(:button_save) diff --git a/plugins/additionals/app/views/hooks/_view_wiki_form_bottom.html.slim b/plugins/additionals/app/views/hooks/_view_wiki_form_bottom.html.slim new file mode 100644 index 0000000..2f94ebd --- /dev/null +++ b/plugins/additionals/app/views/hooks/_view_wiki_form_bottom.html.slim @@ -0,0 +1 @@ += call_hook :view_wiki_form_bottom, content: @content, page: @page diff --git a/plugins/additionals/app/views/hooks/_view_wiki_show_bottom.html.slim b/plugins/additionals/app/views/hooks/_view_wiki_show_bottom.html.slim new file mode 100644 index 0000000..bfefc0f --- /dev/null +++ b/plugins/additionals/app/views/hooks/_view_wiki_show_bottom.html.slim @@ -0,0 +1 @@ += call_hook :view_wiki_show_bottom, content: @content, page: @page diff --git a/plugins/additionals/app/views/issues/_additionals_action_menu.html.slim b/plugins/additionals/app/views/issues/_additionals_action_menu.html.slim index 7ec5a38..99c954d 100755 --- a/plugins/additionals/app/views/issues/_additionals_action_menu.html.slim +++ b/plugins/additionals/app/views/issues/_additionals_action_menu.html.slim @@ -1,5 +1,9 @@ -- 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 } +- if User.current.logged? && \ + Additionals.setting?(:issue_assign_to_me) && \ + @issue.editable? && \ + @issue.safe_attribute?('assigned_to_id') && \ + @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' diff --git a/plugins/additionals/app/views/issues/_additionals_sidebar_issues.html.slim b/plugins/additionals/app/views/issues/_additionals_sidebar_issues.html.slim index cbd8e00..a6cf08b 100644 --- a/plugins/additionals/app/views/issues/_additionals_sidebar_issues.html.slim +++ b/plugins/additionals/app/views/issues/_additionals_sidebar_issues.html.slim @@ -1,19 +1,19 @@ - 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) + @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) + h3 = l :label_issue_change_status ul.issue-status-change-sidebar - statuses.each do |s| - - if s != @issue.status + - unless s == @issue.status li - if s.is_closed? - = link_to(font_awesome_icon('fas_caret-square-left', post_text: s.name), + = 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}") + method: :put, class: "status-switch status-#{s.id}" - else - = link_to(font_awesome_icon('far_caret-square-left', post_text: s.name), + = 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}") + method: :put, class: "status-switch status-#{s.id}" diff --git a/plugins/additionals/app/views/projects/show.html.slim b/plugins/additionals/app/views/projects/show.html.slim index 35005f6..2023f3c 100644 --- a/plugins/additionals/app/views/projects/show.html.slim +++ b/plugins/additionals/app/views/projects/show.html.slim @@ -6,8 +6,7 @@ edit_project_dashboard_path(@project, @dashboard), class: 'icon icon-edit' - - unless Redmine::Plugin.installed? 'redmine_reporting' - = bookmark_link @project + = bookmark_link @project unless Redmine::Plugin.installed? 'redmine_reporting' = call_hook :view_project_contextual_links, project: @project - if @dashboard&.editable? @@ -49,9 +48,7 @@ class: 'icon icon-del' = sidebar_action_toggle @dashboard_sidebar, @dashboard, @project - - unless @dashboard_sidebar - = render_dashboard_actionlist @dashboard, @project - + = render_dashboard_actionlist @dashboard, @project unless @dashboard_sidebar = call_hook :view_project_actions_dropdown, project: @project - if User.current.allowed_to?(:edit_project, @project) diff --git a/plugins/additionals/app/views/users/_additionals_contextual.html.slim b/plugins/additionals/app/views/users/_additionals_contextual.html.slim index ba37a40..68c5b27 100755 --- a/plugins/additionals/app/views/users/_additionals_contextual.html.slim +++ b/plugins/additionals/app/views/users/_additionals_contextual.html.slim @@ -1,4 +1,3 @@ - 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') + - project_url = memberships_new_issue_project_url user, @memberships + = link_to l(:label_issue_new), project_url, class: 'user-new-issue icon icon-add' if project_url.present? diff --git a/plugins/additionals/app/views/welcome/index.html.slim b/plugins/additionals/app/views/welcome/index.html.slim index f7cd53f..40de8ba 100644 --- a/plugins/additionals/app/views/welcome/index.html.slim +++ b/plugins/additionals/app/views/welcome/index.html.slim @@ -21,8 +21,7 @@ = delete_dashboard_link dashboard_path(@dashboard), class: 'icon icon-del' = sidebar_action_toggle @dashboard_sidebar, @dashboard - - unless @dashboard_sidebar - = render_dashboard_actionlist @dashboard + = render_dashboard_actionlist @dashboard unless @dashboard_sidebar = call_hook :view_welcome_show_actions_dropdown diff --git a/plugins/additionals/app/views/wiki/_user_macros.html.slim b/plugins/additionals/app/views/wiki/_user_macros.html.slim index 823d815..00ead10 100755 --- a/plugins/additionals/app/views/wiki/_user_macros.html.slim +++ b/plugins/additionals/app/views/wiki/_user_macros.html.slim @@ -7,6 +7,7 @@ = 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 @@ -16,6 +17,7 @@ = l :field_login ' : = link_to user.login, "/users/#{user.id}" + - unless user.pref.hide_mail .user.line = l :field_mail diff --git a/plugins/additionals/assets/javascripts/Chart.bundle.min.js b/plugins/additionals/assets/javascripts/Chart.bundle.min.js index 55d9eb0..7134d26 100644 --- a/plugins/additionals/assets/javascripts/Chart.bundle.min.js +++ b/plugins/additionals/assets/javascripts/Chart.bundle.min.js @@ -1,7 +1,7 @@ /*! - * Chart.js v2.9.3 + * Chart.js v2.9.4 * https://www.chartjs.org - * (c) 2019 Chart.js Contributors + * (c) 2020 Chart.js Contributors * Released under the MIT License */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Chart=e()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function t(){throw new Error("Dynamic requires are not currently supported by rollup-plugin-commonjs")}function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},i=e((function(t){var e={};for(var i in n)n.hasOwnProperty(i)&&(e[n[i]]=i);var a=t.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var r in a)if(a.hasOwnProperty(r)){if(!("channels"in a[r]))throw new Error("missing channels property: "+r);if(!("labels"in a[r]))throw new Error("missing channel labels property: "+r);if(a[r].labels.length!==a[r].channels)throw new Error("channel and label counts mismatch: "+r);var o=a[r].channels,s=a[r].labels;delete a[r].channels,delete a[r].labels,Object.defineProperty(a[r],"channels",{value:o}),Object.defineProperty(a[r],"labels",{value:s})}a.rgb.hsl=function(t){var e,n,i=t[0]/255,a=t[1]/255,r=t[2]/255,o=Math.min(i,a,r),s=Math.max(i,a,r),l=s-o;return s===o?e=0:i===s?e=(a-r)/l:a===s?e=2+(r-i)/l:r===s&&(e=4+(i-a)/l),(e=Math.min(60*e,360))<0&&(e+=360),n=(o+s)/2,[e,100*(s===o?0:n<=.5?l/(s+o):l/(2-s-o)),100*n]},a.rgb.hsv=function(t){var e,n,i,a,r,o=t[0]/255,s=t[1]/255,l=t[2]/255,u=Math.max(o,s,l),d=u-Math.min(o,s,l),h=function(t){return(u-t)/6/d+.5};return 0===d?a=r=0:(r=d/u,e=h(o),n=h(s),i=h(l),o===u?a=i-n:s===u?a=1/3+e-i:l===u&&(a=2/3+n-e),a<0?a+=1:a>1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t =0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t =0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=kt);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o >1)+h+t+M+S.slice(T);break;default:t=S+h+t+M}return u(t)}return y=void 0===y?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,y)):Math.max(0,Math.min(20,y)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Gu(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(ju(n)/3))),i=Math.pow(10,-r),o=Ju[8+r/3];return function(t){return e(i*t)+o}}}}function nc(n){return Ku=tc(n),t.format=Ku.format,t.formatPrefix=Ku.formatPrefix,Ku}function ec(t){return Math.max(0,-ju(Math.abs(t)))}function rc(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(ju(n)/3)))-ju(Math.abs(t)))}function ic(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,ju(n)-ju(t))+1}nc({thousands:",",grouping:[3],currency:["$",""]});var oc=1e-6,ac=1e-12,uc=Math.PI,cc=uc/2,fc=uc/4,sc=2*uc,lc=180/uc,hc=uc/180,dc=Math.abs,pc=Math.atan,gc=Math.atan2,yc=Math.cos,vc=Math.ceil,_c=Math.exp,bc=Math.hypot,mc=Math.log,xc=Math.pow,wc=Math.sin,Mc=Math.sign||function(t){return t>0?1:t<0?-1:0},Ac=Math.sqrt,Tc=Math.tan;function Sc(t){return t>1?0:t<-1?uc:Math.acos(t)}function Ec(t){return t>1?cc:t<-1?-cc:Math.asin(t)}function kc(t){return(t=wc(t/2))*t}function Nc(){}function Cc(t,n){t&&zc.hasOwnProperty(t.type)&&zc[t.type](t,n)}var Pc={Feature:function(t,n){Cc(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=yc(n=(n*=hc)/2+fc),a=wc(n),u=Bc*a,c=Ic*o+u*yc(i),f=u*r*wc(i);Qc.add(gc(f,c)),Oc=t,Ic=o,Bc=a}function af(t){return[gc(t[1],t[0]),Ec(t[2])]}function uf(t){var n=t[0],e=t[1],r=yc(e);return[r*yc(n),r*wc(n),wc(e)]}function cf(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function ff(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function sf(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function lf(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function hf(t){var n=Ac(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var df,pf,gf,yf,vf,_f,bf,mf,xf,wf,Mf,Af,Tf,Sf,Ef,kf,Nf={point:Cf,lineStart:zf,lineEnd:Df,polygonStart:function(){Nf.point=qf,Nf.lineStart=Rf,Nf.lineEnd=Ff,Wc=new g,tf.polygonStart()},polygonEnd:function(){tf.polygonEnd(),Nf.point=Cf,Nf.lineStart=zf,Nf.lineEnd=Df,Qc<0?(Yc=-(jc=180),Lc=-(Hc=90)):Wc>oc?Hc=90:Wc<-1e-6&&(Lc=-90),Kc[0]=Yc,Kc[1]=jc},sphere:function(){Yc=-(jc=180),Lc=-(Hc=90)}};function Cf(t,n){Zc.push(Kc=[Yc=t,jc=t]),n0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,H.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),H.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!H.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ue=Ye,Ge=je;Ge.positioners=Ue;var qe=H.valueOrDefault;function Ze(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?H.merge(e[t][a],[Re.getScaleDefaults(r),o]):H.merge(e[t][a],o)}else H._merger(t,e,n,i)}})}function $e(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||{},r=n[t];"scales"===t?e[t]=Ze(a,r):"scale"===t?e[t]=H.merge(a,[Re.getScaleDefaults(r.type),r]):H._merger(t,e,n,i)}})}function Xe(t){var e=t.options;H.each(t.scales,(function(e){me.removeBox(t,e)})),e=$e(W.global,W[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Ke(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(H.findIndex(t,a)>=0);return i}function Je(t){return"top"===t||"bottom"===t}function Qe(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}W._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var tn=function(t,e){return this.construct(t,e),this};H.extend(tn.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=$e(W.global,W[t.type],t.options||{}),t}(e);var i=Ie.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=H.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,tn.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Le.notify(t,"beforeInit"),H.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Le.notify(t,"afterInit"),t},clear:function(){return H.canvas.clear(this),this},stop:function(){return J.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(H.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:H.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",H.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Le.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;H.each(e.xAxes,(function(t,n){t.id||(t.id=Ke(e.xAxes,"x-axis-",n))})),H.each(e.yAxes,(function(t,n){t.id||(t.id=Ke(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),H.each(i,(function(e){var i=e.options,r=i.id,o=qe(i.type,e.dtype);Je(i.position)!==Je(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Re.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),H.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Re.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=H.options._parseFont(n),s=Yn(n.fontColor,W.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",H.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:H.noop}),$n=Vn;Zn._defaults=$n;var Xn=H._deprecated,Kn=H.options.resolve,Jn=H.valueOrDefault,Qn=Number.MIN_SAFE_INTEGER||-9007199254740991,ti=Number.MAX_SAFE_INTEGER||9007199254740991,ei={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ni=Object.keys(ei);function ii(t,e){return t-e}function ai(t){return H.valueOrDefault(t.time.min,t.ticks.min)}function ri(t){return H.valueOrDefault(t.time.max,t.ticks.max)}function oi(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||W.global.defaultColor,o&&s&&r.length&&(H.canvas.clipArea(u,t.chartArea),ki(u,r,o,a,s,i._loop),H.canvas.unclipArea(u)))}},Si=H.rtl.getRtlAdapter,Di=H.noop,Ci=H.valueOrDefault;function Pi(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}W._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;e.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,B.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),B.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!B.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ge=ze,qe=Ue;qe.positioners=Ge;var Ze=B.valueOrDefault;function $e(){return B.merge(Object.create(null),[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?B.merge(e[t][a],[Ne.getScaleDefaults(r),o]):B.merge(e[t][a],o)}else B._merger(t,e,n,i)}})}function Xe(){return B.merge(Object.create(null),[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||Object.create(null),r=n[t];"scales"===t?e[t]=$e(a,r):"scale"===t?e[t]=B.merge(a,[Ne.getScaleDefaults(r.type),r]):B._merger(t,e,n,i)}})}function Ke(t){var e=t.options;B.each(t.scales,(function(e){pe.removeBox(t,e)})),e=Xe(Y.global,Y[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Je(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(B.findIndex(t,a)>=0);return i}function Qe(t){return"top"===t||"bottom"===t}function tn(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}Y._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var en=function(t,e){return this.construct(t,e),this};B.extend(en.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||Object.create(null)).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=Xe(Y.global,Y[t.type],t.options||{}),t}(e);var i=Le.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=B.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,en.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Re.notify(t,"beforeInit"),B.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Re.notify(t,"afterInit"),t},clear:function(){return B.canvas.clear(this),this},stop:function(){return Q.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(B.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:B.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",B.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Re.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;B.each(e.xAxes,(function(t,n){t.id||(t.id=Je(e.xAxes,"x-axis-",n))})),B.each(e.yAxes,(function(t,n){t.id||(t.id=Je(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),B.each(i,(function(e){var i=e.options,r=i.id,o=Ze(i.type,e.dtype);Qe(i.position)!==Qe(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Ne.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),B.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Ne.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=B.options._parseFont(n),s=zn(n.fontColor,Y.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",B.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:B.noop}),Xn=Hn;$n._defaults=Xn;var Kn=B._deprecated,Jn=B.options.resolve,Qn=B.valueOrDefault,ti=Number.MIN_SAFE_INTEGER||-9007199254740991,ei=Number.MAX_SAFE_INTEGER||9007199254740991,ni={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ii=Object.keys(ni);function ai(t,e){return t-e}function ri(t){return B.valueOrDefault(t.time.min,t.ticks.min)}function oi(t){return B.valueOrDefault(t.time.max,t.ticks.max)}function si(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||Y.global.defaultColor,o&&s&&r.length&&(B.canvas.clipArea(u,t.chartArea),Mi(u,r,o,a,s,i._loop),B.canvas.unclipArea(u)))}},Di=B.rtl.getRtlAdapter,Ci=B.noop,Pi=B.valueOrDefault;function Ti(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}Y._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;e9999?"+"+Ga(t,6):Ga(t,4)}(t.getUTCFullYear())+"-"+Ga(t.getUTCMonth()+1,2)+"-"+Ga(t.getUTCDate(),2)+(i?"T"+Ga(n,2)+":"+Ga(e,2)+":"+Ga(r,2)+"."+Ga(i,3)+"Z":r?"T"+Ga(n,2)+":"+Ga(e,2)+":"+Ga(r,2)+"Z":e||n?"T"+Ga(n,2)+":"+Ga(e,2)+"Z":"")}function $a(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return ja;if(f)return f=!1,La;var n,r,i=a;if(34===t.charCodeAt(i)){for(;a++=v)<<1|t>=y)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,g.data),b=n-+this._y.call(null,g.data),m=_*_+b*b;if(m