From db2576547e344188f7adb9f8c94f933303e61954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Rama=C5=A1euski?= <andrej@x2.cz> Date: Sun, 8 Aug 2021 22:42:46 +0200 Subject: [PATCH] Updaty pluginu pro verzi 4 --- plugins/easy_mindmup/Gemfile | 4 +- plugins/easy_mindmup/README.md | 1 - plugins/easy_mindmup/after_init.rb | 2 +- .../app/helpers/easy_mindmup_helper.rb | 14 - .../app/views/easy_mindmup/_includes.html.erb | 124 +- .../views/easy_mindmup/_js_prepare.html.erb | 4 +- .../easy_mindmup/_test_includes.html.erb | 28 - .../assets/javascripts/content_patch.js | 49 - .../assets/javascripts/easy_mindmup.js | 11 - .../assets/javascripts/external.js | 2 - .../easy_mindmup/assets/javascripts/filter.js | 58 +- .../javascripts/jasmine/helpers/test.js | 107 - .../javascripts/jasmine/jasmine_lib/boot.js | 135 - .../jasmine/jasmine_lib/jasmine-html.js | 473 -- .../jasmine/jasmine_lib/jasmine.js | 4941 -------------- .../assets/javascripts/jasmine/main.js | 66 - .../assets/javascripts/jasmine/parse_form.js | 87 - .../javascripts/jasmine/saver_linearize.js | 43 - .../assets/javascripts/layout_patch.js | 26 +- .../easy_mindmup/assets/javascripts/legend.js | 2 - .../assets/javascripts/legend_events.js | 47 +- .../easy_mindmup/assets/javascripts/loader.js | 138 +- .../easy_mindmup/assets/javascripts/main.js | 2 - .../assets/javascripts/map_model_patch.js | 8 +- .../assets/javascripts/mapjs_init.js | 65 +- .../javascripts/mindmup/dom-map-widget.js | 2 +- .../assets/javascripts/mindmup/layout.js | 14 +- .../assets/javascripts/mm_context_menu.js | 44 +- .../assets/javascripts/model_classes.js | 6 +- .../assets/javascripts/node_patch.js | 5 +- .../easy_mindmup/assets/javascripts/print.js | 56 +- .../assets/javascripts/redrawer.js | 8 + .../easy_mindmup/assets/javascripts/saver.js | 2 +- .../assets/javascripts/storage.js | 377 +- .../easy_mindmup/assets/javascripts/styles.js | 10 +- .../assets/javascripts/toolbar.js | 2 +- .../easy_mindmup/assets/javascripts/utils.js | 22 +- .../assets/stylesheets/easy_mindmup.css | 14 +- .../stylesheets/easy_mindmup_sass.scss.erb | 1 - .../assets/stylesheets/jasmine.css | 58 - .../sass/_mindmup_default_variables.scss | 2 +- .../stylesheets/sass/_mindmup_frame.scss | 10 +- .../stylesheets/sass/_mindmup_legend.scss | 3 - .../stylesheets/sass/_mindmup_sidebar.scss | 16 +- plugins/easy_mindmup/config/locales/ja.yml | 155 - plugins/easy_mindmup/init.rb | 2 +- .../lib/easy_mindmup/easy_mindmup.rb | 6 - plugins/easy_wbs/README.md | 3 - plugins/easy_wbs/after_init.rb | 2 +- .../app/controllers/easy_wbs_controller.rb | 2 +- .../easy_queries/easy_wbs_easy_issue_query.rb | 53 - .../app/views/easy_wbs/_includes.html.erb | 32 +- .../views/easy_wbs/_test_includes.html.erb | 1 - .../app/views/easy_wbs/index.html.erb | 60 +- .../easy_wbs/assets/javascripts/easy_wbs.js | 3 - .../assets/javascripts/tests/encode_layout.js | 5827 ----------------- .../easy_wbs/assets/javascripts/wbs_main.js | 4 +- .../easy_wbs/assets/javascripts/wbs_modals.js | 2 +- .../assets/stylesheets/generated/easy_wbs.css | 1 - plugins/easy_wbs/config/locales/hu.yml | 92 +- plugins/easy_wbs/config/locales/ja.yml | 70 +- plugins/easy_wbs/init.rb | 4 +- .../controllers/queries_controller_patch.rb | 12 +- .../helpers/application_helper_patch.rb | 9 - .../redmine_patch/models/project_patch.rb | 6 - .../easy_wbs/spec/features/jasmine_spec.rb | 53 +- plugins/redmine_agile/.drone.yml | 78 - plugins/redmine_agile/README.rdoc | 0 .../controllers/agile_boards_controller.rb | 96 +- .../controllers/agile_charts_controller.rb | 96 +- .../controllers/agile_colors_controller.rb | 50 - .../agile_journal_details_controller.rb | 13 +- .../controllers/agile_queries_controller.rb | 124 - .../controllers/agile_versions_controller.rb | 85 - .../app/helpers/agile_boards_helper.rb | 143 +- .../app/helpers/agile_charts_helper.rb | 13 +- .../app/helpers/agile_support_helper.rb | 32 +- .../app/helpers/agile_versions_helper.rb | 54 - .../app/models/agile_charts_query.rb | 215 +- .../redmine_agile/app/models/agile_color.rb | 68 - .../redmine_agile/app/models/agile_data.rb | 3 +- .../redmine_agile/app/models/agile_query.rb | 482 +- .../app/models/agile_versions_query.rb | 150 - .../agile_boards/_add_issue_card.html.erb | 5 - .../app/views/agile_boards/_board.html.erb | 43 +- .../app/views/agile_boards/_index.html.erb | 94 +- .../views/agile_boards/_issue_card.html.erb | 29 +- .../views/agile_boards/_issues_links.html.erb | 16 +- .../agile_boards/_issues_sidebar.html.erb | 8 - .../app/views/agile_boards/index.html.erb | 49 - .../agile_boards/inline_comment.html.erb | 2 +- .../views/agile_charts/_agile_charts.html.erb | 14 +- .../app/views/agile_charts/_chart.html.erb | 100 +- .../agile_charts/_versions_show.html.erb | 24 +- .../app/views/agile_charts/show.html.erb | 35 +- .../app/views/agile_colors/index.html.erb | 29 - .../agile_journal_details/status.html.erb | 65 +- .../app/views/agile_queries/_columns.html.erb | 38 - .../app/views/agile_queries/_filters.html.erb | 28 - .../app/views/agile_queries/_form.html.erb | 80 - .../app/views/agile_queries/edit.html.erb | 6 - .../app/views/agile_queries/index.html.erb | 25 - .../app/views/agile_queries/new.html.erb | 6 - .../app/views/agile_versions/_board.html.erb | 43 - .../agile_versions/_version_issues.html.erb | 22 - .../views/agile_versions/autocomplete.js.erb | 1 - .../app/views/agile_versions/index.html.erb | 56 - .../app/views/agile_versions/load.js.erb | 5 - .../context_menus/_agile_colors.html.erb | 11 - .../views/issues/_agile_data_fields.html.erb | 10 +- .../app/views/issues/_issue_color.html.erb | 11 - .../views/issues/_issue_color_form.html.erb | 14 - .../issues/_issue_story_points_form.html.erb | 21 +- .../projects/_project_color_form.html.erb | 11 - .../views/settings/agile/_general.html.erb | 37 +- .../app/views/users/_user_color_form.html.erb | 11 - .../assets/images/pro_version_agile.png | Bin 49763 -> 0 bytes .../javascripts/jquery.simplecolorpicker.js | 0 .../assets/javascripts/redmine_agile.js | 274 +- .../javascripts/redmine_agile_context_menu.js | 222 - .../assets/javascripts/visibility.min.js | 1 - .../stylesheets/jquery.simplecolorpicker.css | 0 .../assets/stylesheets/redmine_agile.css | 127 +- plugins/redmine_agile/config/locales/de.yml | 116 +- plugins/redmine_agile/config/locales/en.yml | 96 +- plugins/redmine_agile/config/locales/es.yml | 199 +- .../redmine_agile/config/locales/pt-BR.yml | 246 +- plugins/redmine_agile/config/locales/ru.yml | 96 +- .../redmine_agile/config/locales/zh-TW.yml | 4 +- plugins/redmine_agile/config/routes.rb | 19 +- .../migrate/001_create_issue_status_orders.rb | 2 +- .../db/migrate/002_create_agile_colors.rb | 2 +- .../migrate/003_rename_issue_status_orders.rb | 2 +- .../db/migrate/004_rename_agile_ranks.rb | 2 +- .../005_add_story_points_to_agile_ranks.rb | 2 +- plugins/redmine_agile/doc/CHANGELOG | 127 +- plugins/redmine_agile/init.rb | 51 +- .../redmine_agile/lib/acts_as_colored/init.rb | 21 - .../acts_as_colored/lib/acts_as_colored.rb | 69 - plugins/redmine_agile/lib/redmine_agile.rb | 76 +- .../lib/redmine_agile/charts/agile_chart.rb | 106 +- .../charts/average_lead_time_chart.rb | 67 - .../redmine_agile/charts/burndown_chart.rb | 19 +- .../lib/redmine_agile/charts/burnup_chart.rb | 81 - .../charts/cumulative_flow_chart.rb | 72 - .../redmine_agile/charts/lead_time_chart.rb | 77 - .../charts/trackers_cumulative_flow_chart.rb | 56 - .../redmine_agile/charts/velocity_chart.rb | 60 - .../charts/work_burndown_chart.rb | 11 +- .../redmine_agile/charts/work_burnup_chart.rb | 112 - .../lib/redmine_agile/helpers/agile_helper.rb | 139 +- .../hooks/controller_issue_hook.rb | 23 +- .../redmine_agile/hooks/helper_issues_hook.rb | 34 - .../hooks/views_context_menus_hook.rb | 26 - .../redmine_agile/hooks/views_issues_hook.rb | 16 +- .../redmine_agile/hooks/views_layouts_hook.rb | 4 +- .../hooks/views_projects_form_hook.rb | 26 - .../hooks/views_users_form_hook.rb | 27 - .../hooks/views_versions_hook.rb | 2 +- .../application_controller_patch.rb | 41 - .../patches/compatibility_patch.rb | 351 - .../lib/redmine_agile/patches/issue_patch.rb | 22 +- .../patches/issue_priority_patch.rb | 36 - .../patches/issue_query_patch.rb | 62 - .../redmine_agile/patches/project_patch.rb | 12 +- .../patches/queries_controller_patch.rb | 3 +- .../redmine_agile/patches/tracker_patch.rb | 36 - .../lib/redmine_agile/patches/user_patch.rb | 39 - .../lib/redmine_agile/utils/header_tree.rb | 118 - .../functional/agile_board_controller_test.rb | 963 --- .../agile_charts_controller_test.rb | 189 +- .../agile_journal_details_controller_test.rb | 23 +- .../agile_queries_controller_test.rb | 134 - .../agile_versions_controller_test.rb | 124 - .../test/functional/issues_controller_test.rb | 54 +- .../functional/projects_controller_test.rb | 20 +- .../test/functional/users_controller_test.rb | 17 +- .../test/integration/common_views_test.rb | 2 +- plugins/redmine_agile/test/test_helper.rb | 20 +- .../test/ui/agile_board_ui_test.rb | 2 +- .../test/unit/agile_color_test.rb | 80 - .../test/unit/agile_data_test.rb | 2 +- .../test/unit/agile_versions_query_test.rb | 138 - .../unit/helpers/agile_boards_helper_test.rb | 131 +- ...hecklist_template_categories_controller.rb | 72 - .../checklist_templates_controller.rb | 94 - .../app/controllers/checklists_controller.rb | 43 +- .../app/helpers/checklists_helper.rb | 17 +- .../app/models/checklist.rb | 12 +- .../app/models/checklist_template.rb | 62 - .../app/models/checklist_template_category.rb | 33 - .../app/models/journal_checklist_history.rb | 43 +- .../_form.html.erb | 6 - .../edit.html.erb | 6 - .../new.html.erb | 6 - .../views/checklist_templates/_form.html.erb | 59 - .../views/checklist_templates/edit.html.erb | 6 - .../views/checklist_templates/new.html.erb | 6 - .../views/checklists/_checklist_item.html.erb | 15 +- .../app/views/checklists/done.js.erb | 13 + .../app/views/issues/_checklist.html.erb | 10 +- .../views/issues/_checklist_fields.html.erb | 37 +- .../app/views/issues/_checklist_form.html.erb | 3 - .../issues/_checklist_templates.html.erb | 4 - .../settings/_checklist_templates.html.erb | 34 - .../settings/checklists/_checklists.html.erb | 6 - .../checklists/_template_categories.html.erb | 29 - .../settings/checklists/_templates.html.erb | 44 - .../assets/javascripts/checklists.js | 219 +- .../assets/stylesheets/checklists.css | 69 +- .../redmine_checklists/config/locales/de.yml | 14 +- .../redmine_checklists/config/locales/en.yml | 12 +- .../redmine_checklists/config/locales/fr.yml | 5 + .../config/locales/pt-BR.yml | 43 +- .../redmine_checklists/config/locales/ru.yml | 13 + plugins/redmine_checklists/config/routes.rb | 9 +- .../db/migrate/001_create_checklists.rb | 2 +- .../002_add_time_stamps_to_checklists.rb | 2 +- .../003_create_checklist_template_category.rb | 2 +- .../migrate/004_create_checklist_templates.rb | 2 +- .../005_modify_checklist_subject_length.rb | 2 +- .../006_add_fields_to_checklist_template.rb | 2 +- plugins/redmine_checklists/doc/CHANGELOG | 60 +- plugins/redmine_checklists/init.rb | 10 +- .../hooks/controller_issues_hook.rb | 16 +- .../hooks/views_issues_hook.rb | 2 +- .../hooks/views_layouts_hook.rb | 2 +- .../add_helpers_for_checklists_patch.rb | 2 +- .../2.1/redmine_api_test_patch.rb | 2 +- .../application_controller_patch.rb | 41 - .../compatibility/application_helper_patch.rb | 2 +- .../patches/compatibility/journal_patch.rb | 27 +- .../compatibility/open_struct_patch.rb | 2 +- .../patches/compatibility_patch.rb | 2 +- .../redmine_checklists/patches/issue_patch.rb | 39 +- .../patches/issue_query_patch.rb | 58 +- .../patches/issues_controller_patch.rb | 61 +- .../patches/issues_helper_patch.rb | 45 +- .../patches/notifiable_patch.rb | 31 +- .../patches/project_patch.rb | 10 +- .../patches/projects_helper_patch.rb | 52 - .../redmine_checklist_setting.rb | 26 - .../redmine_checklists/redmine_checklists.rb | 16 +- .../redmine_checklists/scripts/run_local.sh | 12 - .../test/fixtures/checklists.yml | 13 +- ...ist_template_categories_controller_test.rb | 92 - .../checklist_templates_controller_test.rb | 135 - .../functional/checklists_controller_test.rb | 15 +- .../test/functional/issues_controller_test.rb | 216 +- .../integration/api_test/checklists_test.rb | 31 +- .../test/integration/common_issue_test.rb | 19 +- .../redmine_checklists/test/test_helper.rb | 13 +- .../unit/checklist_template_category_test.rb | 80 - .../test/unit/checklist_template_test.rb | 32 - .../test/unit/checklist_test.rb | 51 +- .../test/unit/issue_test.rb | 6 +- .../unit/journal_checklist_history_test.rb | 251 - .../test/unit/project_test.rb | 2 +- plugins/redmine_monitoring_controlling | 1 - plugins/redmine_user_specific_theme | 1 - plugins/redmine_work_time/.hgignore | 3 - plugins/redmine_work_time/README.md | 11 +- .../app/controllers/work_time_controller.rb | 73 +- .../app/models/user_issue_month.rb | 7 +- .../app/models/wt_daily_memo.rb | 7 +- .../app/models/wt_holidays.rb | 7 +- .../app/models/wt_member_order.rb | 7 +- .../app/models/wt_project_orders.rb | 7 +- .../app/models/wt_ticket_relay.rb | 7 +- .../work_time/_user_month_table.html.erb | 4 + .../redmine_work_time/config/locales/ca.yml | 95 +- .../redmine_work_time/config/locales/de.yml | 1 + .../redmine_work_time/config/locales/en.yml | 1 + .../redmine_work_time/config/locales/es.yml | 47 +- .../redmine_work_time/config/locales/fr.yml | 1 + .../redmine_work_time/config/locales/it.yml | 1 + .../redmine_work_time/config/locales/ja.yml | 1 + .../redmine_work_time/config/locales/ko.yml | 1 + .../redmine_work_time/config/locales/no.yml | 1 + .../redmine_work_time/config/locales/po.yml | 1 + .../config/locales/pt-BR.yml | 1 + .../redmine_work_time/config/locales/ru.yml | 1 + .../redmine_work_time/config/locales/tr.yml | 1 + .../redmine_work_time/config/locales/zh.yml | 1 + .../migrate/001_create_user_issue_months.rb | 2 +- .../db/migrate/002_create_wt_member_orders.rb | 2 +- .../db/migrate/003_create_wt_ticket_relays.rb | 2 +- .../db/migrate/004_add_prj_to_mem_odr.rb | 2 +- .../db/migrate/005_create_wt_daily_memos.rb | 2 +- .../migrate/006_create_wt_project_orders.rb | 2 +- .../db/migrate/007_create_wt_holidays.rb | 2 +- .../008_remove_month_from_user_issue_month.rb | 2 +- .../009_remove_prj_from_wt_project_orders.rb | 2 +- plugins/redmine_work_time/init.rb | 2 +- .../lib/work_time_projects_helper_patch.rb | 25 +- plugins/redmineup_tags/Gemfile.lock | 108 - .../app/controllers/issue_tags_controller.rb | 53 +- .../app/controllers/tags_controller.rb | 26 +- .../app/helpers/issues_tags_helper.rb | 20 +- .../redmineup_tags/app/helpers/tags_helper.rb | 71 +- .../views/context_menus/_issues_tags.html.erb | 15 +- .../app/views/issue_tags/_edit_modal.html.erb | 12 +- .../views/issues/._tags_sidebar.html.erb.swp | Bin 12288 -> 0 bytes .../app/views/issues/_tags.html.erb | 2 +- .../app/views/issues/_tags_form.html.erb | 6 +- .../app/views/issues/_tags_sidebar.html.erb | 2 - .../app/views/tags/context_menu.html.erb | 6 +- .../assets/stylesheets/redmine_tags.css | 1 + plugins/redmineup_tags/config/locales/de.yml | 39 +- plugins/redmineup_tags/config/locales/ru.yml | 33 +- plugins/redmineup_tags/config/locales/zh.yml | 18 +- plugins/redmineup_tags/config/routes.rb | 18 +- .../db/migrate/001_add_tags_and_taggings.rb | 2 +- .../db/migrate/002_create_tags.rb | 2 +- plugins/redmineup_tags/doc/CHANGELOG | 46 +- plugins/redmineup_tags/init.rb | 52 +- .../redmineup_tags/lib/query_tags_column.rb | 2 +- plugins/redmineup_tags/lib/redmine_tags.rb | 27 - .../redmine_tags/hooks/model_issue_hook.rb | 47 - .../hooks/views_context_menus_hook.rb | 26 - .../redmine_tags/hooks/views_issues_hook.rb | 30 - .../redmine_tags/hooks/views_layouts_hook.rb | 27 - .../patches/action_controller_patch.rb | 49 - .../add_helpers_for_issue_tags_patch.rb | 29 - .../redmine_tags/patches/agile_query_patch.rb | 69 - .../auto_completes_controller_patch.rb | 56 - .../lib/redmine_tags/patches/issue_patch.rb | 115 - .../redmine_tags/patches/issue_query_patch.rb | 87 - .../patches/queries_helper_patch.rb | 51 - .../auto_completes_controller_test.rb | 33 +- .../functional/issue_tags_controller_test.rb | 106 +- .../test/functional/issues_controller_test.rb | 155 +- .../test/functional/tags_controller_test.rb | 43 +- plugins/redmineup_tags/test/test_helper.rb | 22 +- .../redmineup_tags/test/unit/issue_test.rb | 46 +- 335 files changed, 4099 insertions(+), 21787 deletions(-) delete mode 100644 plugins/easy_mindmup/README.md delete mode 100644 plugins/easy_mindmup/app/views/easy_mindmup/_test_includes.html.erb delete mode 100644 plugins/easy_mindmup/assets/javascripts/easy_mindmup.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/helpers/test.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/boot.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine-html.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/main.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/parse_form.js delete mode 100644 plugins/easy_mindmup/assets/javascripts/jasmine/saver_linearize.js delete mode 100644 plugins/easy_mindmup/assets/stylesheets/jasmine.css delete mode 100644 plugins/easy_mindmup/config/locales/ja.yml delete mode 100644 plugins/easy_wbs/README.md delete mode 100644 plugins/easy_wbs/app/models/easy_queries/easy_wbs_easy_issue_query.rb delete mode 100644 plugins/easy_wbs/assets/javascripts/easy_wbs.js delete mode 100644 plugins/easy_wbs/assets/javascripts/tests/encode_layout.js delete mode 100644 plugins/easy_wbs/assets/stylesheets/generated/easy_wbs.css delete mode 100644 plugins/redmine_agile/.drone.yml mode change 100755 => 100644 plugins/redmine_agile/README.rdoc mode change 100755 => 100644 plugins/redmine_agile/app/controllers/agile_boards_controller.rb delete mode 100644 plugins/redmine_agile/app/controllers/agile_colors_controller.rb delete mode 100644 plugins/redmine_agile/app/controllers/agile_queries_controller.rb delete mode 100644 plugins/redmine_agile/app/controllers/agile_versions_controller.rb delete mode 100644 plugins/redmine_agile/app/helpers/agile_versions_helper.rb delete mode 100644 plugins/redmine_agile/app/models/agile_color.rb mode change 100755 => 100644 plugins/redmine_agile/app/models/agile_data.rb delete mode 100644 plugins/redmine_agile/app/models/agile_versions_query.rb delete mode 100755 plugins/redmine_agile/app/views/agile_boards/_issues_sidebar.html.erb mode change 100755 => 100644 plugins/redmine_agile/app/views/agile_boards/index.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_colors/index.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/_columns.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/_filters.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/_form.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/edit.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/index.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_queries/new.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_versions/_board.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_versions/_version_issues.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_versions/autocomplete.js.erb delete mode 100644 plugins/redmine_agile/app/views/agile_versions/index.html.erb delete mode 100644 plugins/redmine_agile/app/views/agile_versions/load.js.erb delete mode 100644 plugins/redmine_agile/app/views/context_menus/_agile_colors.html.erb delete mode 100644 plugins/redmine_agile/app/views/issues/_issue_color.html.erb delete mode 100644 plugins/redmine_agile/app/views/issues/_issue_color_form.html.erb delete mode 100644 plugins/redmine_agile/app/views/projects/_project_color_form.html.erb mode change 100755 => 100644 plugins/redmine_agile/app/views/settings/agile/_general.html.erb delete mode 100644 plugins/redmine_agile/app/views/users/_user_color_form.html.erb delete mode 100644 plugins/redmine_agile/assets/images/pro_version_agile.png mode change 100755 => 100644 plugins/redmine_agile/assets/javascripts/jquery.simplecolorpicker.js mode change 100755 => 100644 plugins/redmine_agile/assets/javascripts/redmine_agile.js delete mode 100644 plugins/redmine_agile/assets/javascripts/redmine_agile_context_menu.js delete mode 100644 plugins/redmine_agile/assets/javascripts/visibility.min.js mode change 100755 => 100644 plugins/redmine_agile/assets/stylesheets/jquery.simplecolorpicker.css mode change 100755 => 100644 plugins/redmine_agile/assets/stylesheets/redmine_agile.css mode change 100755 => 100644 plugins/redmine_agile/config/locales/en.yml mode change 100755 => 100644 plugins/redmine_agile/config/locales/ru.yml mode change 100755 => 100644 plugins/redmine_agile/config/routes.rb mode change 100755 => 100644 plugins/redmine_agile/db/migrate/001_create_issue_status_orders.rb mode change 100755 => 100644 plugins/redmine_agile/doc/CHANGELOG mode change 100755 => 100644 plugins/redmine_agile/init.rb delete mode 100644 plugins/redmine_agile/lib/acts_as_colored/init.rb delete mode 100644 plugins/redmine_agile/lib/acts_as_colored/lib/acts_as_colored.rb mode change 100755 => 100644 plugins/redmine_agile/lib/redmine_agile.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/average_lead_time_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/burnup_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/cumulative_flow_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/lead_time_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/trackers_cumulative_flow_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/velocity_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/charts/work_burnup_chart.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/hooks/helper_issues_hook.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/hooks/views_context_menus_hook.rb mode change 100755 => 100644 plugins/redmine_agile/lib/redmine_agile/hooks/views_issues_hook.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/hooks/views_projects_form_hook.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/hooks/views_users_form_hook.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/compatibility/application_controller_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/compatibility_patch.rb mode change 100755 => 100644 plugins/redmine_agile/lib/redmine_agile/patches/issue_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/issue_priority_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/issue_query_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/tracker_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/patches/user_patch.rb delete mode 100644 plugins/redmine_agile/lib/redmine_agile/utils/header_tree.rb delete mode 100755 plugins/redmine_agile/test/functional/agile_board_controller_test.rb delete mode 100644 plugins/redmine_agile/test/functional/agile_queries_controller_test.rb delete mode 100644 plugins/redmine_agile/test/functional/agile_versions_controller_test.rb delete mode 100644 plugins/redmine_agile/test/unit/agile_color_test.rb delete mode 100644 plugins/redmine_agile/test/unit/agile_versions_query_test.rb delete mode 100644 plugins/redmine_checklists/app/controllers/checklist_template_categories_controller.rb delete mode 100644 plugins/redmine_checklists/app/controllers/checklist_templates_controller.rb delete mode 100644 plugins/redmine_checklists/app/models/checklist_template.rb delete mode 100644 plugins/redmine_checklists/app/models/checklist_template_category.rb delete mode 100644 plugins/redmine_checklists/app/views/checklist_template_categories/_form.html.erb delete mode 100644 plugins/redmine_checklists/app/views/checklist_template_categories/edit.html.erb delete mode 100644 plugins/redmine_checklists/app/views/checklist_template_categories/new.html.erb delete mode 100644 plugins/redmine_checklists/app/views/checklist_templates/_form.html.erb delete mode 100644 plugins/redmine_checklists/app/views/checklist_templates/edit.html.erb delete mode 100644 plugins/redmine_checklists/app/views/checklist_templates/new.html.erb delete mode 100644 plugins/redmine_checklists/app/views/issues/_checklist_templates.html.erb delete mode 100644 plugins/redmine_checklists/app/views/projects/settings/_checklist_templates.html.erb delete mode 100644 plugins/redmine_checklists/app/views/settings/checklists/_template_categories.html.erb delete mode 100644 plugins/redmine_checklists/app/views/settings/checklists/_templates.html.erb mode change 100755 => 100644 plugins/redmine_checklists/config/locales/fr.yml delete mode 100644 plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_controller_patch.rb delete mode 100644 plugins/redmine_checklists/lib/redmine_checklists/patches/projects_helper_patch.rb delete mode 100644 plugins/redmine_checklists/lib/redmine_checklists/redmine_checklist_setting.rb delete mode 100755 plugins/redmine_checklists/scripts/run_local.sh delete mode 100644 plugins/redmine_checklists/test/functional/checklist_template_categories_controller_test.rb delete mode 100644 plugins/redmine_checklists/test/functional/checklist_templates_controller_test.rb delete mode 100644 plugins/redmine_checklists/test/unit/checklist_template_category_test.rb delete mode 100644 plugins/redmine_checklists/test/unit/checklist_template_test.rb delete mode 100644 plugins/redmine_checklists/test/unit/journal_checklist_history_test.rb delete mode 160000 plugins/redmine_monitoring_controlling delete mode 160000 plugins/redmine_user_specific_theme delete mode 100644 plugins/redmine_work_time/.hgignore mode change 100755 => 100644 plugins/redmine_work_time/app/controllers/work_time_controller.rb mode change 100755 => 100644 plugins/redmine_work_time/config/locales/zh.yml delete mode 100644 plugins/redmineup_tags/Gemfile.lock delete mode 100644 plugins/redmineup_tags/app/views/issues/._tags_sidebar.html.erb.swp mode change 100755 => 100644 plugins/redmineup_tags/doc/CHANGELOG delete mode 100644 plugins/redmineup_tags/lib/redmine_tags.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/hooks/model_issue_hook.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/hooks/views_context_menus_hook.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/hooks/views_issues_hook.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/hooks/views_layouts_hook.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/action_controller_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/add_helpers_for_issue_tags_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/agile_query_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/auto_completes_controller_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/issue_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/issue_query_patch.rb delete mode 100644 plugins/redmineup_tags/lib/redmine_tags/patches/queries_helper_patch.rb diff --git a/plugins/easy_mindmup/Gemfile b/plugins/easy_mindmup/Gemfile index 0ab92e6..772940c 100644 --- a/plugins/easy_mindmup/Gemfile +++ b/plugins/easy_mindmup/Gemfile @@ -1,3 +1 @@ -unless %w(easyproject easy_gantt).any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) } - gem 'redmine_extensions', '~> 0.2.4' -end \ No newline at end of file +gem 'redmine_extensions' unless Dir.exist?(File.expand_path('../../easyproject', __FILE__)) diff --git a/plugins/easy_mindmup/README.md b/plugins/easy_mindmup/README.md deleted file mode 100644 index a89d93d..0000000 --- a/plugins/easy_mindmup/README.md +++ /dev/null @@ -1 +0,0 @@ -# Easy Mindmup diff --git a/plugins/easy_mindmup/after_init.rb b/plugins/easy_mindmup/after_init.rb index 837c915..560a27c 100644 --- a/plugins/easy_mindmup/after_init.rb +++ b/plugins/easy_mindmup/after_init.rb @@ -1,6 +1,6 @@ app_dir = File.join(File.dirname(__FILE__), 'app') lib_dir = File.join(File.dirname(__FILE__), 'lib', 'easy_mindmup') -ActionDispatch::Reloader.to_prepare do +RedmineExtensions::Reloader.to_prepare do require 'easy_mindmup/easy_mindmup' end diff --git a/plugins/easy_mindmup/app/helpers/easy_mindmup_helper.rb b/plugins/easy_mindmup/app/helpers/easy_mindmup_helper.rb index 4bebec3..a72e35a 100644 --- a/plugins/easy_mindmup/app/helpers/easy_mindmup_helper.rb +++ b/plugins/easy_mindmup/app/helpers/easy_mindmup_helper.rb @@ -18,18 +18,4 @@ module EasyMindmupHelper end end - def prepare_test_includes(tests) - includes = [] - tests.each do |test_file| - plugin = 'easy_mindmup' - if test_file.include?('/') - split = test_file.split('/') - plugin = split[0] - test_file = split[1] - end - includes << [test_file, plugin] - end - includes - end - end diff --git a/plugins/easy_mindmup/app/views/easy_mindmup/_includes.html.erb b/plugins/easy_mindmup/app/views/easy_mindmup/_includes.html.erb index 3a24075..842e70d 100644 --- a/plugins/easy_mindmup/app/views/easy_mindmup/_includes.html.erb +++ b/plugins/easy_mindmup/app/views/easy_mindmup/_includes.html.erb @@ -1,71 +1,77 @@ <% include_calendar_headers_tags %> <% heads_for_wiki_formatter %> -<% if EasyMindmup.easy_extensions? %> +<% if defined?(EasyExtensions) %> <%= stylesheet_link_tag('easy_mindmup', :media => 'all') %> <% else %> + <%= stylesheet_link_tag('generated/easy_mindmup', :plugin => 'easy_mindmup', :media => 'all') %> + <%#= stylesheet_link_tag('easy_mindmup', :plugin => 'easy_mindmup', :media => 'all') %> <%= stylesheet_link_tag('context_menu', :media => 'all') %> <% end %> -<% if EasyMindmup.combine_by_pipeline?(params) %> - <%= javascript_include_tag('easy_mindmup') %> +<%= javascript_include_tag('external', :plugin => 'easy_mindmup') %> +<% if false && defined?(EasyExtensions) %> + <%= javascript_include_tag('easy_wbs', :plugin => 'easy_mindmup') %> <% else %> - <%= javascript_include_tag( - 'external', - - 'mindmup/mapjs', - 'mindmup/clipboard', - 'mindmup/content', - 'mindmup/dom-map-view', - 'mindmup/dom-map-widget', - 'mindmup/hammer-draggable', - # 'mindmup/image-drop-widget', - 'mindmup/layout', - #'mindmup/link-edit-widget', - 'mindmup/map-model', - 'mindmup/map-toolbar-widget', - 'mindmup/observable', - # 'mindmup/url-helper', - - 'polyfill', - 'utils', - 'main', - 'mapjs_init', - 'event_bus', - 'redrawer', - 'toolbar', - 'history', - 'data', - 'links', - 'loader', - # 'last_state', - 'saver', - 'autosave', - 'save_progress', - 'save_info', - 'filter', - 'node_patch', - 'dom_patch', - 'map_model_patch', - 'model_classes', - 'after_change', - 'styles', - 'logger', - # 'gateway', - # 'modals', - 'validator', - 'storage', - 'legend', - 'legend_events', - 'expand_all', - 'print', - 'mm_context_menu', - 'layout_patch', - 'content_patch', - 'links', - 'link_edit_widget', :plugin => 'easy_mindmup') - %> + <script type="application/javascript"> + window.easyDartLoaders = window.easyDartLoaders || []; + </script> + <%= begin javascript_include_tag( + 'mindmup/mapjs', + 'mindmup/clipboard', + 'mindmup/content', + 'mindmup/dom-map-view', + 'mindmup/dom-map-widget', + 'mindmup/hammer-draggable', + 'mindmup/image-drop-widget', + 'mindmup/layout', + #'mindmup/link-edit-widget', + 'mindmup/map-model', + 'mindmup/map-toolbar-widget', + 'mindmup/observable', + 'mindmup/url-helper', :plugin => 'easy_mindmup') + end %> + <%= begin javascript_include_tag( + :libs, + :polyfill, + :utils, + :main, + :mapjs_init, + :event_bus, + :redrawer, + :toolbar, + :history, + :data, + :links, + :loader, + # :last_state, + :saver, + :autosave, + :save_progress, + :save_info, + :filter, + :node_patch, + :dom_patch, + :map_model_patch, + :model_classes, + :after_change, + :styles, + :logger, + # :gateway, + # :modals, + :validator, + :storage, + :legend, + :legend_events, + :expand_all, + :print, + :mm_context_menu, + :layout_patch, + :content_patch, + :links, + :link_edit_widget, :plugin => 'easy_mindmup') + end %> <% end %> -<% if EasyMindmup.easy_extensions? +<% if defined?(EasyExtensions) include_front_end_commons if respond_to? :include_front_end_commons end %> diff --git a/plugins/easy_mindmup/app/views/easy_mindmup/_js_prepare.html.erb b/plugins/easy_mindmup/app/views/easy_mindmup/_js_prepare.html.erb index 0aaff39..14cb604 100644 --- a/plugins/easy_mindmup/app/views/easy_mindmup/_js_prepare.html.erb +++ b/plugins/easy_mindmup/app/views/easy_mindmup/_js_prepare.html.erb @@ -1,5 +1,5 @@ <script type="text/javascript"> - window.easyMindMupSetting = $.extend(true, window.easyMindMupSetting, <%= { + window.easyMindMupSetting = <%= { easyRedmine: EasyMindmup.easy_extensions?, rootID: @project.id, apiKey: User.current.api_key, @@ -133,7 +133,7 @@ </div> } } - }.to_json.html_safe %>); + }.to_json.html_safe %> $(document).ready(function(){ $("p.nodata").remove() diff --git a/plugins/easy_mindmup/app/views/easy_mindmup/_test_includes.html.erb b/plugins/easy_mindmup/app/views/easy_mindmup/_test_includes.html.erb deleted file mode 100644 index 55bcea0..0000000 --- a/plugins/easy_mindmup/app/views/easy_mindmup/_test_includes.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%= - javascript_include_tag( - # test framework - 'jasmine/helpers/test', - 'jasmine/jasmine_lib/jasmine', - 'jasmine/jasmine_lib/jasmine-html', - 'jasmine/jasmine_lib/boot', - - # common tests - 'jasmine/main', - 'jasmine/parse_form', - 'jasmine/saver_linearize', - plugin: :easy_mindmup) -%> -<% extra_test_names = params[:run_jasmine_tests] - if extra_test_names != 'true' - if extra_test_names.is_a?(String) - extra_tests = prepare_test_includes([extra_test_names]) - elsif extra_test_names.is_a?(Array) - extra_tests = prepare_test_includes(extra_test_names) - else - extra_tests = [] - end - extra_tests.each do |test, plugin| %> - <%= javascript_include_tag("jasmine/#{test}", plugin: plugin) %> - <% end %> -<% end %> -<%= stylesheet_link_tag('jasmine', media: 'all', plugin: :easy_mindmup) %> diff --git a/plugins/easy_mindmup/assets/javascripts/content_patch.js b/plugins/easy_mindmup/assets/javascripts/content_patch.js index f80f77b..373c849 100644 --- a/plugins/easy_mindmup/assets/javascripts/content_patch.js +++ b/plugins/easy_mindmup/assets/javascripts/content_patch.js @@ -2,11 +2,9 @@ /** * * @param {MindMup} ysy - * @property {MindMup} ysy * @constructor */ function ContentPatch(ysy) { - this.ysy = ysy; this.patch(ysy); } @@ -15,7 +13,6 @@ * @param {MindMup} ysy */ ContentPatch.prototype.patch = function (ysy) { - var self = this; ysy.eventBus.register("TreeLoaded", /** @param {RootIdea} contentAggregate*/ function (contentAggregate) { var commandProcessors = contentAggregate.getCommandProcessors(); contentAggregate.clone = function (subIdeaId) { @@ -32,53 +29,7 @@ commandProcessors.setCustomData = function (originId, idea, obj) { ysy.setCustomData(idea, obj); }; - contentAggregate.updateOneSide = $.proxy(self.updateOneSide, self); }); }; - /** - * - * @param {RootIdea} idea - */ - ContentPatch.prototype.updateOneSide = function (idea) { - var i; - var oneSideOn = this.ysy.settings.oneSideOn; - if (oneSideOn === idea.oneSideOn) return; - var ranks = Object.getOwnPropertyNames(idea.ideas).map(function (i) { - return parseFloat(i); - }).sort(function (a, b) { - return a - b - }); - if (ranks.length === 0) return; - var constructed = {}; - if (oneSideOn) { - var firstPositive = _.findIndex(ranks, function (rank) { - return rank > 0; - }); - var pointer = 1; - for (i = firstPositive; i < ranks.length; i++) { - constructed[pointer++] = idea.ideas[ranks[i]]; - } - for (i = 0; i < firstPositive; i++) { - constructed[pointer++] = idea.ideas[ranks[i]]; - } - } else { - if (ranks[0] < 0) { - return; - } - var middlePoint = Math.ceil(ranks.length / 2); - //index = i % 2 !== 0 ? -i / 2 - 0.5 : i / 2 + 1; - for (i = 0; i < middlePoint; i++) { - constructed[i + 1] = idea.ideas[ranks[i]]; - } - for (i = middlePoint; i < ranks.length; i++) { - constructed[i - ranks.length] = idea.ideas[ranks[i]]; - } - } - idea.oneSideOn = oneSideOn; - idea.ideas = constructed; - if (idea.dispatchEvent) { - idea.dispatchEvent("changed"); - } - }; window.easyMindMupClasses.ContentPatch = ContentPatch; })(); \ No newline at end of file diff --git a/plugins/easy_mindmup/assets/javascripts/easy_mindmup.js b/plugins/easy_mindmup/assets/javascripts/easy_mindmup.js deleted file mode 100644 index ed30119..0000000 --- a/plugins/easy_mindmup/assets/javascripts/easy_mindmup.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * = require external - * = require mindmup/mapjs - * = require_directory ./mindmup - * = require libs - * = require polyfill - * = require utils - * = require_directory . - * = stub mindmup/link-edit-widget - * = stub jsdocs_external - */ diff --git a/plugins/easy_mindmup/assets/javascripts/external.js b/plugins/easy_mindmup/assets/javascripts/external.js index b267393..00f9b85 100644 --- a/plugins/easy_mindmup/assets/javascripts/external.js +++ b/plugins/easy_mindmup/assets/javascripts/external.js @@ -38,8 +38,6 @@ * Licensed under the MIT license */ !function(a,b){"use strict";function c(a,c){Date.now||(Date.now=function(){return(new Date).getTime()}),a.utils.each(["on","off"],function(d){a.utils[d]=function(a,e,f){c(a)[d](e,function(a){var d=c.extend({},a.originalEvent,a);d.button===b&&(d.button=a.which-1),f.call(this,d)})}}),a.Instance.prototype.trigger=function(a,b){var d=c(this.element);return d.has(b.target).length&&(d=c(b.target)),d.trigger({type:a,gesture:b})},c.fn.hammer=function(b){return this.each(function(){var d=c(this),e=d.data("hammer");e?e&&b&&a.utils.extend(e.options,b):d.data("hammer",new a(this,b||{}))})}}"function"==typeof define&&define.amd?define(["hammerjs","jquery"],c):c(a.Hammer,a.jQuery||a.Zepto)}(window); -/* Mustache.js */ -(function defineMustache(global,factory){if(typeof exports==="object"&&exports&&typeof exports.nodeName!=="string"){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{global.Mustache={};factory(global.Mustache)}})(this,function mustacheFactory(mustache){var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i<valueLength;++i){chr=value.charAt(i);if(isWhitespace(chr)){spaces.push(tokens.length)}else{nonSpace=true}tokens.push(["text",chr,start,start+1]);start+=1;if(chr==="\n")stripSpace()}}if(!scanner.scan(openingTagRe))break;hasTag=true;type=scanner.scan(tagRe)||"name";scanner.scan(whiteRe);if(type==="="){value=scanner.scanUntil(equalsRe);scanner.scan(equalsRe);scanner.scanUntil(closingTagRe)}else if(type==="{"){value=scanner.scanUntil(closingCurlyRe);scanner.scan(curlyRe);scanner.scanUntil(closingTagRe);type="&"}else{value=scanner.scanUntil(closingTagRe)}if(!scanner.scan(closingTagRe))throw new Error("Unclosed tag at "+scanner.pos);token=[type,value,start,scanner.pos];tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i<numTokens;++i){token=tokens[i];if(token){if(token[0]==="text"&&lastToken&&lastToken[0]==="text"){lastToken[1]+=token[1];lastToken[3]=token[3]}else{squashedTokens.push(token);lastToken=token}}}return squashedTokens}function nestTokens(tokens){var nestedTokens=[];var collector=nestedTokens;var sections=[];var token,section;for(var i=0,numTokens=tokens.length;i<numTokens;++i){token=tokens[i];switch(token[0]){case"#":case"^":collector.push(token);sections.push(token);collector=token[4]=[];break;case"/":section=sections.pop();section[5]=token[2];collector=sections.length>0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){value=context.view;names=name.split(".");index=0;while(value!=null&&index<names.length){if(index===names.length-1)lookupHit=hasProperty(value,names[index]);value=value[names[index++]]}}else{value=context.view[name];lookupHit=hasProperty(context.view,name)}if(lookupHit)break;context=context.parent}cache[name]=value}if(isFunction(value))value=value.call(this.view);return value};function Writer(){this.cache={}}Writer.prototype.clearCache=function clearCache(){this.cache={}};Writer.prototype.parse=function parse(template,tags){var cache=this.cache;var tokens=cache[template];if(tokens==null)tokens=cache[template]=parseTemplate(template,tags);return tokens};Writer.prototype.render=function render(template,view,partials){var tokens=this.parse(template);var context=view instanceof Context?view:new Context(view);return this.renderTokens(tokens,context,partials,template)};Writer.prototype.renderTokens=function renderTokens(tokens,context,partials,originalTemplate){var buffer="";var token,symbol,value;for(var i=0,numTokens=tokens.length;i<numTokens;++i){value=undefined;token=tokens[i];symbol=token[0];if(symbol==="#")value=this.renderSection(token,context,partials,originalTemplate);else if(symbol==="^")value=this.renderInverted(token,context,partials,originalTemplate);else if(symbol===">")value=this.renderPartial(token,context,partials,originalTemplate);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j<valueLength;++j){buffer+=this.renderTokens(token[4],context.push(value[j]),partials,originalTemplate)}}else if(typeof value==="object"||typeof value==="string"||typeof value==="number"){buffer+=this.renderTokens(token[4],context.push(value),partials,originalTemplate)}else if(isFunction(value)){if(typeof originalTemplate!=="string")throw new Error("Cannot use higher-order sections without the original template");value=value.call(context.view,originalTemplate.slice(token[3],token[5]),subRender);if(value!=null)buffer+=value}else{buffer+=this.renderTokens(token[4],context,partials,originalTemplate)}return buffer};Writer.prototype.renderInverted=function renderInverted(token,context,partials,originalTemplate){var value=context.lookup(token[1]);if(!value||isArray(value)&&value.length===0)return this.renderTokens(token[4],context,partials,originalTemplate)};Writer.prototype.renderPartial=function renderPartial(token,context,partials){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null)return this.renderTokens(this.parse(value),context,partials,value)};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};mustache.name="mustache.js";mustache.version="2.2.1";mustache.tags=["{{","}}"];var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer}); /* * jQuery Hotkeys Plugin * Copyright 2010, John Resig diff --git a/plugins/easy_mindmup/assets/javascripts/filter.js b/plugins/easy_mindmup/assets/javascripts/filter.js index 5069334..b4c7206 100644 --- a/plugins/easy_mindmup/assets/javascripts/filter.js +++ b/plugins/easy_mindmup/assets/javascripts/filter.js @@ -6,45 +6,15 @@ */ function Filter(ysy) { this.ysy = ysy; - this.allowedValues = []; - this.init(ysy); } - Filter.prototype.init = function (ysy) { - var self = this; - ysy.eventBus.register("nodeStyleChanged", function () { - self.reset(); - }); - }; - - Filter.prototype.className = "mindmup-node-filtered"; - Filter.prototype.pushAllowed = function (value) { - this.allowedValues.push(value || 0); - this.sweepNodes(); - }; Filter.prototype.isOn = function(){ - return !!this.allowedValues.length; - }; - Filter.prototype.removeAllowed = function (value) { - this.allowedValues = _.without(this.allowedValues, value); - this.sweepNodes(); - }; - Filter.prototype.toggleAllowed = function (value) { - var store = this.ysy.styles.getCurrentStyle(); - if (store) this.store = store; - if (_.contains(this.allowedValues, value)) { - this.removeAllowed(value); - } else { - this.pushAllowed(value); - } + return false; }; Filter.prototype.cssByBannedValue = function (value) { - if (!this.allowedValues.length) return ""; - return _.contains(this.allowedValues, value) ? "" : " " + this.className; + return ""; }; Filter.prototype.reset = function () { - this.ysy.$container.find("." + this.className).removeClass(this.className); - this.allowedValues = []; }; /** * @@ -52,29 +22,7 @@ * @return {boolean} */ Filter.prototype.isBanned = function (idea) { - if (!this.allowedValues.length) return false; - if (idea.attr.isFresh) return false; - var data = this.ysy.getData(idea); - var value = this.store.value(data); - return this.allowedValues.indexOf(value) === -1; - }; - Filter.prototype.sweepNodes = function () { - this.recursiveBanner(this.ysy.idea); + return false; }; - /** - * - * @param {ModelEntity} idea - */ - Filter.prototype.recursiveBanner = function (idea) { - var banned = this.isBanned(idea); - var node = this.ysy.getNodeElement(idea); - if (node.length) { - node.toggleClass(this.className, banned); - } - if (idea.ideas) { - _.each(idea.ideas, this.recursiveBanner, this) - } - }; - window.easyMindMupClasses.Filter = Filter; })(); diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/helpers/test.js b/plugins/easy_mindmup/assets/javascripts/jasmine/helpers/test.js deleted file mode 100644 index e3357e9..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/helpers/test.js +++ /dev/null @@ -1,107 +0,0 @@ -(function () { - /** - * Class responsible for Jasmine testing - * @param {MindMup} ysy - * @property {MindMup} ysy - * @constructor - */ - function JasmineTests(ysy) { - this.ysy = ysy; - this._redrawRequested = true; - this.jasmineStarted = false; - this.startCounter = 0; - this.beatCallbacks = []; - this.extraTestNames = []; - this.extraTestFunctions = []; - this.init(ysy); - } - - /** - * - * @param {MindMup} ysy - */ - JasmineTests.prototype.init = function (ysy) { - ysy.eventBus.register("TreeLoaded",function () { - if (!this.jasmineStarted) { - this.jasmineStarted = true; - window.jasmine.jasmineStart(ysy); - } - }); - ysy.repainter.redrawMe(this); - this.loadExtraTests(); - }; - JasmineTests.prototype._render = function () { - if (this.beatCallbacks.length) { - var newCallbacks = []; - for (var i = 0; i < this.beatCallbacks.length; i++) { - var callPack = this.beatCallbacks[i]; - if (callPack.rounds === 1) { - callPack.callback(); - } else { - callPack.rounds--; - newCallbacks.push(callPack); - } - } - this.beatCallbacks = newCallbacks; - if(newCallbacks.length){ - this.ysy.repainter.redrawMe(this); - } - } - }; - JasmineTests.prototype.fewBeatsAfter = function (callback, count) { - if (count === undefined) count = 2; - this.beatCallbacks.push({callback: callback, rounds: count}); - this.ysy.repainter.redrawMe(this); - }; - JasmineTests.prototype.loadExtraTests = function () { - var self = this; - describe("(EXTRA)", function () { - for (var i = 0; i < self.extraTestFunctions.length; i++) { - self.extraTestFunctions[i](); - } - }); - }; - JasmineTests.prototype.parseResult = function () { - var specs = window.jsApiReporter.specs(); - var shortReport = ""; - var report = ""; - var allPassed = true; - var result = ""; - for (var i = 0; i < specs.length; i++) { - var spec = specs[i]; - if (spec.status === "passed") { - shortReport += "."; - } else { - allPassed = false; - shortReport += "X"; - report += "__TEST " + spec.fullName + "______\n"; - for (var j = 0; j < spec.failedExpectations.length; j++) { - var fail = spec.failedExpectations[j]; - var split = fail.stack.split("\n"); - result += window.location + "\n"; - report += " " + fail.message + "\n"; - for (var k = 1; k < split.length; k++) { - if (split[k].indexOf("/jasmine_lib/") > -1) break; - report += split[k] + "\n"; - } - } - } - } - if (allPassed) { - return "success"; - } - result += " RESULTS: " + shortReport + "\n" + report; - $("#content").text(result.replace("\n", "<br>")); - return result; - }; - easyMindMupClasses.JasmineTests = JasmineTests; -})(); -window.describeExtra = function (file, func) { - if (file.indexOf("/") === -1) { - file = "easy_mindmup/" + file - } - jasmine.ysyInstance.tests.extraTestNames.push(file); - jasmine.ysyInstance.tests.extraTestFunctions.push(function () { - describe(file, func); - }); -}; diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/boot.js b/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/boot.js deleted file mode 100644 index c1fd802..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/boot.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. - - If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. - - The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. - - [jasmine-gem]: http://github.com/pivotal/jasmine-gem - */ - -(function() { - - /** - * ## Require & Instantiate - * - * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. - */ - window.jasmine = jasmineRequire.core(jasmineRequire); - - /** - * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. - */ - jasmineRequire.html(jasmine); - - /** - * Create the Jasmine environment. This is used to run all specs in a project. - */ - var env = jasmine.getEnv(); - - /** - * ## The Global Interface - * - * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. - */ - var jasmineInterface = jasmineRequire.interface(jasmine, env); - - /** - * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. - */ - extend(window, jasmineInterface); - - /** - * ## Runner Parameters - * - * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. - */ - - var queryString = new jasmine.QueryString({ - getWindowLocation: function() { return window.location; } - }); - - var catchingExceptions = queryString.getParam("catch"); - env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); - - var throwingExpectationFailures = queryString.getParam("throwFailures"); - env.throwOnExpectationFailure(throwingExpectationFailures); - - var random = queryString.getParam("random"); - env.randomizeTests(random); - - var seed = queryString.getParam("seed"); - if (seed) { - env.seed(seed); - } - - /** - * ## Reporters - * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). - */ - var htmlReporter = new jasmine.HtmlReporter({ - env: env, - onRaiseExceptionsClick: function() { queryString.navigateWithNewParam("catch", !env.catchingExceptions()); }, - onThrowExpectationsClick: function() { queryString.navigateWithNewParam("throwFailures", !env.throwingExpectationFailures()); }, - onRandomClick: function() { queryString.navigateWithNewParam("random", !env.randomTests()); }, - addToExistingQueryString: function(key, value) { return queryString.fullStringWithNewParam(key, value); }, - getContainer: function() { return document.body; }, - createElement: function() { return document.createElement.apply(document, arguments); }, - createTextNode: function() { return document.createTextNode.apply(document, arguments); }, - timer: new jasmine.Timer() - }); - - /** - * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. - */ - env.addReporter(jasmineInterface.jsApiReporter); - env.addReporter(htmlReporter); - - /** - * Filter which specs will be run by matching the start of the full name against the `spec` query param. - */ - var specFilter = new jasmine.HtmlSpecFilter({ - filterString: function() { return queryString.getParam("spec"); } - }); - - env.specFilter = function(spec) { - return specFilter.matches(spec.getFullName()); - }; - - /** - * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. - */ - window.setTimeout = window.setTimeout; - window.setInterval = window.setInterval; - window.clearTimeout = window.clearTimeout; - window.clearInterval = window.clearInterval; - - /** - * ## Execution - * - * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. - */ - var currentWindowOnload = window.onload; - /** - * @type {MindMup} - */ - jasmine.ysyInstance = null; - jasmine.jasmineStart = function(ysyInstance) { - jasmine.ysyInstance = ysyInstance; - // window.onload = function() { // HOSEKP - // if (currentWindowOnload) { - // currentWindowOnload(); - // } // HOSEKP - htmlReporter.initialize(); - env.execute(); - }; - - /** - * Helper function for readability above. - */ - function extend(destination, source) { - for (var property in source) destination[property] = source[property]; - return destination; - } - -}()); diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine-html.js b/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine-html.js deleted file mode 100644 index da23532..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine-html.js +++ /dev/null @@ -1,473 +0,0 @@ -/* -Copyright (c) 2008-2015 Pivotal Labs - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -jasmineRequire.html = function(j$) { - j$.ResultsNode = jasmineRequire.ResultsNode(); - j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); - j$.QueryString = jasmineRequire.QueryString(); - j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); -}; - -jasmineRequire.HtmlReporter = function(j$) { - - var noopTimer = { - start: function() {}, - elapsed: function() { return 0; } - }; - - function HtmlReporter(options) { - var env = options.env || {}, - getContainer = options.getContainer, - createElement = options.createElement, - createTextNode = options.createTextNode, - onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, - onThrowExpectationsClick = options.onThrowExpectationsClick || function() {}, - onRandomClick = options.onRandomClick || function() {}, - addToExistingQueryString = options.addToExistingQueryString || defaultQueryString, - timer = options.timer || noopTimer, - results = [], - specsExecuted = 0, - failureCount = 0, - pendingSpecCount = 0, - htmlReporterMain, - symbols, - failedSuites = []; - - this.initialize = function() { - clearPrior(); - htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'}, - createDom('div', {className: 'jasmine-banner'}, - createDom('a', {className: 'jasmine-title', href: 'http://jasmine.github.io/', target: '_blank'}), - createDom('span', {className: 'jasmine-version'}, j$.version) - ), - createDom('ul', {className: 'jasmine-symbol-summary'}), - createDom('div', {className: 'jasmine-alert'}), - createDom('div', {className: 'jasmine-results'}, - createDom('div', {className: 'jasmine-failures'}) - ) - ); - getContainer().appendChild(htmlReporterMain); - }; - - var totalSpecsDefined; - this.jasmineStarted = function(options) { - totalSpecsDefined = options.totalSpecsDefined || 0; - timer.start(); - }; - - var summary = createDom('div', {className: 'jasmine-summary'}); - - var topResults = new j$.ResultsNode({}, '', null), - currentParent = topResults; - - this.suiteStarted = function(result) { - currentParent.addChild(result, 'suite'); - currentParent = currentParent.last(); - }; - - this.suiteDone = function(result) { - if (result.status == 'failed') { - failedSuites.push(result); - } - - if (currentParent == topResults) { - return; - } - - currentParent = currentParent.parent; - }; - - this.specStarted = function(result) { - currentParent.addChild(result, 'spec'); - }; - - var failures = []; - this.specDone = function(result) { - if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { - console.error('Spec \'' + result.fullName + '\' has no expectations.'); - } - - if (result.status != 'disabled') { - specsExecuted++; - } - - if (!symbols){ - symbols = find('.jasmine-symbol-summary'); - } - - symbols.appendChild(createDom('li', { - className: noExpectations(result) ? 'jasmine-empty' : 'jasmine-' + result.status, - id: 'spec_' + result.id, - title: result.fullName - } - )); - - if (result.status == 'failed') { - failureCount++; - - var failure = - createDom('div', {className: 'jasmine-spec-detail jasmine-failed'}, - createDom('div', {className: 'jasmine-description'}, - createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName) - ), - createDom('div', {className: 'jasmine-messages'}) - ); - var messages = failure.childNodes[1]; - - for (var i = 0; i < result.failedExpectations.length; i++) { - var expectation = result.failedExpectations[i]; - messages.appendChild(createDom('div', {className: 'jasmine-result-message'}, expectation.message)); - messages.appendChild(createDom('div', {className: 'jasmine-stack-trace'}, expectation.stack)); - } - - failures.push(failure); - } - - if (result.status == 'pending') { - pendingSpecCount++; - } - }; - - this.jasmineDone = function(doneResult) { - var banner = find('.jasmine-banner'); - var alert = find('.jasmine-alert'); - var order = doneResult && doneResult.order; - alert.appendChild(createDom('span', {className: 'jasmine-duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); - - banner.appendChild( - createDom('div', { className: 'jasmine-run-options' }, - createDom('span', { className: 'jasmine-trigger' }, 'Options'), - createDom('div', { className: 'jasmine-payload' }, - createDom('div', { className: 'jasmine-exceptions' }, - createDom('input', { - className: 'jasmine-raise', - id: 'jasmine-raise-exceptions', - type: 'checkbox' - }), - createDom('label', { className: 'jasmine-label', 'for': 'jasmine-raise-exceptions' }, 'raise exceptions')), - createDom('div', { className: 'jasmine-throw-failures' }, - createDom('input', { - className: 'jasmine-throw', - id: 'jasmine-throw-failures', - type: 'checkbox' - }), - createDom('label', { className: 'jasmine-label', 'for': 'jasmine-throw-failures' }, 'stop spec on expectation failure')), - createDom('div', { className: 'jasmine-random-order' }, - createDom('input', { - className: 'jasmine-random', - id: 'jasmine-random-order', - type: 'checkbox' - }), - createDom('label', { className: 'jasmine-label', 'for': 'jasmine-random-order' }, 'run tests in random order')) - ) - )); - - var raiseCheckbox = find('#jasmine-raise-exceptions'); - - raiseCheckbox.checked = !env.catchingExceptions(); - raiseCheckbox.onclick = onRaiseExceptionsClick; - - var throwCheckbox = find('#jasmine-throw-failures'); - throwCheckbox.checked = env.throwingExpectationFailures(); - throwCheckbox.onclick = onThrowExpectationsClick; - - var randomCheckbox = find('#jasmine-random-order'); - randomCheckbox.checked = env.randomTests(); - randomCheckbox.onclick = onRandomClick; - - var optionsMenu = find('.jasmine-run-options'), - optionsTrigger = optionsMenu.querySelector('.jasmine-trigger'), - optionsPayload = optionsMenu.querySelector('.jasmine-payload'), - isOpen = /\bjasmine-open\b/; - - optionsTrigger.onclick = function() { - if (isOpen.test(optionsPayload.className)) { - optionsPayload.className = optionsPayload.className.replace(isOpen, ''); - } else { - optionsPayload.className += ' jasmine-open'; - } - }; - - if (specsExecuted < totalSpecsDefined) { - var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; - alert.appendChild( - createDom('span', {className: 'jasmine-bar jasmine-skipped'}, - createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage) - ) - ); - } - var statusBarMessage = ''; - var statusBarClassName = 'jasmine-bar '; - - if (totalSpecsDefined > 0) { - statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); - if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } - statusBarClassName += (failureCount > 0) ? 'jasmine-failed' : 'jasmine-passed'; - } else { - statusBarClassName += 'jasmine-skipped'; - statusBarMessage += 'No specs found'; - } - - var seedBar; - if (order && order.random) { - seedBar = createDom('span', {className: 'jasmine-seed-bar'}, - ', randomized with seed ', - createDom('a', {title: 'randomized with seed ' + order.seed, href: seedHref(order.seed)}, order.seed) - ); - } - - alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage, seedBar)); - - for(i = 0; i < failedSuites.length; i++) { - var failedSuite = failedSuites[i]; - for(var j = 0; j < failedSuite.failedExpectations.length; j++) { - var errorBarMessage = 'AfterAll ' + failedSuite.failedExpectations[j].message; - var errorBarClassName = 'jasmine-bar jasmine-errored'; - alert.appendChild(createDom('span', {className: errorBarClassName}, errorBarMessage)); - } - } - - var results = find('.jasmine-results'); - results.appendChild(summary); - - summaryList(topResults, summary); - - function summaryList(resultsTree, domParent) { - var specListNode; - for (var i = 0; i < resultsTree.children.length; i++) { - var resultNode = resultsTree.children[i]; - if (resultNode.type == 'suite') { - var suiteListNode = createDom('ul', {className: 'jasmine-suite', id: 'suite-' + resultNode.result.id}, - createDom('li', {className: 'jasmine-suite-detail'}, - createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description) - ) - ); - - summaryList(resultNode, suiteListNode); - domParent.appendChild(suiteListNode); - } - if (resultNode.type == 'spec') { - if (domParent.getAttribute('class') != 'jasmine-specs') { - specListNode = createDom('ul', {className: 'jasmine-specs'}); - domParent.appendChild(specListNode); - } - var specDescription = resultNode.result.description; - if(noExpectations(resultNode.result)) { - specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; - } - if(resultNode.result.status === 'pending' && resultNode.result.pendingReason !== '') { - specDescription = specDescription + ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason; - } - specListNode.appendChild( - createDom('li', { - className: 'jasmine-' + resultNode.result.status, - id: 'spec-' + resultNode.result.id - }, - createDom('a', {href: specHref(resultNode.result)}, specDescription) - ) - ); - } - } - } - - if (failures.length) { - alert.appendChild( - createDom('span', {className: 'jasmine-menu jasmine-bar jasmine-spec-list'}, - createDom('span', {}, 'Spec List | '), - createDom('a', {className: 'jasmine-failures-menu', href: '#'}, 'Failures'))); - alert.appendChild( - createDom('span', {className: 'jasmine-menu jasmine-bar jasmine-failure-list'}, - createDom('a', {className: 'jasmine-spec-list-menu', href: '#'}, 'Spec List'), - createDom('span', {}, ' | Failures '))); - - find('.jasmine-failures-menu').onclick = function() { - setMenuModeTo('jasmine-failure-list'); - }; - find('.jasmine-spec-list-menu').onclick = function() { - setMenuModeTo('jasmine-spec-list'); - }; - - setMenuModeTo('jasmine-failure-list'); - - var failureNode = find('.jasmine-failures'); - for (var i = 0; i < failures.length; i++) { - failureNode.appendChild(failures[i]); - } - } - }; - - return this; - - function find(selector) { - return getContainer().querySelector('.jasmine_html-reporter ' + selector); - } - - function clearPrior() { - // return the reporter - var oldReporter = find(''); - - if(oldReporter) { - getContainer().removeChild(oldReporter); - } - } - - function createDom(type, attrs, childrenVarArgs) { - var el = createElement(type); - - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; - - if (typeof child === 'string') { - el.appendChild(createTextNode(child)); - } else { - if (child) { - el.appendChild(child); - } - } - } - - for (var attr in attrs) { - if (attr == 'className') { - el[attr] = attrs[attr]; - } else { - el.setAttribute(attr, attrs[attr]); - } - } - - return el; - } - - function pluralize(singular, count) { - var word = (count == 1 ? singular : singular + 's'); - - return '' + count + ' ' + word; - } - - function specHref(result) { - return addToExistingQueryString('spec', result.fullName); - } - - function seedHref(seed) { - return addToExistingQueryString('seed', seed); - } - - function defaultQueryString(key, value) { - return '?' + key + '=' + value; - } - - function setMenuModeTo(mode) { - htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); - } - - function noExpectations(result) { - return (result.failedExpectations.length + result.passedExpectations.length) === 0 && - result.status === 'passed'; - } - } - - return HtmlReporter; -}; - -jasmineRequire.HtmlSpecFilter = function() { - function HtmlSpecFilter(options) { - var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - var filterPattern = new RegExp(filterString); - - this.matches = function(specName) { - return filterPattern.test(specName); - }; - } - - return HtmlSpecFilter; -}; - -jasmineRequire.ResultsNode = function() { - function ResultsNode(result, type, parent) { - this.result = result; - this.type = type; - this.parent = parent; - - this.children = []; - - this.addChild = function(result, type) { - this.children.push(new ResultsNode(result, type, this)); - }; - - this.last = function() { - return this.children[this.children.length - 1]; - }; - } - - return ResultsNode; -}; - -jasmineRequire.QueryString = function() { - function QueryString(options) { - - this.navigateWithNewParam = function(key, value) { - options.getWindowLocation().search = this.fullStringWithNewParam(key, value); - }; - - this.fullStringWithNewParam = function(key, value) { - var paramMap = queryStringToParamMap(); - paramMap[key] = value; - return toQueryString(paramMap); - }; - - this.getParam = function(key) { - return queryStringToParamMap()[key]; - }; - - return this; - - function toQueryString(paramMap) { - var qStrPairs = []; - for (var prop in paramMap) { - qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])); - } - return '?' + qStrPairs.join('&'); - } - - function queryStringToParamMap() { - var paramStr = options.getWindowLocation().search.substring(1), - params = [], - paramMap = {}; - - if (paramStr.length > 0) { - params = paramStr.split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - var value = decodeURIComponent(p[1]); - if (value === 'true' || value === 'false') { - value = JSON.parse(value); - } - paramMap[decodeURIComponent(p[0])] = value; - } - } - - return paramMap; - } - - } - - return QueryString; -}; diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine.js b/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine.js deleted file mode 100644 index 7156e66..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/jasmine_lib/jasmine.js +++ /dev/null @@ -1,4941 +0,0 @@ -/* - Copyright (c) 2008-2017 Pivotal Labs - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -var getJasmineRequireObj = (function (jasmineGlobal) { - var jasmineRequire; - - if (typeof module !== 'undefined' && module.exports && typeof exports !== 'undefined') { - if (typeof global !== 'undefined') { - jasmineGlobal = global; - } else { - jasmineGlobal = {}; - } - jasmineRequire = exports; - } else { - if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') { - jasmineGlobal = window; - } - jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {}; - } - - function getJasmineRequire() { - return jasmineRequire; - } - - getJasmineRequire().core = function(jRequire) { - var j$ = {}; - - jRequire.base(j$, jasmineGlobal); - j$.util = jRequire.util(); - j$.errors = jRequire.errors(); - j$.formatErrorMsg = jRequire.formatErrorMsg(); - j$.Any = jRequire.Any(j$); - j$.Anything = jRequire.Anything(j$); - j$.CallTracker = jRequire.CallTracker(j$); - j$.MockDate = jRequire.MockDate(); - j$.getClearStack = jRequire.clearStack(j$); - j$.Clock = jRequire.Clock(); - j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(); - j$.Env = jRequire.Env(j$); - j$.ExceptionFormatter = jRequire.ExceptionFormatter(); - j$.Expectation = jRequire.Expectation(); - j$.buildExpectationResult = jRequire.buildExpectationResult(); - j$.JsApiReporter = jRequire.JsApiReporter(); - j$.matchersUtil = jRequire.matchersUtil(j$); - j$.ObjectContaining = jRequire.ObjectContaining(j$); - j$.ArrayContaining = jRequire.ArrayContaining(j$); - j$.pp = jRequire.pp(j$); - j$.QueueRunner = jRequire.QueueRunner(j$); - j$.ReportDispatcher = jRequire.ReportDispatcher(); - j$.Spec = jRequire.Spec(j$); - j$.Spy = jRequire.Spy(j$); - j$.SpyRegistry = jRequire.SpyRegistry(j$); - j$.SpyStrategy = jRequire.SpyStrategy(j$); - j$.StringMatching = jRequire.StringMatching(j$); - j$.Suite = jRequire.Suite(j$); - j$.Timer = jRequire.Timer(); - j$.TreeProcessor = jRequire.TreeProcessor(); - j$.version = jRequire.version(); - j$.Order = jRequire.Order(); - j$.DiffBuilder = jRequire.DiffBuilder(j$); - j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$); - j$.ObjectPath = jRequire.ObjectPath(j$); - j$.GlobalErrors = jRequire.GlobalErrors(j$); - - j$.matchers = jRequire.requireMatchers(jRequire, j$); - - return j$; - }; - - return getJasmineRequire; -})(this); - -getJasmineRequireObj().requireMatchers = function(jRequire, j$) { - var availableMatchers = [ - 'toBe', - 'toBeCloseTo', - 'toBeDefined', - 'toBeFalsy', - 'toBeGreaterThan', - 'toBeGreaterThanOrEqual', - 'toBeLessThan', - 'toBeLessThanOrEqual', - 'toBeNaN', - 'toBeNegativeInfinity', - 'toBeNull', - 'toBePositiveInfinity', - 'toBeTruthy', - 'toBeUndefined', - 'toContain', - 'toEqual', - 'toHaveBeenCalled', - 'toHaveBeenCalledBefore', - 'toHaveBeenCalledTimes', - 'toHaveBeenCalledWith', - 'toMatch', - 'toThrow', - 'toThrowError' - ], - matchers = {}; - - for (var i = 0; i < availableMatchers.length; i++) { - var name = availableMatchers[i]; - matchers[name] = jRequire[name](j$); - } - - return matchers; -}; - -getJasmineRequireObj().base = function(j$, jasmineGlobal) { - j$.unimplementedMethod_ = function() { - throw new Error('unimplemented method'); - }; - - /** - * Maximum object depth the pretty printer will print to. - * Set this to a lower value to speed up pretty printing if you have large objects. - * @name jasmine.MAX_PRETTY_PRINT_DEPTH - */ - j$.MAX_PRETTY_PRINT_DEPTH = 40; - /** - * Maximum number of array elements to display when pretty printing objects. - * Elements past this number will be ellipised. - * @name jasmine.MAX_PRETTY_PRINT_ARRAY_LENGTH - */ - j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 100; - /** - * Default number of milliseconds Jasmine will wait for an asynchronous spec to complete. - * @name jasmine.DEFAULT_TIMEOUT_INTERVAL - */ - j$.DEFAULT_TIMEOUT_INTERVAL = 5000; - - j$.getGlobal = function() { - return jasmineGlobal; - }; - - /** - * Get the currently booted Jasmine Environment. - * - * @name jasmine.getEnv - * @function - * @return {Env} - */ - j$.getEnv = function(options) { - var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options); - //jasmine. singletons in here (setTimeout blah blah). - return env; - }; - - j$.isArray_ = function(value) { - return j$.isA_('Array', value); - }; - - j$.isObject_ = function(value) { - return !j$.util.isUndefined(value) && value !== null && j$.isA_('Object', value); - }; - - j$.isString_ = function(value) { - return j$.isA_('String', value); - }; - - j$.isNumber_ = function(value) { - return j$.isA_('Number', value); - }; - - j$.isFunction_ = function(value) { - return j$.isA_('Function', value); - }; - - j$.isA_ = function(typeName, value) { - return j$.getType_(value) === '[object ' + typeName + ']'; - }; - - j$.getType_ = function(value) { - return Object.prototype.toString.apply(value); - }; - - j$.isDomNode = function(obj) { - return obj.nodeType > 0; - }; - - j$.fnNameFor = function(func) { - if (func.name) { - return func.name; - } - - var matches = func.toString().match(/^\s*function\s*(\w*)\s*\(/); - return matches ? matches[1] : '<anonymous>'; - }; - - /** - * Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), - * that will succeed if the actual value being compared is an instance of the specified class/constructor. - * @name jasmine.any - * @function - * @param {Constructor} clazz - The constructor to check against. - */ - j$.any = function(clazz) { - return new j$.Any(clazz); - }; - - /** - * Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), - * that will succeed if the actual value being compared is not `null` and not `undefined`. - * @name jasmine.anything - * @function - */ - j$.anything = function() { - return new j$.Anything(); - }; - - /** - * Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), - * that will succeed if the actual value being compared contains at least the keys and values. - * @name jasmine.objectContaining - * @function - * @param {Object} sample - The subset of properties that _must_ be in the actual. - */ - j$.objectContaining = function(sample) { - return new j$.ObjectContaining(sample); - }; - - /** - * Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), - * that will succeed if the actual value is a `String` that matches the `RegExp` or `String`. - * @name jasmine.stringMatching - * @function - * @param {RegExp|String} expected - */ - j$.stringMatching = function(expected) { - return new j$.StringMatching(expected); - }; - - /** - * Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), - * that will succeed if the actual value is an `Array` that contains at least the elements in the sample. - * @name jasmine.arrayContaining - * @function - * @param {Array} sample - */ - j$.arrayContaining = function(sample) { - return new j$.ArrayContaining(sample); - }; - - /** - * Create a bare {@link Spy} object. This won't be installed anywhere and will not have any implementation behind it. - * @name jasmine.createSpy - * @function - * @param {String} [name] - Name to give the spy. This will be displayed in failure messages. - * @param {Function} [originalFn] - Function to act as the real implementation. - * @return {Spy} - */ - j$.createSpy = function(name, originalFn) { - return j$.Spy(name, originalFn); - }; - - j$.isSpy = function(putativeSpy) { - if (!putativeSpy) { - return false; - } - return putativeSpy.and instanceof j$.SpyStrategy && - putativeSpy.calls instanceof j$.CallTracker; - }; - - /** - * Create an object with multiple {@link Spy}s as its members. - * @name jasmine.createSpyObj - * @function - * @param {String} [baseName] - Base name for the spies in the object. - * @param {String[]|Object} methodNames - Array of method names to create spies for, or Object whose keys will be method names and values the {@link Spy#and#returnValue|returnValue}. - * @return {Object} - */ - j$.createSpyObj = function(baseName, methodNames) { - var baseNameIsCollection = j$.isObject_(baseName) || j$.isArray_(baseName); - - if (baseNameIsCollection && j$.util.isUndefined(methodNames)) { - methodNames = baseName; - baseName = 'unknown'; - } - - var obj = {}; - var spiesWereSet = false; - - if (j$.isArray_(methodNames)) { - for (var i = 0; i < methodNames.length; i++) { - obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]); - spiesWereSet = true; - } - } else if (j$.isObject_(methodNames)) { - for (var key in methodNames) { - if (methodNames.hasOwnProperty(key)) { - obj[key] = j$.createSpy(baseName + '.' + key); - obj[key].and.returnValue(methodNames[key]); - spiesWereSet = true; - } - } - } - - if (!spiesWereSet) { - throw 'createSpyObj requires a non-empty array or object of method names to create spies for'; - } - - return obj; - }; -}; - -getJasmineRequireObj().util = function() { - - var util = {}; - - util.inherit = function(childClass, parentClass) { - var Subclass = function() { - }; - Subclass.prototype = parentClass.prototype; - childClass.prototype = new Subclass(); - }; - - util.htmlEscape = function(str) { - if (!str) { - return str; - } - return str.replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>'); - }; - - util.argsToArray = function(args) { - var arrayOfArgs = []; - for (var i = 0; i < args.length; i++) { - arrayOfArgs.push(args[i]); - } - return arrayOfArgs; - }; - - util.isUndefined = function(obj) { - return obj === void 0; - }; - - util.arrayContains = function(array, search) { - var i = array.length; - while (i--) { - if (array[i] === search) { - return true; - } - } - return false; - }; - - util.clone = function(obj) { - if (Object.prototype.toString.apply(obj) === '[object Array]') { - return obj.slice(); - } - - var cloned = {}; - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - cloned[prop] = obj[prop]; - } - } - - return cloned; - }; - - util.getPropertyDescriptor = function(obj, methodName) { - var descriptor, - proto = obj; - - do { - descriptor = Object.getOwnPropertyDescriptor(proto, methodName); - proto = Object.getPrototypeOf(proto); - } while (!descriptor && proto); - - return descriptor; - }; - - util.objectDifference = function(obj, toRemove) { - var diff = {}; - - for (var key in obj) { - if (util.has(obj, key) && !util.has(toRemove, key)) { - diff[key] = obj[key]; - } - } - - return diff; - }; - - util.has = function(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); - }; - - return util; -}; - -getJasmineRequireObj().Spec = function(j$) { - function Spec(attrs) { - this.expectationFactory = attrs.expectationFactory; - this.resultCallback = attrs.resultCallback || function() {}; - this.id = attrs.id; - this.description = attrs.description || ''; - this.queueableFn = attrs.queueableFn; - this.beforeAndAfterFns = attrs.beforeAndAfterFns || function() { return {befores: [], afters: []}; }; - this.userContext = attrs.userContext || function() { return {}; }; - this.onStart = attrs.onStart || function() {}; - this.getSpecName = attrs.getSpecName || function() { return ''; }; - this.expectationResultFactory = attrs.expectationResultFactory || function() { }; - this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; - this.catchingExceptions = attrs.catchingExceptions || function() { return true; }; - this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; - - if (!this.queueableFn.fn) { - this.pend(); - } - - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [], - passedExpectations: [], - pendingReason: '' - }; - } - - Spec.prototype.addExpectationResult = function(passed, data, isError) { - var expectationResult = this.expectationResultFactory(data); - if (passed) { - this.result.passedExpectations.push(expectationResult); - } else { - this.result.failedExpectations.push(expectationResult); - - if (this.throwOnExpectationFailure && !isError) { - throw new j$.errors.ExpectationFailed(); - } - } - }; - - Spec.prototype.expect = function(actual) { - return this.expectationFactory(actual, this); - }; - - Spec.prototype.execute = function(onComplete, enabled) { - var self = this; - - this.onStart(this); - - if (!this.isExecutable() || this.markedPending || enabled === false) { - complete(enabled); - return; - } - - var fns = this.beforeAndAfterFns(); - var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters); - - this.queueRunnerFactory({ - queueableFns: allFns, - onException: function() { self.onException.apply(self, arguments); }, - onComplete: complete, - userContext: this.userContext() - }); - - function complete(enabledAgain) { - self.result.status = self.status(enabledAgain); - self.resultCallback(self.result); - - if (onComplete) { - onComplete(); - } - } - }; - - Spec.prototype.onException = function onException(e) { - if (Spec.isPendingSpecException(e)) { - this.pend(extractCustomPendingMessage(e)); - return; - } - - if (e instanceof j$.errors.ExpectationFailed) { - return; - } - - this.addExpectationResult(false, { - matcherName: '', - passed: false, - expected: '', - actual: '', - error: e - }, true); - }; - - Spec.prototype.disable = function() { - this.disabled = true; - }; - - Spec.prototype.pend = function(message) { - this.markedPending = true; - if (message) { - this.result.pendingReason = message; - } - }; - - Spec.prototype.getResult = function() { - this.result.status = this.status(); - return this.result; - }; - - Spec.prototype.status = function(enabled) { - if (this.disabled || enabled === false) { - return 'disabled'; - } - - if (this.markedPending) { - return 'pending'; - } - - if (this.result.failedExpectations.length > 0) { - return 'failed'; - } else { - return 'passed'; - } - }; - - Spec.prototype.isExecutable = function() { - return !this.disabled; - }; - - Spec.prototype.getFullName = function() { - return this.getSpecName(this); - }; - - var extractCustomPendingMessage = function(e) { - var fullMessage = e.toString(), - boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage), - boilerplateEnd = boilerplateStart + Spec.pendingSpecExceptionMessage.length; - - return fullMessage.substr(boilerplateEnd); - }; - - Spec.pendingSpecExceptionMessage = '=> marked Pending'; - - Spec.isPendingSpecException = function(e) { - return !!(e && e.toString && e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1); - }; - - return Spec; -}; - -if (typeof window == void 0 && typeof exports == 'object') { - exports.Spec = jasmineRequire.Spec; -} - -/*jshint bitwise: false*/ - -getJasmineRequireObj().Order = function() { - function Order(options) { - this.random = 'random' in options ? options.random : true; - var seed = this.seed = options.seed || generateSeed(); - this.sort = this.random ? randomOrder : naturalOrder; - - function naturalOrder(items) { - return items; - } - - function randomOrder(items) { - var copy = items.slice(); - copy.sort(function(a, b) { - return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id); - }); - return copy; - } - - function generateSeed() { - return String(Math.random()).slice(-5); - } - - // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function - // used to get a different output when the key changes slighly. - // We use your return to sort the children randomly in a consistent way when - // used in conjunction with a seed - - function jenkinsHash(key) { - var hash, i; - for(hash = i = 0; i < key.length; ++i) { - hash += key.charCodeAt(i); - hash += (hash << 10); - hash ^= (hash >> 6); - } - hash += (hash << 3); - hash ^= (hash >> 11); - hash += (hash << 15); - return hash; - } - - } - - return Order; -}; - -getJasmineRequireObj().Env = function(j$) { - /** - * _Note:_ Do not construct this directly, Jasmine will make one during booting. - * @name Env - * @classdesc The Jasmine environment - * @constructor - */ - function Env(options) { - options = options || {}; - - var self = this; - var global = options.global || j$.getGlobal(); - - var totalSpecsDefined = 0; - - var catchExceptions = true; - - var realSetTimeout = j$.getGlobal().setTimeout; - var realClearTimeout = j$.getGlobal().clearTimeout; - var clearStack = j$.getClearStack(j$.getGlobal()); - this.clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); - - var runnableResources = {}; - - var currentSpec = null; - var currentlyExecutingSuites = []; - var currentDeclarationSuite = null; - var throwOnExpectationFailure = false; - var random = false; - var seed = null; - - var currentSuite = function() { - return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; - }; - - var currentRunnable = function() { - return currentSpec || currentSuite(); - }; - - var reporter = new j$.ReportDispatcher([ - 'jasmineStarted', - 'jasmineDone', - 'suiteStarted', - 'suiteDone', - 'specStarted', - 'specDone' - ]); - - var globalErrors = new j$.GlobalErrors(); - - this.specFilter = function() { - return true; - }; - - this.addCustomEqualityTester = function(tester) { - if(!currentRunnable()) { - throw new Error('Custom Equalities must be added in a before function or a spec'); - } - runnableResources[currentRunnable().id].customEqualityTesters.push(tester); - }; - - this.addMatchers = function(matchersToAdd) { - if(!currentRunnable()) { - throw new Error('Matchers must be added in a before function or a spec'); - } - var customMatchers = runnableResources[currentRunnable().id].customMatchers; - for (var matcherName in matchersToAdd) { - customMatchers[matcherName] = matchersToAdd[matcherName]; - } - }; - - j$.Expectation.addCoreMatchers(j$.matchers); - - var nextSpecId = 0; - var getNextSpecId = function() { - return 'spec' + nextSpecId++; - }; - - var nextSuiteId = 0; - var getNextSuiteId = function() { - return 'suite' + nextSuiteId++; - }; - - var expectationFactory = function(actual, spec) { - return j$.Expectation.Factory({ - util: j$.matchersUtil, - customEqualityTesters: runnableResources[spec.id].customEqualityTesters, - customMatchers: runnableResources[spec.id].customMatchers, - actual: actual, - addExpectationResult: addExpectationResult - }); - - function addExpectationResult(passed, result) { - return spec.addExpectationResult(passed, result); - } - }; - - var defaultResourcesForRunnable = function(id, parentRunnableId) { - var resources = {spies: [], customEqualityTesters: [], customMatchers: {}}; - - if(runnableResources[parentRunnableId]){ - resources.customEqualityTesters = j$.util.clone(runnableResources[parentRunnableId].customEqualityTesters); - resources.customMatchers = j$.util.clone(runnableResources[parentRunnableId].customMatchers); - } - - runnableResources[id] = resources; - }; - - var clearResourcesForRunnable = function(id) { - spyRegistry.clearSpies(); - delete runnableResources[id]; - }; - - var beforeAndAfterFns = function(suite) { - return function() { - var befores = [], - afters = []; - - while(suite) { - befores = befores.concat(suite.beforeFns); - afters = afters.concat(suite.afterFns); - - suite = suite.parentSuite; - } - - return { - befores: befores.reverse(), - afters: afters - }; - }; - }; - - var getSpecName = function(spec, suite) { - var fullName = [spec.description], - suiteFullName = suite.getFullName(); - - if (suiteFullName !== '') { - fullName.unshift(suiteFullName); - } - return fullName.join(' '); - }; - - // TODO: we may just be able to pass in the fn instead of wrapping here - var buildExpectationResult = j$.buildExpectationResult, - exceptionFormatter = new j$.ExceptionFormatter(), - expectationResultFactory = function(attrs) { - attrs.messageFormatter = exceptionFormatter.message; - attrs.stackFormatter = exceptionFormatter.stack; - - return buildExpectationResult(attrs); - }; - - // TODO: fix this naming, and here's where the value comes in - this.catchExceptions = function(value) { - catchExceptions = !!value; - return catchExceptions; - }; - - this.catchingExceptions = function() { - return catchExceptions; - }; - - var maximumSpecCallbackDepth = 20; - var currentSpecCallbackDepth = 0; - - var catchException = function(e) { - return j$.Spec.isPendingSpecException(e) || catchExceptions; - }; - - this.throwOnExpectationFailure = function(value) { - throwOnExpectationFailure = !!value; - }; - - this.throwingExpectationFailures = function() { - return throwOnExpectationFailure; - }; - - this.randomizeTests = function(value) { - random = !!value; - }; - - this.randomTests = function() { - return random; - }; - - this.seed = function(value) { - if (value) { - seed = value; - } - return seed; - }; - - var queueRunnerFactory = function(options) { - options.catchException = catchException; - options.clearStack = options.clearStack || clearStack; - options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; - options.fail = self.fail; - options.globalErrors = globalErrors; - - new j$.QueueRunner(options).execute(); - }; - - var topSuite = new j$.Suite({ - env: this, - id: getNextSuiteId(), - description: 'Jasmine__TopLevel__Suite', - expectationFactory: expectationFactory, - expectationResultFactory: expectationResultFactory - }); - defaultResourcesForRunnable(topSuite.id); - currentDeclarationSuite = topSuite; - - this.topSuite = function() { - return topSuite; - }; - - this.execute = function(runnablesToRun) { - if(!runnablesToRun) { - if (focusedRunnables.length) { - runnablesToRun = focusedRunnables; - } else { - runnablesToRun = [topSuite.id]; - } - } - - var order = new j$.Order({ - random: random, - seed: seed - }); - - var processor = new j$.TreeProcessor({ - tree: topSuite, - runnableIds: runnablesToRun, - queueRunnerFactory: queueRunnerFactory, - nodeStart: function(suite) { - currentlyExecutingSuites.push(suite); - defaultResourcesForRunnable(suite.id, suite.parentSuite.id); - reporter.suiteStarted(suite.result); - }, - nodeComplete: function(suite, result) { - if (!suite.markedPending) { - clearResourcesForRunnable(suite.id); - } - currentlyExecutingSuites.pop(); - reporter.suiteDone(result); - }, - orderChildren: function(node) { - return order.sort(node.children); - } - }); - - if(!processor.processTree().valid) { - throw new Error('Invalid order: would cause a beforeAll or afterAll to be run multiple times'); - } - - reporter.jasmineStarted({ - totalSpecsDefined: totalSpecsDefined - }); - - currentlyExecutingSuites.push(topSuite); - - globalErrors.install(); - processor.execute(function() { - clearResourcesForRunnable(topSuite.id); - currentlyExecutingSuites.pop(); - globalErrors.uninstall(); - - reporter.jasmineDone({ - order: order, - failedExpectations: topSuite.result.failedExpectations - }); - }); - }; - - /** - * Add a custom reporter to the Jasmine environment. - * @name Env#addReporter - * @function - * @see custom_reporter - */ - this.addReporter = function(reporterToAdd) { - reporter.addReporter(reporterToAdd); - }; - - this.provideFallbackReporter = function(reporterToAdd) { - reporter.provideFallbackReporter(reporterToAdd); - }; - - this.clearReporters = function() { - reporter.clearReporters(); - }; - - var spyRegistry = new j$.SpyRegistry({currentSpies: function() { - if(!currentRunnable()) { - throw new Error('Spies must be created in a before function or a spec'); - } - return runnableResources[currentRunnable().id].spies; - }}); - - this.allowRespy = function(allow){ - spyRegistry.allowRespy(allow); - }; - - this.spyOn = function() { - return spyRegistry.spyOn.apply(spyRegistry, arguments); - }; - - this.spyOnProperty = function() { - return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); - }; - - var ensureIsFunction = function(fn, caller) { - if (!j$.isFunction_(fn)) { - throw new Error(caller + ' expects a function argument; received ' + j$.getType_(fn)); - } - }; - - var suiteFactory = function(description) { - var suite = new j$.Suite({ - env: self, - id: getNextSuiteId(), - description: description, - parentSuite: currentDeclarationSuite, - expectationFactory: expectationFactory, - expectationResultFactory: expectationResultFactory, - throwOnExpectationFailure: throwOnExpectationFailure - }); - - return suite; - }; - - this.describe = function(description, specDefinitions) { - ensureIsFunction(specDefinitions, 'describe'); - var suite = suiteFactory(description); - if (specDefinitions.length > 0) { - throw new Error('describe does not expect any arguments'); - } - if (currentDeclarationSuite.markedPending) { - suite.pend(); - } - addSpecsToSuite(suite, specDefinitions); - return suite; - }; - - this.xdescribe = function(description, specDefinitions) { - ensureIsFunction(specDefinitions, 'xdescribe'); - var suite = suiteFactory(description); - suite.pend(); - addSpecsToSuite(suite, specDefinitions); - return suite; - }; - - var focusedRunnables = []; - - this.fdescribe = function(description, specDefinitions) { - ensureIsFunction(specDefinitions, 'fdescribe'); - var suite = suiteFactory(description); - suite.isFocused = true; - - focusedRunnables.push(suite.id); - unfocusAncestor(); - addSpecsToSuite(suite, specDefinitions); - - return suite; - }; - - function addSpecsToSuite(suite, specDefinitions) { - var parentSuite = currentDeclarationSuite; - parentSuite.addChild(suite); - currentDeclarationSuite = suite; - - var declarationError = null; - try { - specDefinitions.call(suite); - } catch (e) { - declarationError = e; - } - - if (declarationError) { - self.it('encountered a declaration exception', function() { - throw declarationError; - }); - } - - currentDeclarationSuite = parentSuite; - } - - function findFocusedAncestor(suite) { - while (suite) { - if (suite.isFocused) { - return suite.id; - } - suite = suite.parentSuite; - } - - return null; - } - - function unfocusAncestor() { - var focusedAncestor = findFocusedAncestor(currentDeclarationSuite); - if (focusedAncestor) { - for (var i = 0; i < focusedRunnables.length; i++) { - if (focusedRunnables[i] === focusedAncestor) { - focusedRunnables.splice(i, 1); - break; - } - } - } - } - - var specFactory = function(description, fn, suite, timeout) { - totalSpecsDefined++; - var spec = new j$.Spec({ - id: getNextSpecId(), - beforeAndAfterFns: beforeAndAfterFns(suite), - expectationFactory: expectationFactory, - resultCallback: specResultCallback, - getSpecName: function(spec) { - return getSpecName(spec, suite); - }, - onStart: specStarted, - description: description, - expectationResultFactory: expectationResultFactory, - queueRunnerFactory: queueRunnerFactory, - userContext: function() { return suite.clonedSharedUserContext(); }, - queueableFn: { - fn: fn, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - }, - throwOnExpectationFailure: throwOnExpectationFailure - }); - - if (!self.specFilter(spec)) { - spec.disable(); - } - - return spec; - - function specResultCallback(result) { - clearResourcesForRunnable(spec.id); - currentSpec = null; - reporter.specDone(result); - } - - function specStarted(spec) { - currentSpec = spec; - defaultResourcesForRunnable(spec.id, suite.id); - reporter.specStarted(spec.result); - } - }; - - this.it = function(description, fn, timeout) { - // it() sometimes doesn't have a fn argument, so only check the type if - // it's given. - if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'it'); - } - var spec = specFactory(description, fn, currentDeclarationSuite, timeout); - if (currentDeclarationSuite.markedPending) { - spec.pend(); - } - currentDeclarationSuite.addChild(spec); - return spec; - }; - - this.xit = function(description, fn, timeout) { - // xit(), like it(), doesn't always have a fn argument, so only check the - // type when needed. - if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'xit'); - } - var spec = this.it.apply(this, arguments); - spec.pend('Temporarily disabled with xit'); - return spec; - }; - - this.fit = function(description, fn, timeout){ - ensureIsFunction(fn, 'fit'); - var spec = specFactory(description, fn, currentDeclarationSuite, timeout); - currentDeclarationSuite.addChild(spec); - focusedRunnables.push(spec.id); - unfocusAncestor(); - return spec; - }; - - this.expect = function(actual) { - if (!currentRunnable()) { - throw new Error('\'expect\' was used when there was no current spec, this could be because an asynchronous test timed out'); - } - - return currentRunnable().expect(actual); - }; - - this.beforeEach = function(beforeEachFunction, timeout) { - ensureIsFunction(beforeEachFunction, 'beforeEach'); - currentDeclarationSuite.beforeEach({ - fn: beforeEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - }); - }; - - this.beforeAll = function(beforeAllFunction, timeout) { - ensureIsFunction(beforeAllFunction, 'beforeAll'); - currentDeclarationSuite.beforeAll({ - fn: beforeAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - }); - }; - - this.afterEach = function(afterEachFunction, timeout) { - ensureIsFunction(afterEachFunction, 'afterEach'); - currentDeclarationSuite.afterEach({ - fn: afterEachFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - }); - }; - - this.afterAll = function(afterAllFunction, timeout) { - ensureIsFunction(afterAllFunction, 'afterAll'); - currentDeclarationSuite.afterAll({ - fn: afterAllFunction, - timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - }); - }; - - this.pending = function(message) { - var fullMessage = j$.Spec.pendingSpecExceptionMessage; - if(message) { - fullMessage += message; - } - throw fullMessage; - }; - - this.fail = function(error) { - if (!currentRunnable()) { - throw new Error('\'fail\' was used when there was no current spec, this could be because an asynchronous test timed out'); - } - - var message = 'Failed'; - if (error) { - message += ': '; - if (error.message) { - message += error.message; - } else if (jasmine.isString_(error)) { - message += error; - } else { - // pretty print all kind of objects. This includes arrays. - message += jasmine.pp(error); - } - } - - currentRunnable().addExpectationResult(false, { - matcherName: '', - passed: false, - expected: '', - actual: '', - message: message, - error: error && error.message ? error : null - }); - }; - } - - return Env; -}; - -getJasmineRequireObj().JsApiReporter = function() { - - var noopTimer = { - start: function(){}, - elapsed: function(){ return 0; } - }; - - /** - * _Note:_ Do not construct this directly, use the global `jsApiReporter` to retrieve the instantiated object. - * - * @name jsApiReporter - * @classdesc Reporter added by default in `boot.js` to record results for retrieval in javascript code. - * @class - */ - function JsApiReporter(options) { - var timer = options.timer || noopTimer, - status = 'loaded'; - - this.started = false; - this.finished = false; - this.runDetails = {}; - - this.jasmineStarted = function() { - this.started = true; - status = 'started'; - timer.start(); - }; - - var executionTime; - - this.jasmineDone = function(runDetails) { - this.finished = true; - this.runDetails = runDetails; - executionTime = timer.elapsed(); - status = 'done'; - }; - - /** - * Get the current status for the Jasmine environment. - * @name jsApiReporter#status - * @function - * @return {String} - One of `loaded`, `started`, or `done` - */ - this.status = function() { - return status; - }; - - var suites = [], - suites_hash = {}; - - this.suiteStarted = function(result) { - suites_hash[result.id] = result; - }; - - this.suiteDone = function(result) { - storeSuite(result); - }; - - /** - * Get the results for a set of suites. - * - * Retrievable in slices for easier serialization. - * @name jsApiReporter#suiteResults - * @function - * @param {Number} index - The position in the suites list to start from. - * @param {Number} length - Maximum number of suite results to return. - * @return {Object[]} - */ - this.suiteResults = function(index, length) { - return suites.slice(index, index + length); - }; - - function storeSuite(result) { - suites.push(result); - suites_hash[result.id] = result; - } - - /** - * Get all of the suites in a single object, with their `id` as the key. - * @name jsApiReporter#suites - * @function - * @return {Object} - */ - this.suites = function() { - return suites_hash; - }; - - var specs = []; - - this.specDone = function(result) { - specs.push(result); - }; - - /** - * Get the results for a set of specs. - * - * Retrievable in slices for easier serialization. - * @name jsApiReporter#specResults - * @function - * @param {Number} index - The position in the specs list to start from. - * @param {Number} length - Maximum number of specs results to return. - * @return {Object[]} - */ - this.specResults = function(index, length) { - return specs.slice(index, index + length); - }; - - /** - * Get all spec results. - * @name jsApiReporter#specs - * @function - * @return {Object[]} - */ - this.specs = function() { - return specs; - }; - - /** - * Get the number of milliseconds it took for the full Jasmine suite to run. - * @name jsApiReporter#executionTime - * @function - * @return {Number} - */ - this.executionTime = function() { - return executionTime; - }; - - } - - return JsApiReporter; -}; - -getJasmineRequireObj().Any = function(j$) { - - function Any(expectedObject) { - if (typeof expectedObject === 'undefined') { - throw new TypeError( - 'jasmine.any() expects to be passed a constructor function. ' + - 'Please pass one or use jasmine.anything() to match any object.' - ); - } - this.expectedObject = expectedObject; - } - - Any.prototype.asymmetricMatch = function(other) { - if (this.expectedObject == String) { - return typeof other == 'string' || other instanceof String; - } - - if (this.expectedObject == Number) { - return typeof other == 'number' || other instanceof Number; - } - - if (this.expectedObject == Function) { - return typeof other == 'function' || other instanceof Function; - } - - if (this.expectedObject == Object) { - return typeof other == 'object'; - } - - if (this.expectedObject == Boolean) { - return typeof other == 'boolean'; - } - - return other instanceof this.expectedObject; - }; - - Any.prototype.jasmineToString = function() { - return '<jasmine.any(' + j$.fnNameFor(this.expectedObject) + ')>'; - }; - - return Any; -}; - -getJasmineRequireObj().Anything = function(j$) { - - function Anything() {} - - Anything.prototype.asymmetricMatch = function(other) { - return !j$.util.isUndefined(other) && other !== null; - }; - - Anything.prototype.jasmineToString = function() { - return '<jasmine.anything>'; - }; - - return Anything; -}; - -getJasmineRequireObj().ArrayContaining = function(j$) { - function ArrayContaining(sample) { - this.sample = sample; - } - - ArrayContaining.prototype.asymmetricMatch = function(other, customTesters) { - var className = Object.prototype.toString.call(this.sample); - if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not \'' + this.sample + '\'.'); } - - for (var i = 0; i < this.sample.length; i++) { - var item = this.sample[i]; - if (!j$.matchersUtil.contains(other, item, customTesters)) { - return false; - } - } - - return true; - }; - - ArrayContaining.prototype.jasmineToString = function () { - return '<jasmine.arrayContaining(' + jasmine.pp(this.sample) +')>'; - }; - - return ArrayContaining; -}; - -getJasmineRequireObj().ObjectContaining = function(j$) { - - function ObjectContaining(sample) { - this.sample = sample; - } - - function getPrototype(obj) { - if (Object.getPrototypeOf) { - return Object.getPrototypeOf(obj); - } - - if (obj.constructor.prototype == obj) { - return null; - } - - return obj.constructor.prototype; - } - - function hasProperty(obj, property) { - if (!obj) { - return false; - } - - if (Object.prototype.hasOwnProperty.call(obj, property)) { - return true; - } - - return hasProperty(getPrototype(obj), property); - } - - ObjectContaining.prototype.asymmetricMatch = function(other, customTesters) { - if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } - - for (var property in this.sample) { - if (!hasProperty(other, property) || - !j$.matchersUtil.equals(this.sample[property], other[property], customTesters)) { - return false; - } - } - - return true; - }; - - ObjectContaining.prototype.jasmineToString = function() { - return '<jasmine.objectContaining(' + j$.pp(this.sample) + ')>'; - }; - - return ObjectContaining; -}; - -getJasmineRequireObj().StringMatching = function(j$) { - - function StringMatching(expected) { - if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { - throw new Error('Expected is not a String or a RegExp'); - } - - this.regexp = new RegExp(expected); - } - - StringMatching.prototype.asymmetricMatch = function(other) { - return this.regexp.test(other); - }; - - StringMatching.prototype.jasmineToString = function() { - return '<jasmine.stringMatching(' + this.regexp + ')>'; - }; - - return StringMatching; -}; - -getJasmineRequireObj().CallTracker = function(j$) { - - /** - * @namespace Spy#calls - */ - function CallTracker() { - var calls = []; - var opts = {}; - - function argCloner(context) { - var clonedArgs = []; - var argsAsArray = j$.util.argsToArray(context.args); - for(var i = 0; i < argsAsArray.length; i++) { - if(Object.prototype.toString.apply(argsAsArray[i]).match(/^\[object/)) { - clonedArgs.push(j$.util.clone(argsAsArray[i])); - } else { - clonedArgs.push(argsAsArray[i]); - } - } - context.args = clonedArgs; - } - - this.track = function(context) { - if(opts.cloneArgs) { - argCloner(context); - } - calls.push(context); - }; - - /** - * Check whether this spy has been invoked. - * @name Spy#calls#any - * @function - * @return {Boolean} - */ - this.any = function() { - return !!calls.length; - }; - - /** - * Get the number of invocations of this spy. - * @name Spy#calls#count - * @function - * @return {Integer} - */ - this.count = function() { - return calls.length; - }; - - /** - * Get the arguments that were passed to a specific invocation of this spy. - * @name Spy#calls#argsFor - * @function - * @param {Integer} index The 0-based invocation index. - * @return {Array} - */ - this.argsFor = function(index) { - var call = calls[index]; - return call ? call.args : []; - }; - - /** - * Get the raw calls array for this spy. - * @name Spy#calls#all - * @function - * @return {Spy.callData[]} - */ - this.all = function() { - return calls; - }; - - /** - * Get all of the arguments for each invocation of this spy in the order they were received. - * @name Spy#calls#allArgs - * @function - * @return {Array} - */ - this.allArgs = function() { - var callArgs = []; - for(var i = 0; i < calls.length; i++){ - callArgs.push(calls[i].args); - } - - return callArgs; - }; - - /** - * Get the first invocation of this spy. - * @name Spy#calls#first - * @function - * @return {ObjecSpy.callData} - */ - this.first = function() { - return calls[0]; - }; - - /** - * Get the most recent invocation of this spy. - * @name Spy#calls#mostRecent - * @function - * @return {ObjecSpy.callData} - */ - this.mostRecent = function() { - return calls[calls.length - 1]; - }; - - /** - * Reset this spy as if it has never been called. - * @name Spy#calls#reset - * @function - */ - this.reset = function() { - calls = []; - }; - - /** - * Set this spy to do a shallow clone of arguments passed to each invocation. - * @name Spy#calls#saveArgumentsByValue - * @function - */ - this.saveArgumentsByValue = function() { - opts.cloneArgs = true; - }; - - } - - return CallTracker; -}; - -getJasmineRequireObj().clearStack = function(j$) { - function messageChannelImpl(global) { - var channel = new global.MessageChannel(), - head = {}, - tail = head; - - channel.port1.onmessage = function() { - head = head.next; - var task = head.task; - delete head.task; - task(); - }; - - return function clearStack(fn) { - tail = tail.next = { task: fn }; - channel.port2.postMessage(0); - }; - } - - function getClearStack(global) { - if (j$.isFunction_(global.setImmediate)) { - var realSetImmediate = global.setImmediate; - return function(fn) { - realSetImmediate(fn); - }; - } else if (!j$.util.isUndefined(global.MessageChannel)) { - return messageChannelImpl(global); - } else { - var realSetTimeout = global.setTimeout; - return function clearStack(fn) { - Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]); - }; - } - } - - return getClearStack; -}; - -getJasmineRequireObj().Clock = function() { - /** - * _Note:_ Do not construct this directly, Jasmine will make one during booting. You can get the current clock with {@link jasmine.clock}. - * @class Clock - * @classdesc Jasmine's mock clock is used when testing time dependent code. - */ - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - var self = this, - realTimingFunctions = { - setTimeout: global.setTimeout, - clearTimeout: global.clearTimeout, - setInterval: global.setInterval, - clearInterval: global.clearInterval - }, - fakeTimingFunctions = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval - }, - installed = false, - delayedFunctionScheduler, - timer; - - - /** - * Install the mock clock over the built-in methods. - * @name Clock#install - * @function - * @return {Clock} - */ - self.install = function() { - if(!originalTimingFunctionsIntact()) { - throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); - } - replace(global, fakeTimingFunctions); - timer = fakeTimingFunctions; - delayedFunctionScheduler = delayedFunctionSchedulerFactory(); - installed = true; - - return self; - }; - - /** - * Uninstall the mock clock, returning the built-in methods to their places. - * @name Clock#uninstall - * @function - */ - self.uninstall = function() { - delayedFunctionScheduler = null; - mockDate.uninstall(); - replace(global, realTimingFunctions); - - timer = realTimingFunctions; - installed = false; - }; - - /** - * Execute a function with a mocked Clock - * - * The clock will be {@link Clock#install|install}ed before the function is called and {@link Clock#uninstall|uninstall}ed in a `finally` after the function completes. - * @name Clock#withMock - * @function - * @param {closure} Function The function to be called. - */ - self.withMock = function(closure) { - this.install(); - try { - closure(); - } finally { - this.uninstall(); - } - }; - - /** - * Instruct the installed Clock to also mock the date returned by `new Date()` - * @name Clock#mockDate - * @function - * @param {Date} [initialDate=now] The `Date` to provide. - */ - self.mockDate = function(initialDate) { - mockDate.install(initialDate); - }; - - self.setTimeout = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); - } - return timer.setTimeout(fn, delay); - } - return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); - }; - - self.setInterval = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); - } - return timer.setInterval(fn, delay); - } - return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); - }; - - self.clearTimeout = function(id) { - return Function.prototype.call.apply(timer.clearTimeout, [global, id]); - }; - - self.clearInterval = function(id) { - return Function.prototype.call.apply(timer.clearInterval, [global, id]); - }; - - /** - * Tick the Clock forward, running any enqueued timeouts along the way - * @name Clock#tick - * @function - * @param {int} millis The number of milliseconds to tick. - */ - self.tick = function(millis) { - if (installed) { - delayedFunctionScheduler.tick(millis, function(millis) { mockDate.tick(millis); }); - } else { - throw new Error('Mock clock is not installed, use jasmine.clock().install()'); - } - }; - - return self; - - function originalTimingFunctionsIntact() { - return global.setTimeout === realTimingFunctions.setTimeout && - global.clearTimeout === realTimingFunctions.clearTimeout && - global.setInterval === realTimingFunctions.setInterval && - global.clearInterval === realTimingFunctions.clearInterval; - } - - function legacyIE() { - //if these methods are polyfilled, apply will be present - return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; - } - - function replace(dest, source) { - for (var prop in source) { - dest[prop] = source[prop]; - } - } - - function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); - } - - function clearTimeout(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); - } - - function clearInterval(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function argSlice(argsObj, n) { - return Array.prototype.slice.call(argsObj, n); - } - } - - return Clock; -}; - -getJasmineRequireObj().DelayedFunctionScheduler = function() { - function DelayedFunctionScheduler() { - var self = this; - var scheduledLookup = []; - var scheduledFunctions = {}; - var currentTime = 0; - var delayedFnCount = 0; - - self.tick = function(millis, tickDate) { - millis = millis || 0; - var endTime = currentTime + millis; - - runScheduledFunctions(endTime, tickDate); - currentTime = endTime; - }; - - self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { - var f; - if (typeof(funcToCall) === 'string') { - /* jshint evil: true */ - f = function() { return eval(funcToCall); }; - /* jshint evil: false */ - } else { - f = funcToCall; - } - - millis = millis || 0; - timeoutKey = timeoutKey || ++delayedFnCount; - runAtMillis = runAtMillis || (currentTime + millis); - - var funcToSchedule = { - runAtMillis: runAtMillis, - funcToCall: f, - recurring: recurring, - params: params, - timeoutKey: timeoutKey, - millis: millis - }; - - if (runAtMillis in scheduledFunctions) { - scheduledFunctions[runAtMillis].push(funcToSchedule); - } else { - scheduledFunctions[runAtMillis] = [funcToSchedule]; - scheduledLookup.push(runAtMillis); - scheduledLookup.sort(function (a, b) { - return a - b; - }); - } - - return timeoutKey; - }; - - self.removeFunctionWithId = function(timeoutKey) { - for (var runAtMillis in scheduledFunctions) { - var funcs = scheduledFunctions[runAtMillis]; - var i = indexOfFirstToPass(funcs, function (func) { - return func.timeoutKey === timeoutKey; - }); - - if (i > -1) { - if (funcs.length === 1) { - delete scheduledFunctions[runAtMillis]; - deleteFromLookup(runAtMillis); - } else { - funcs.splice(i, 1); - } - - // intervals get rescheduled when executed, so there's never more - // than a single scheduled function with a given timeoutKey - break; - } - } - }; - - return self; - - function indexOfFirstToPass(array, testFn) { - var index = -1; - - for (var i = 0; i < array.length; ++i) { - if (testFn(array[i])) { - index = i; - break; - } - } - - return index; - } - - function deleteFromLookup(key) { - var value = Number(key); - var i = indexOfFirstToPass(scheduledLookup, function (millis) { - return millis === value; - }); - - if (i > -1) { - scheduledLookup.splice(i, 1); - } - } - - function reschedule(scheduledFn) { - self.scheduleFunction(scheduledFn.funcToCall, - scheduledFn.millis, - scheduledFn.params, - true, - scheduledFn.timeoutKey, - scheduledFn.runAtMillis + scheduledFn.millis); - } - - function forEachFunction(funcsToRun, callback) { - for (var i = 0; i < funcsToRun.length; ++i) { - callback(funcsToRun[i]); - } - } - - function runScheduledFunctions(endTime, tickDate) { - tickDate = tickDate || function() {}; - if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { - tickDate(endTime - currentTime); - return; - } - - do { - var newCurrentTime = scheduledLookup.shift(); - tickDate(newCurrentTime - currentTime); - - currentTime = newCurrentTime; - - var funcsToRun = scheduledFunctions[currentTime]; - delete scheduledFunctions[currentTime]; - - forEachFunction(funcsToRun, function(funcToRun) { - if (funcToRun.recurring) { - reschedule(funcToRun); - } - }); - - forEachFunction(funcsToRun, function(funcToRun) { - funcToRun.funcToCall.apply(null, funcToRun.params || []); - }); - } while (scheduledLookup.length > 0 && - // checking first if we're out of time prevents setTimeout(0) - // scheduled in a funcToRun from forcing an extra iteration - currentTime !== endTime && - scheduledLookup[0] <= endTime); - - // ran out of functions to call, but still time left on the clock - if (currentTime !== endTime) { - tickDate(endTime - currentTime); - } - } - } - - return DelayedFunctionScheduler; -}; - -getJasmineRequireObj().errors = function() { - function ExpectationFailed() {} - - ExpectationFailed.prototype = new Error(); - ExpectationFailed.prototype.constructor = ExpectationFailed; - - return { - ExpectationFailed: ExpectationFailed - }; -}; -getJasmineRequireObj().ExceptionFormatter = function() { - function ExceptionFormatter() { - this.message = function(error) { - var message = ''; - - if (error.name && error.message) { - message += error.name + ': ' + error.message; - } else { - message += error.toString() + ' thrown'; - } - - if (error.fileName || error.sourceURL) { - message += ' in ' + (error.fileName || error.sourceURL); - } - - if (error.line || error.lineNumber) { - message += ' (line ' + (error.line || error.lineNumber) + ')'; - } - - return message; - }; - - this.stack = function(error) { - return error ? error.stack : null; - }; - } - - return ExceptionFormatter; -}; - -getJasmineRequireObj().Expectation = function() { - - /** - * Matchers that come with Jasmine out of the box. - * @namespace matchers - */ - function Expectation(options) { - this.util = options.util || { buildFailureMessage: function() {} }; - this.customEqualityTesters = options.customEqualityTesters || []; - this.actual = options.actual; - this.addExpectationResult = options.addExpectationResult || function(){}; - this.isNot = options.isNot; - - var customMatchers = options.customMatchers || {}; - for (var matcherName in customMatchers) { - this[matcherName] = Expectation.prototype.wrapCompare(matcherName, customMatchers[matcherName]); - } - } - - Expectation.prototype.wrapCompare = function(name, matcherFactory) { - return function() { - var args = Array.prototype.slice.call(arguments, 0), - expected = args.slice(0), - message = ''; - - args.unshift(this.actual); - - var matcher = matcherFactory(this.util, this.customEqualityTesters), - matcherCompare = matcher.compare; - - function defaultNegativeCompare() { - var result = matcher.compare.apply(null, args); - result.pass = !result.pass; - return result; - } - - if (this.isNot) { - matcherCompare = matcher.negativeCompare || defaultNegativeCompare; - } - - var result = matcherCompare.apply(null, args); - - if (!result.pass) { - if (!result.message) { - args.unshift(this.isNot); - args.unshift(name); - message = this.util.buildFailureMessage.apply(null, args); - } else { - if (Object.prototype.toString.apply(result.message) === '[object Function]') { - message = result.message(); - } else { - message = result.message; - } - } - } - - if (expected.length == 1) { - expected = expected[0]; - } - - // TODO: how many of these params are needed? - this.addExpectationResult( - result.pass, - { - matcherName: name, - passed: result.pass, - message: message, - error: result.error, - actual: this.actual, - expected: expected // TODO: this may need to be arrayified/sliced - } - ); - }; - }; - - Expectation.addCoreMatchers = function(matchers) { - var prototype = Expectation.prototype; - for (var matcherName in matchers) { - var matcher = matchers[matcherName]; - prototype[matcherName] = prototype.wrapCompare(matcherName, matcher); - } - }; - - Expectation.Factory = function(options) { - options = options || {}; - - var expect = new Expectation(options); - - // TODO: this would be nice as its own Object - NegativeExpectation - // TODO: copy instead of mutate options - options.isNot = true; - expect.not = new Expectation(options); - - return expect; - }; - - return Expectation; -}; - -//TODO: expectation result may make more sense as a presentation of an expectation. -getJasmineRequireObj().buildExpectationResult = function() { - function buildExpectationResult(options) { - var messageFormatter = options.messageFormatter || function() {}, - stackFormatter = options.stackFormatter || function() {}; - - var result = { - matcherName: options.matcherName, - message: message(), - stack: stack(), - passed: options.passed - }; - - if(!result.passed) { - result.expected = options.expected; - result.actual = options.actual; - } - - return result; - - function message() { - if (options.passed) { - return 'Passed.'; - } else if (options.message) { - return options.message; - } else if (options.error) { - return messageFormatter(options.error); - } - return ''; - } - - function stack() { - if (options.passed) { - return ''; - } - - var error = options.error; - if (!error) { - try { - throw new Error(message()); - } catch (e) { - error = e; - } - } - return stackFormatter(error); - } - } - - return buildExpectationResult; -}; - -getJasmineRequireObj().formatErrorMsg = function() { - function generateErrorMsg(domain, usage) { - var usageDefinition = usage ? '\nUsage: ' + usage : ''; - - return function errorMsg(msg) { - return domain + ' : ' + msg + usageDefinition; - }; - } - - return generateErrorMsg; -}; - -getJasmineRequireObj().GlobalErrors = function(j$) { - function GlobalErrors(global) { - var handlers = []; - global = global || j$.getGlobal(); - - var onerror = function onerror() { - var handler = handlers[handlers.length - 1]; - handler.apply(null, Array.prototype.slice.call(arguments, 0)); - }; - - this.uninstall = function noop() {}; - - this.install = function install() { - if (global.process && global.process.listeners && j$.isFunction_(global.process.on)) { - var originalHandlers = global.process.listeners('uncaughtException'); - global.process.removeAllListeners('uncaughtException'); - global.process.on('uncaughtException', onerror); - - this.uninstall = function uninstall() { - global.process.removeListener('uncaughtException', onerror); - for (var i = 0; i < originalHandlers.length; i++) { - global.process.on('uncaughtException', originalHandlers[i]); - } - }; - } else { - var originalHandler = global.onerror; - global.onerror = onerror; - - this.uninstall = function uninstall() { - global.onerror = originalHandler; - }; - } - }; - - this.pushListener = function pushListener(listener) { - handlers.push(listener); - }; - - this.popListener = function popListener() { - handlers.pop(); - }; - } - - return GlobalErrors; -}; - -getJasmineRequireObj().DiffBuilder = function(j$) { - return function DiffBuilder() { - var path = new j$.ObjectPath(), - mismatches = []; - - return { - record: function (actual, expected, formatter) { - formatter = formatter || defaultFormatter; - mismatches.push(formatter(actual, expected, path)); - }, - - getMessage: function () { - return mismatches.join('\n'); - }, - - withPath: function (pathComponent, block) { - var oldPath = path; - path = path.add(pathComponent); - block(); - path = oldPath; - } - }; - - function defaultFormatter (actual, expected, path) { - return 'Expected ' + - path + (path.depth() ? ' = ' : '') + - j$.pp(actual) + - ' to equal ' + - j$.pp(expected) + - '.'; - } - }; -}; - -getJasmineRequireObj().matchersUtil = function(j$) { - // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? - - return { - equals: equals, - - contains: function(haystack, needle, customTesters) { - customTesters = customTesters || []; - - if ((Object.prototype.toString.apply(haystack) === '[object Set]')) { - return haystack.has(needle); - } - - if ((Object.prototype.toString.apply(haystack) === '[object Array]') || - (!!haystack && !haystack.indexOf)) - { - for (var i = 0; i < haystack.length; i++) { - if (equals(haystack[i], needle, customTesters)) { - return true; - } - } - return false; - } - - return !!haystack && haystack.indexOf(needle) >= 0; - }, - - buildFailureMessage: function() { - var args = Array.prototype.slice.call(arguments, 0), - matcherName = args[0], - isNot = args[1], - actual = args[2], - expected = args.slice(3), - englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); - - var message = 'Expected ' + - j$.pp(actual) + - (isNot ? ' not ' : ' ') + - englishyPredicate; - - if (expected.length > 0) { - for (var i = 0; i < expected.length; i++) { - if (i > 0) { - message += ','; - } - message += ' ' + j$.pp(expected[i]); - } - } - - return message + '.'; - } - }; - - function isAsymmetric(obj) { - return obj && j$.isA_('Function', obj.asymmetricMatch); - } - - function asymmetricMatch(a, b, customTesters, diffBuilder) { - var asymmetricA = isAsymmetric(a), - asymmetricB = isAsymmetric(b), - result; - - if (asymmetricA && asymmetricB) { - return undefined; - } - - if (asymmetricA) { - result = a.asymmetricMatch(b, customTesters); - diffBuilder.record(a, b); - return result; - } - - if (asymmetricB) { - result = b.asymmetricMatch(a, customTesters); - diffBuilder.record(a, b); - return result; - } - } - - function equals(a, b, customTesters, diffBuilder) { - customTesters = customTesters || []; - diffBuilder = diffBuilder || j$.NullDiffBuilder(); - - return eq(a, b, [], [], customTesters, diffBuilder); - } - - // Equality function lovingly adapted from isEqual in - // [Underscore](http://underscorejs.org) - function eq(a, b, aStack, bStack, customTesters, diffBuilder) { - var result = true, i; - - var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder); - if (!j$.util.isUndefined(asymmetricResult)) { - return asymmetricResult; - } - - for (i = 0; i < customTesters.length; i++) { - var customTesterResult = customTesters[i](a, b); - if (!j$.util.isUndefined(customTesterResult)) { - if (!customTesterResult) { - diffBuilder.record(a, b); - } - return customTesterResult; - } - } - - if (a instanceof Error && b instanceof Error) { - result = a.message == b.message; - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) { - result = a !== 0 || 1 / a == 1 / b; - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - // A strict comparison is necessary because `null == undefined`. - if (a === null || b === null) { - result = a === b; - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - var className = Object.prototype.toString.call(a); - if (className != Object.prototype.toString.call(b)) { - diffBuilder.record(a, b); - return false; - } - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - result = a == String(b); - if (!result) { - diffBuilder.record(a, b); - } - return result; - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); - if (!result) { - diffBuilder.record(a, b); - } - return result; - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - result = +a == +b; - if (!result) { - diffBuilder.record(a, b); - } - return result; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') { - diffBuilder.record(a, b); - return false; - } - - var aIsDomNode = j$.isDomNode(a); - var bIsDomNode = j$.isDomNode(b); - if (aIsDomNode && bIsDomNode) { - // At first try to use DOM3 method isEqualNode - if (a.isEqualNode) { - result = a.isEqualNode(b); - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - // IE8 doesn't support isEqualNode, try to use outerHTML && innerText - var aIsElement = a instanceof Element; - var bIsElement = b instanceof Element; - if (aIsElement && bIsElement) { - result = a.outerHTML == b.outerHTML; - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - if (aIsElement || bIsElement) { - diffBuilder.record(a, b); - return false; - } - result = a.innerText == b.innerText && a.textContent == b.textContent; - if (!result) { - diffBuilder.record(a, b); - } - return result; - } - if (aIsDomNode || bIsDomNode) { - diffBuilder.record(a, b); - return false; - } - - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) { return bStack[length] == b; } - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0; - // Recursively compare objects and arrays. - // Compare array lengths to determine if a deep comparison is necessary. - if (className == '[object Array]') { - size = a.length; - if (size !== b.length) { - diffBuilder.record(a, b); - return false; - } - - for (i = 0; i < size; i++) { - diffBuilder.withPath(i, function() { - result = eq(a[i], b[i], aStack, bStack, customTesters, diffBuilder) && result; - }); - } - if (!result) { - return false; - } - } else if (className == '[object Set]') { - if (a.size != b.size) { - diffBuilder.record(a, b); - return false; - } - var iterA = a.values(), iterB = b.values(); - var valA, valB; - do { - valA = iterA.next(); - valB = iterB.next(); - if (!eq(valA.value, valB.value, aStack, bStack, customTesters, j$.NullDiffBuilder())) { - diffBuilder.record(a, b); - return false; - } - } while (!valA.done && !valB.done); - } else { - - // Objects with different constructors are not equivalent, but `Object`s - // or `Array`s from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && - isFunction(aCtor) && isFunction(bCtor) && - a instanceof aCtor && b instanceof bCtor && - !(aCtor instanceof aCtor && bCtor instanceof bCtor)) { - - diffBuilder.record(a, b, constructorsAreDifferentFormatter); - return false; - } - } - - // Deep compare objects. - var aKeys = keys(a, className == '[object Array]'), key; - size = aKeys.length; - - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b, className == '[object Array]').length !== size) { - diffBuilder.record(a, b, objectKeysAreDifferentFormatter); - return false; - } - - for (i = 0; i < size; i++) { - key = aKeys[i]; - // Deep compare each member - if (!j$.util.has(b, key)) { - diffBuilder.record(a, b, objectKeysAreDifferentFormatter); - result = false; - continue; - } - - diffBuilder.withPath(key, function() { - if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) { - result = false; - } - }); - } - - if (!result) { - return false; - } - - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - - return result; - } - - function keys(obj, isArray) { - var allKeys = Object.keys ? Object.keys(obj) : - (function(o) { - var keys = []; - for (var key in o) { - if (j$.util.has(o, key)) { - keys.push(key); - } - } - return keys; - })(obj); - - if (!isArray) { - return allKeys; - } - - if (allKeys.length === 0) { - return allKeys; - } - - var extraKeys = []; - for (var i = 0; i < allKeys.length; i++) { - if (!/^[0-9]+$/.test(allKeys[i])) { - extraKeys.push(allKeys[i]); - } - } - - return extraKeys; - } - - function has(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); - } - - function isFunction(obj) { - return typeof obj === 'function'; - } - - function objectKeysAreDifferentFormatter(actual, expected, path) { - var missingProperties = j$.util.objectDifference(expected, actual), - extraProperties = j$.util.objectDifference(actual, expected), - missingPropertiesMessage = formatKeyValuePairs(missingProperties), - extraPropertiesMessage = formatKeyValuePairs(extraProperties), - messages = []; - - if (!path.depth()) { - path = 'object'; - } - - if (missingPropertiesMessage.length) { - messages.push('Expected ' + path + ' to have properties' + missingPropertiesMessage); - } - - if (extraPropertiesMessage.length) { - messages.push('Expected ' + path + ' not to have properties' + extraPropertiesMessage); - } - - return messages.join('\n'); - } - - function constructorsAreDifferentFormatter(actual, expected, path) { - if (!path.depth()) { - path = 'object'; - } - - return 'Expected ' + - path + ' to be a kind of ' + - j$.fnNameFor(expected.constructor) + - ', but was ' + j$.pp(actual) + '.'; - } - - function formatKeyValuePairs(obj) { - var formatted = ''; - for (var key in obj) { - formatted += '\n ' + key + ': ' + j$.pp(obj[key]); - } - return formatted; - } -}; - -getJasmineRequireObj().NullDiffBuilder = function(j$) { - return function() { - return { - withPath: function(_, block) { - block(); - }, - record: function() {} - }; - }; -}; - -getJasmineRequireObj().ObjectPath = function(j$) { - function ObjectPath(components) { - this.components = components || []; - } - - ObjectPath.prototype.toString = function() { - if (this.components.length) { - return '$' + map(this.components, formatPropertyAccess).join(''); - } else { - return ''; - } - }; - - ObjectPath.prototype.add = function(component) { - return new ObjectPath(this.components.concat([component])); - }; - - ObjectPath.prototype.depth = function() { - return this.components.length; - }; - - function formatPropertyAccess(prop) { - if (typeof prop === 'number') { - return '[' + prop + ']'; - } - - if (isValidIdentifier(prop)) { - return '.' + prop; - } - - return '[\'' + prop + '\']'; - } - - function map(array, fn) { - var results = []; - for (var i = 0; i < array.length; i++) { - results.push(fn(array[i])); - } - return results; - } - - function isValidIdentifier(string) { - return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string); - } - - return ObjectPath; -}; - -getJasmineRequireObj().toBe = function() { - /** - * {@link expect} the actual value to be `===` to the expected value. - * @function - * @name matchers#toBe - * @param {Object} expected - The expected value to compare against. - * @example - * expect(thing).toBe(realThing); - */ - function toBe() { - return { - compare: function(actual, expected) { - return { - pass: actual === expected - }; - } - }; - } - - return toBe; -}; - -getJasmineRequireObj().toBeCloseTo = function() { - /** - * {@link expect} the actual value to be within a specified precision of the expected value. - * @function - * @name matchers#toBeCloseTo - * @param {Object} expected - The expected value to compare against. - * @param {Number} [precision=2] - The number of decimal points to check. - * @example - * expect(number).toBeCloseTo(42.2, 3); - */ - function toBeCloseTo() { - return { - compare: function(actual, expected, precision) { - if (precision !== 0) { - precision = precision || 2; - } - - return { - pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2) - }; - } - }; - } - - return toBeCloseTo; -}; - -getJasmineRequireObj().toBeDefined = function() { - /** - * {@link expect} the actual value to be defined. (Not `undefined`) - * @function - * @name matchers#toBeDefined - * @example - * expect(result).toBeDefined(); - */ - function toBeDefined() { - return { - compare: function(actual) { - return { - pass: (void 0 !== actual) - }; - } - }; - } - - return toBeDefined; -}; - -getJasmineRequireObj().toBeFalsy = function() { - /** - * {@link expect} the actual value to be falsy - * @function - * @name matchers#toBeFalsy - * @example - * expect(result).toBeFalsy(); - */ - function toBeFalsy() { - return { - compare: function(actual) { - return { - pass: !!!actual - }; - } - }; - } - - return toBeFalsy; -}; - -getJasmineRequireObj().toBeGreaterThan = function() { - /** - * {@link expect} the actual value to be greater than the expected value. - * @function - * @name matchers#toBeGreaterThan - * @param {Number} expected - The value to compare against. - * @example - * expect(result).toBeGreaterThan(3); - */ - function toBeGreaterThan() { - return { - compare: function(actual, expected) { - return { - pass: actual > expected - }; - } - }; - } - - return toBeGreaterThan; -}; - - -getJasmineRequireObj().toBeGreaterThanOrEqual = function() { - /** - * {@link expect} the actual value to be greater than or equal to the expected value. - * @function - * @name matchers#toBeGreaterThanOrEqual - * @param {Number} expected - The expected value to compare against. - * @example - * expect(result).toBeGreaterThanOrEqual(25); - */ - function toBeGreaterThanOrEqual() { - return { - compare: function(actual, expected) { - return { - pass: actual >= expected - }; - } - }; - } - - return toBeGreaterThanOrEqual; -}; - -getJasmineRequireObj().toBeLessThan = function() { - /** - * {@link expect} the actual value to be less than the expected value. - * @function - * @name matchers#toBeLessThan - * @param {Number} expected - The expected value to compare against. - * @example - * expect(result).toBeLessThan(0); - */ - function toBeLessThan() { - return { - - compare: function(actual, expected) { - return { - pass: actual < expected - }; - } - }; - } - - return toBeLessThan; -}; - -getJasmineRequireObj().toBeLessThanOrEqual = function() { - /** - * {@link expect} the actual value to be less than or equal to the expected value. - * @function - * @name matchers#toBeLessThanOrEqual - * @param {Number} expected - The expected value to compare against. - * @example - * expect(result).toBeLessThanOrEqual(123); - */ - function toBeLessThanOrEqual() { - return { - - compare: function(actual, expected) { - return { - pass: actual <= expected - }; - } - }; - } - - return toBeLessThanOrEqual; -}; - -getJasmineRequireObj().toBeNaN = function(j$) { - /** - * {@link expect} the actual value to be `NaN` (Not a Number). - * @function - * @name matchers#toBeNaN - * @example - * expect(thing).toBeNaN(); - */ - function toBeNaN() { - return { - compare: function(actual) { - var result = { - pass: (actual !== actual) - }; - - if (result.pass) { - result.message = 'Expected actual not to be NaN.'; - } else { - result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; }; - } - - return result; - } - }; - } - - return toBeNaN; -}; - -getJasmineRequireObj().toBeNegativeInfinity = function(j$) { - /** - * {@link expect} the actual value to be `-Infinity` (-infinity). - * @function - * @name matchers#toBeNegativeInfinity - * @example - * expect(thing).toBeNegativeInfinity(); - */ - function toBeNegativeInfinity() { - return { - compare: function(actual) { - var result = { - pass: (actual === Number.NEGATIVE_INFINITY) - }; - - if (result.pass) { - result.message = 'Expected actual to be -Infinity.'; - } else { - result.message = function() { return 'Expected ' + j$.pp(actual) + ' not to be -Infinity.'; }; - } - - return result; - } - }; - } - - return toBeNegativeInfinity; -}; - -getJasmineRequireObj().toBeNull = function() { - /** - * {@link expect} the actual value to be `null`. - * @function - * @name matchers#toBeNull - * @example - * expect(result).toBeNull(); - */ - function toBeNull() { - return { - compare: function(actual) { - return { - pass: actual === null - }; - } - }; - } - - return toBeNull; -}; - -getJasmineRequireObj().toBePositiveInfinity = function(j$) { - /** - * {@link expect} the actual value to be `Infinity` (infinity). - * @function - * @name matchers#toBePositiveInfinity - * @example - * expect(thing).toBePositiveInfinity(); - */ - function toBePositiveInfinity() { - return { - compare: function(actual) { - var result = { - pass: (actual === Number.POSITIVE_INFINITY) - }; - - if (result.pass) { - result.message = 'Expected actual to be Infinity.'; - } else { - result.message = function() { return 'Expected ' + j$.pp(actual) + ' not to be Infinity.'; }; - } - - return result; - } - }; - } - - return toBePositiveInfinity; -}; - -getJasmineRequireObj().toBeTruthy = function() { - /** - * {@link expect} the actual value to be truthy. - * @function - * @name matchers#toBeTruthy - * @example - * expect(thing).toBeTruthy(); - */ - function toBeTruthy() { - return { - compare: function(actual) { - return { - pass: !!actual - }; - } - }; - } - - return toBeTruthy; -}; - -getJasmineRequireObj().toBeUndefined = function() { - /** - * {@link expect} the actual value to be `undefined`. - * @function - * @name matchers#toBeUndefined - * @example - * expect(result).toBeUndefined(): - */ - function toBeUndefined() { - return { - compare: function(actual) { - return { - pass: void 0 === actual - }; - } - }; - } - - return toBeUndefined; -}; - -getJasmineRequireObj().toContain = function() { - /** - * {@link expect} the actual value to contain a specific value. - * @function - * @name matchers#toContain - * @param {Object} expected - The value to look for. - * @example - * expect(array).toContain(anElement); - * expect(string).toContain(substring); - */ - function toContain(util, customEqualityTesters) { - customEqualityTesters = customEqualityTesters || []; - - return { - compare: function(actual, expected) { - - return { - pass: util.contains(actual, expected, customEqualityTesters) - }; - } - }; - } - - return toContain; -}; - -getJasmineRequireObj().toEqual = function(j$) { - /** - * {@link expect} the actual value to be equal to the expected, using deep equality comparison. - * @function - * @name matchers#toEqual - * @param {Object} expected - Expected value - * @example - * expect(bigObject).toEqual({"foo": ['bar', 'baz']}); - */ - function toEqual(util, customEqualityTesters) { - customEqualityTesters = customEqualityTesters || []; - - return { - compare: function(actual, expected) { - var result = { - pass: false - }, - diffBuilder = j$.DiffBuilder(); - - result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder); - - // TODO: only set error message if test fails - result.message = diffBuilder.getMessage(); - - return result; - } - }; - } - - return toEqual; -}; - -getJasmineRequireObj().toHaveBeenCalled = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalled>', 'expect(<spyObj>).toHaveBeenCalled()'); - - /** - * {@link expect} the actual (a {@link Spy}) to have been called. - * @function - * @name matchers#toHaveBeenCalled - * @example - * expect(mySpy).toHaveBeenCalled(); - * expect(mySpy).not.toHaveBeenCalled(); - */ - function toHaveBeenCalled() { - return { - compare: function(actual) { - var result = {}; - - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - if (arguments.length > 1) { - throw new Error(getErrorMsg('Does not take arguments, use toHaveBeenCalledWith')); - } - - result.pass = actual.calls.any(); - - result.message = result.pass ? - 'Expected spy ' + actual.and.identity() + ' not to have been called.' : - 'Expected spy ' + actual.and.identity() + ' to have been called.'; - - return result; - } - }; - } - - return toHaveBeenCalled; -}; - -getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalledBefore>', 'expect(<spyObj>).toHaveBeenCalledBefore(<spyObj>)'); - - /** - * {@link expect} the actual value (a {@link Spy}) to have been called before another {@link Spy}. - * @function - * @name matchers#toHaveBeenCalledBefore - * @param {Spy} expected - {@link Spy} that should have been called after the `actual` {@link Spy}. - * @example - * expect(mySpy).toHaveBeenCalledBefore(otherSpy); - */ - function toHaveBeenCalledBefore() { - return { - compare: function(firstSpy, latterSpy) { - if (!j$.isSpy(firstSpy)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(firstSpy) + '.')); - } - if (!j$.isSpy(latterSpy)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(latterSpy) + '.')); - } - - var result = { pass: false }; - - if (!firstSpy.calls.count()) { - result.message = 'Expected spy ' + firstSpy.and.identity() + ' to have been called.'; - return result; - } - if (!latterSpy.calls.count()) { - result.message = 'Expected spy ' + latterSpy.and.identity() + ' to have been called.'; - return result; - } - - var latest1stSpyCall = firstSpy.calls.mostRecent().invocationOrder; - var first2ndSpyCall = latterSpy.calls.first().invocationOrder; - - result.pass = latest1stSpyCall < first2ndSpyCall; - - if (result.pass) { - result.message = 'Expected spy ' + firstSpy.and.identity() + ' to not have been called before spy ' + latterSpy.and.identity() + ', but it was'; - } else { - var first1stSpyCall = firstSpy.calls.first().invocationOrder; - var latest2ndSpyCall = latterSpy.calls.mostRecent().invocationOrder; - - if(first1stSpyCall < first2ndSpyCall) { - result.message = 'Expected latest call to spy ' + firstSpy.and.identity() + ' to have been called before first call to spy ' + latterSpy.and.identity() + ' (no interleaved calls)'; - } else if (latest2ndSpyCall > latest1stSpyCall) { - result.message = 'Expected first call to spy ' + latterSpy.and.identity() + ' to have been called after latest call to spy ' + firstSpy.and.identity() + ' (no interleaved calls)'; - } else { - result.message = 'Expected spy ' + firstSpy.and.identity() + ' to have been called before spy ' + latterSpy.and.identity(); - } - } - - return result; - } - }; - } - - return toHaveBeenCalledBefore; -}; - -getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalledTimes>', 'expect(<spyObj>).toHaveBeenCalledTimes(<Number>)'); - - /** - * {@link expect} the actual (a {@link Spy}) to have been called the specified number of times. - * @function - * @name matchers#toHaveBeenCalledTimes - * @param {Number} expected - The number of invocations to look for. - * @example - * expect(mySpy).toHaveBeenCalledTimes(3); - */ - function toHaveBeenCalledTimes() { - return { - compare: function(actual, expected) { - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - var args = Array.prototype.slice.call(arguments, 0), - result = { pass: false }; - - if (!j$.isNumber_(expected)){ - throw new Error(getErrorMsg('The expected times failed is a required argument and must be a number.')); - } - - actual = args[0]; - var calls = actual.calls.count(); - var timesMessage = expected === 1 ? 'once' : expected + ' times'; - result.pass = calls === expected; - result.message = result.pass ? - 'Expected spy ' + actual.and.identity() + ' not to have been called ' + timesMessage + '. It was called ' + calls + ' times.' : - 'Expected spy ' + actual.and.identity() + ' to have been called ' + timesMessage + '. It was called ' + calls + ' times.'; - return result; - } - }; - } - - return toHaveBeenCalledTimes; -}; - -getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalledWith>', 'expect(<spyObj>).toHaveBeenCalledWith(...arguments)'); - - /** - * {@link expect} the actual (a {@link Spy}) to have been called with particular arguments at least once. - * @function - * @name matchers#toHaveBeenCalledWith - * @param {...Object} - The arguments to look for - * @example - * expect(mySpy).toHaveBeenCalledWith('foo', 'bar', 2); - */ - function toHaveBeenCalledWith(util, customEqualityTesters) { - return { - compare: function() { - var args = Array.prototype.slice.call(arguments, 0), - actual = args[0], - expectedArgs = args.slice(1), - result = { pass: false }; - - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - if (!actual.calls.any()) { - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; }; - return result; - } - - if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) { - result.pass = true; - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; }; - } else { - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; }; - } - - return result; - } - }; - } - - return toHaveBeenCalledWith; -}; - -getJasmineRequireObj().toMatch = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toMatch>', 'expect(<expectation>).toMatch(<string> || <regexp>)'); - - /** - * {@link expect} the actual value to match a regular expression - * @function - * @name matchers#toMatch - * @param {RegExp|String} expected - Value to look for in the string. - * @example - * expect("my string").toMatch(/string$/); - * expect("other string").toMatch("her"); - */ - function toMatch() { - return { - compare: function(actual, expected) { - if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { - throw new Error(getErrorMsg('Expected is not a String or a RegExp')); - } - - var regexp = new RegExp(expected); - - return { - pass: regexp.test(actual) - }; - } - }; - } - - return toMatch; -}; - -getJasmineRequireObj().toThrow = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toThrow>', 'expect(function() {<expectation>}).toThrow()'); - - /** - * {@link expect} a function to `throw` something. - * @function - * @name matchers#toThrow - * @param {Object} [expected] - Value that should be thrown. If not provided, simply the fact that something was thrown will be checked. - * @example - * expect(function() { return 'things'; }).toThrow('foo'); - * expect(function() { return 'stuff'; }).toThrow(); - */ - function toThrow(util) { - return { - compare: function(actual, expected) { - var result = { pass: false }, - threw = false, - thrown; - - if (typeof actual != 'function') { - throw new Error(getErrorMsg('Actual is not a Function')); - } - - try { - actual(); - } catch (e) { - threw = true; - thrown = e; - } - - if (!threw) { - result.message = 'Expected function to throw an exception.'; - return result; - } - - if (arguments.length == 1) { - result.pass = true; - result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; }; - - return result; - } - - if (util.equals(thrown, expected)) { - result.pass = true; - result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; }; - } else { - result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' + j$.pp(thrown) + '.'; }; - } - - return result; - } - }; - } - - return toThrow; -}; - -getJasmineRequireObj().toThrowError = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<toThrowError>', 'expect(function() {<expectation>}).toThrowError(<ErrorConstructor>, <message>)'); - - /** - * {@link expect} a function to `throw` an `Error`. - * @function - * @name matchers#toThrowError - * @param {Error} [expected] - `Error` constructor the object that was thrown needs to be an instance of. If not provided, `Error` will be used. - * @param {RegExp|String} [message] - The message that should be set on the thrown `Error` - * @example - * expect(function() { return 'things'; }).toThrowError(MyCustomError, 'message'); - * expect(function() { return 'things'; }).toThrowError(MyCustomError, /bar/); - * expect(function() { return 'stuff'; }).toThrowError(MyCustomError); - * expect(function() { return 'other'; }).toThrowError(/foo/); - * expect(function() { return 'other'; }).toThrowError(); - */ - function toThrowError () { - return { - compare: function(actual) { - var threw = false, - pass = {pass: true}, - fail = {pass: false}, - thrown; - - if (typeof actual != 'function') { - throw new Error(getErrorMsg('Actual is not a Function')); - } - - var errorMatcher = getMatcher.apply(null, arguments); - - try { - actual(); - } catch (e) { - threw = true; - thrown = e; - } - - if (!threw) { - fail.message = 'Expected function to throw an Error.'; - return fail; - } - - // Get Error constructor of thrown - if (!isErrorObject(thrown)) { - fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; }; - return fail; - } - - if (errorMatcher.hasNoSpecifics()) { - pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + '.'; - return pass; - } - - if (errorMatcher.matches(thrown)) { - pass.message = function() { - return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.'; - }; - return pass; - } else { - fail.message = function() { - return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + - ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.'; - }; - return fail; - } - } - }; - - function getMatcher() { - var expected = null, - errorType = null; - - if (arguments.length == 2) { - expected = arguments[1]; - if (isAnErrorType(expected)) { - errorType = expected; - expected = null; - } - } else if (arguments.length > 2) { - errorType = arguments[1]; - expected = arguments[2]; - if (!isAnErrorType(errorType)) { - throw new Error(getErrorMsg('Expected error type is not an Error.')); - } - } - - if (expected && !isStringOrRegExp(expected)) { - if (errorType) { - throw new Error(getErrorMsg('Expected error message is not a string or RegExp.')); - } else { - throw new Error(getErrorMsg('Expected is not an Error, string, or RegExp.')); - } - } - - function messageMatch(message) { - if (typeof expected == 'string') { - return expected == message; - } else { - return expected.test(message); - } - } - - return { - errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception', - thrownDescription: function(thrown) { - var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception', - thrownMessage = ''; - - if (expected) { - thrownMessage = ' with message ' + j$.pp(thrown.message); - } - - return thrownName + thrownMessage; - }, - messageDescription: function() { - if (expected === null) { - return ''; - } else if (expected instanceof RegExp) { - return ' with a message matching ' + j$.pp(expected); - } else { - return ' with message ' + j$.pp(expected); - } - }, - hasNoSpecifics: function() { - return expected === null && errorType === null; - }, - matches: function(error) { - return (errorType === null || error instanceof errorType) && - (expected === null || messageMatch(error.message)); - } - }; - } - - function isStringOrRegExp(potential) { - return potential instanceof RegExp || (typeof potential == 'string'); - } - - function isAnErrorType(type) { - if (typeof type !== 'function') { - return false; - } - - var Surrogate = function() {}; - Surrogate.prototype = type.prototype; - return isErrorObject(new Surrogate()); - } - - function isErrorObject(thrown) { - if (thrown instanceof Error) { - return true; - } - if (thrown && thrown.constructor && thrown.constructor.constructor && - (thrown instanceof (thrown.constructor.constructor('return this')()).Error)) { - return true; - } - return false; - } - } - - return toThrowError; -}; - -getJasmineRequireObj().MockDate = function() { - function MockDate(global) { - var self = this; - var currentTime = 0; - - if (!global || !global.Date) { - self.install = function() {}; - self.tick = function() {}; - self.uninstall = function() {}; - return self; - } - - var GlobalDate = global.Date; - - self.install = function(mockDate) { - if (mockDate instanceof GlobalDate) { - currentTime = mockDate.getTime(); - } else { - currentTime = new GlobalDate().getTime(); - } - - global.Date = FakeDate; - }; - - self.tick = function(millis) { - millis = millis || 0; - currentTime = currentTime + millis; - }; - - self.uninstall = function() { - currentTime = 0; - global.Date = GlobalDate; - }; - - createDateProperties(); - - return self; - - function FakeDate() { - switch(arguments.length) { - case 0: - return new GlobalDate(currentTime); - case 1: - return new GlobalDate(arguments[0]); - case 2: - return new GlobalDate(arguments[0], arguments[1]); - case 3: - return new GlobalDate(arguments[0], arguments[1], arguments[2]); - case 4: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); - case 5: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4]); - case 6: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5]); - default: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5], arguments[6]); - } - } - - function createDateProperties() { - FakeDate.prototype = GlobalDate.prototype; - - FakeDate.now = function() { - if (GlobalDate.now) { - return currentTime; - } else { - throw new Error('Browser does not support Date.now()'); - } - }; - - FakeDate.toSource = GlobalDate.toSource; - FakeDate.toString = GlobalDate.toString; - FakeDate.parse = GlobalDate.parse; - FakeDate.UTC = GlobalDate.UTC; - } - } - - return MockDate; -}; - -getJasmineRequireObj().pp = function(j$) { - - function PrettyPrinter() { - this.ppNestLevel_ = 0; - this.seen = []; - } - - function hasCustomToString(value) { - // value.toString !== Object.prototype.toString if value has no custom toString but is from another context (e.g. - // iframe, web worker) - return value.toString !== Object.prototype.toString && (value.toString() !== Object.prototype.toString.call(value)); - } - - PrettyPrinter.prototype.format = function(value) { - this.ppNestLevel_++; - try { - if (j$.util.isUndefined(value)) { - this.emitScalar('undefined'); - } else if (value === null) { - this.emitScalar('null'); - } else if (value === 0 && 1/value === -Infinity) { - this.emitScalar('-0'); - } else if (value === j$.getGlobal()) { - this.emitScalar('<global>'); - } else if (value.jasmineToString) { - this.emitScalar(value.jasmineToString()); - } else if (typeof value === 'string') { - this.emitString(value); - } else if (j$.isSpy(value)) { - this.emitScalar('spy on ' + value.and.identity()); - } else if (value instanceof RegExp) { - this.emitScalar(value.toString()); - } else if (typeof value === 'function') { - this.emitScalar('Function'); - } else if (typeof value.nodeType === 'number') { - this.emitScalar('HTMLNode'); - } else if (value instanceof Date) { - this.emitScalar('Date(' + value + ')'); - } else if (value.toString && value.toString() == '[object Set]') { - this.emitSet(value); - } else if (value.toString && typeof value === 'object' && !j$.isArray_(value) && hasCustomToString(value)) { - this.emitScalar(value.toString()); - } else if (j$.util.arrayContains(this.seen, value)) { - this.emitScalar('<circular reference: ' + (j$.isArray_(value) ? 'Array' : 'Object') + '>'); - } else if (j$.isArray_(value) || j$.isA_('Object', value)) { - this.seen.push(value); - if (j$.isArray_(value)) { - this.emitArray(value); - } else { - this.emitObject(value); - } - this.seen.pop(); - } else { - this.emitScalar(value.toString()); - } - } finally { - this.ppNestLevel_--; - } - }; - - PrettyPrinter.prototype.iterateObject = function(obj, fn) { - for (var property in obj) { - if (!Object.prototype.hasOwnProperty.call(obj, property)) { continue; } - fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) && - obj.__lookupGetter__(property) !== null) : false); - } - }; - - PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitSet = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_; - PrettyPrinter.prototype.emitString = j$.unimplementedMethod_; - - function StringPrettyPrinter() { - PrettyPrinter.call(this); - - this.string = ''; - } - - j$.util.inherit(StringPrettyPrinter, PrettyPrinter); - - StringPrettyPrinter.prototype.emitScalar = function(value) { - this.append(value); - }; - - StringPrettyPrinter.prototype.emitString = function(value) { - this.append('\'' + value + '\''); - }; - - StringPrettyPrinter.prototype.emitArray = function(array) { - if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { - this.append('Array'); - return; - } - var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); - this.append('[ '); - for (var i = 0; i < length; i++) { - if (i > 0) { - this.append(', '); - } - this.format(array[i]); - } - if(array.length > length){ - this.append(', ...'); - } - - var self = this; - var first = array.length === 0; - this.iterateObject(array, function(property, isGetter) { - if (property.match(/^\d+$/)) { - return; - } - - if (first) { - first = false; - } else { - self.append(', '); - } - - self.formatProperty(array, property, isGetter); - }); - - this.append(' ]'); - }; - - StringPrettyPrinter.prototype.emitSet = function(set) { - if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { - this.append('Set'); - return; - } - this.append('Set( '); - var size = Math.min(set.size, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); - var iter = set.values(); - for (var i = 0; i < size; i++) { - if (i > 0) { - this.append(', '); - } - this.format(iter.next().value); - } - if (set.size > size){ - this.append(', ...'); - } - this.append(' )'); - }; - - StringPrettyPrinter.prototype.emitObject = function(obj) { - var ctor = obj.constructor, - constructorName; - - constructorName = typeof ctor === 'function' && obj instanceof ctor ? - j$.fnNameFor(obj.constructor) : - 'null'; - - this.append(constructorName); - - if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { - return; - } - - var self = this; - this.append('({ '); - var first = true; - - this.iterateObject(obj, function(property, isGetter) { - if (first) { - first = false; - } else { - self.append(', '); - } - - self.formatProperty(obj, property, isGetter); - }); - - this.append(' })'); - }; - - StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { - this.append(property); - this.append(': '); - if (isGetter) { - this.append('<getter>'); - } else { - this.format(obj[property]); - } - }; - - StringPrettyPrinter.prototype.append = function(value) { - this.string += value; - }; - - return function(value) { - var stringPrettyPrinter = new StringPrettyPrinter(); - stringPrettyPrinter.format(value); - return stringPrettyPrinter.string; - }; -}; - -getJasmineRequireObj().QueueRunner = function(j$) { - - function once(fn) { - var called = false; - return function() { - if (!called) { - called = true; - fn(); - } - return null; - }; - } - - function QueueRunner(attrs) { - this.queueableFns = attrs.queueableFns || []; - this.onComplete = attrs.onComplete || function() {}; - this.clearStack = attrs.clearStack || function(fn) {fn();}; - this.onException = attrs.onException || function() {}; - this.catchException = attrs.catchException || function() { return true; }; - this.userContext = attrs.userContext || {}; - this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; - this.fail = attrs.fail || function() {}; - this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} }; - } - - QueueRunner.prototype.execute = function() { - this.run(this.queueableFns, 0); - }; - - QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { - var length = queueableFns.length, - self = this, - iterativeIndex; - - - for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { - var queueableFn = queueableFns[iterativeIndex]; - if (queueableFn.fn.length > 0) { - attemptAsync(queueableFn); - return; - } else { - attemptSync(queueableFn); - } - } - - this.clearStack(this.onComplete); - - function attemptSync(queueableFn) { - try { - queueableFn.fn.call(self.userContext); - } catch (e) { - handleException(e, queueableFn); - } - } - - function attemptAsync(queueableFn) { - var clearTimeout = function () { - Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); - }, - handleError = function(error) { - onException(error); - next(); - }, - next = once(function () { - clearTimeout(timeoutId); - self.globalErrors.popListener(handleError); - self.run(queueableFns, iterativeIndex + 1); - }), - timeoutId; - - next.fail = function() { - self.fail.apply(null, arguments); - next(); - }; - - self.globalErrors.pushListener(handleError); - - if (queueableFn.timeout) { - timeoutId = Function.prototype.apply.apply(self.timeout.setTimeout, [j$.getGlobal(), [function() { - var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); - onException(error); - next(); - }, queueableFn.timeout()]]); - } - - try { - queueableFn.fn.call(self.userContext, next); - } catch (e) { - handleException(e, queueableFn); - next(); - } - } - - function onException(e) { - self.onException(e); - } - - function handleException(e, queueableFn) { - onException(e); - if (!self.catchException(e)) { - //TODO: set a var when we catch an exception and - //use a finally block to close the loop in a nice way.. - throw e; - } - } - }; - - return QueueRunner; -}; - -getJasmineRequireObj().ReportDispatcher = function() { - function ReportDispatcher(methods) { - - var dispatchedMethods = methods || []; - - for (var i = 0; i < dispatchedMethods.length; i++) { - var method = dispatchedMethods[i]; - this[method] = (function(m) { - return function() { - dispatch(m, arguments); - }; - }(method)); - } - - var reporters = []; - var fallbackReporter = null; - - this.addReporter = function(reporter) { - reporters.push(reporter); - }; - - this.provideFallbackReporter = function(reporter) { - fallbackReporter = reporter; - }; - - this.clearReporters = function() { - reporters = []; - }; - - return this; - - function dispatch(method, args) { - if (reporters.length === 0 && fallbackReporter !== null) { - reporters.push(fallbackReporter); - } - for (var i = 0; i < reporters.length; i++) { - var reporter = reporters[i]; - if (reporter[method]) { - reporter[method].apply(reporter, args); - } - } - } - } - - return ReportDispatcher; -}; - - -getJasmineRequireObj().interface = function(jasmine, env) { - var jasmineInterface = { - /** - * Create a group of specs (often called a suite). - * - * Calls to `describe` can be nested within other calls to compose your suite as a tree. - * @name describe - * @function - * @global - * @param {String} description Textual description of the group - * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites a specs - */ - describe: function(description, specDefinitions) { - return env.describe(description, specDefinitions); - }, - - /** - * A temporarily disabled [`describe`]{@link describe} - * - * Specs within an `xdescribe` will be marked pending and not executed - * @name xdescribe - * @function - * @global - * @param {String} description Textual description of the group - * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites a specs - */ - xdescribe: function(description, specDefinitions) { - return env.xdescribe(description, specDefinitions); - }, - - /** - * A focused [`describe`]{@link describe} - * - * If suites or specs are focused, only those that are focused will be executed - * @see fit - * @name fdescribe - * @function - * @global - * @param {String} description Textual description of the group - * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites a specs - */ - fdescribe: function(description, specDefinitions) { - return env.fdescribe(description, specDefinitions); - }, - - /** - * Define a single spec. A spec should contain one or more {@link expect|expectations} that test the state of the code. - * - * A spec whose expectations all succeed will be passing and a spec with any failures will fail. - * @name it - * @function - * @global - * @param {String} description Textual description of what this spec is checking - * @param {Function} [testFunction] Function that contains the code of your test. If not provided the test will be `pending`. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec. - */ - it: function() { - return env.it.apply(env, arguments); - }, - - /** - * A temporarily disabled [`it`]{@link it} - * - * The spec will report as `pending` and will not be executed. - * @name xit - * @function - * @global - * @param {String} description Textual description of what this spec is checking. - * @param {Function} [testFunction] Function that contains the code of your test. Will not be executed. - */ - xit: function() { - return env.xit.apply(env, arguments); - }, - - /** - * A focused [`it`]{@link it} - * - * If suites or specs are focused, only those that are focused will be executed. - * @name fit - * @function - * @global - * @param {String} description Textual description of what this spec is checking. - * @param {Function} testFunction Function that contains the code of your test. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec. - */ - fit: function() { - return env.fit.apply(env, arguments); - }, - - /** - * Run some shared setup before each of the specs in the {@link describe} in which it is called. - * @name beforeEach - * @function - * @global - * @param {Function} [function] Function that contains the code to setup your specs. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeEach. - */ - beforeEach: function() { - return env.beforeEach.apply(env, arguments); - }, - - /** - * Run some shared teardown after each of the specs in the {@link describe} in which it is called. - * @name afterEach - * @function - * @global - * @param {Function} [function] Function that contains the code to teardown your specs. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterEach. - */ - afterEach: function() { - return env.afterEach.apply(env, arguments); - }, - - /** - * Run some shared setup once before all of the specs in the {@link describe} are run. - * - * _Note:_ Be careful, sharing the setup from a beforeAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail. - * @name beforeAll - * @function - * @global - * @param {Function} [function] Function that contains the code to setup your specs. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeAll. - */ - beforeAll: function() { - return env.beforeAll.apply(env, arguments); - }, - - /** - * Run some shared teardown once before all of the specs in the {@link describe} are run. - * - * _Note:_ Be careful, sharing the teardown from a afterAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail. - * @name afterAll - * @function - * @global - * @param {Function} [function] Function that contains the code to teardown your specs. - * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterAll. - */ - afterAll: function() { - return env.afterAll.apply(env, arguments); - }, - - /** - * Create an expectation for a spec. - * @name expect - * @function - * @global - * @param {Object} actual - Actual computed value to test expectations against. - * @return {matchers} - */ - expect: function(actual) { - return env.expect(actual); - }, - - /** - * Mark a spec as pending, expectation results will be ignored. - * @name pending - * @function - * @global - * @param {String} [message] - Reason the spec is pending. - */ - pending: function() { - return env.pending.apply(env, arguments); - }, - - /** - * Explicitly mark a spec as failed. - * @name fail - * @function - * @global - * @param {String|Error} [error] - Reason for the failure. - */ - fail: function() { - return env.fail.apply(env, arguments); - }, - - /** - * Install a spy onto an existing object. - * @name spyOn - * @function - * @global - * @param {Object} obj - The object upon which to install the {@link Spy}. - * @param {String} methodName - The name of the method to replace with a {@link Spy}. - * @returns {Spy} - */ - spyOn: function(obj, methodName) { - return env.spyOn(obj, methodName); - }, - - /** - * Install a spy on a property onto an existing object. - * @name spyOnProperty - * @function - * @global - * @param {Object} obj - The object upon which to install the {@link Spy} - * @param {String} propertyName - The name of the property to replace with a {@link Spy}. - * @param {String} [accessType=get] - The access type (get|set) of the property to {@link Spy} on. - * @returns {Spy} - */ - spyOnProperty: function(obj, methodName, accessType) { - return env.spyOnProperty(obj, methodName, accessType); - }, - - jsApiReporter: new jasmine.JsApiReporter({ - timer: new jasmine.Timer() - }), - - /** - * @namespace jasmine - */ - jasmine: jasmine - }; - - /** - * Add a custom equality tester for the current scope of specs. - * - * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. - * @name jasmine.addCustomEqualityTester - * @function - * @param {Function} tester - A function which takes two arguments to compare and returns a `true` or `false` comparison result if it knows how to compare them, and `undefined` otherwise. - * @see custom_equality - */ - jasmine.addCustomEqualityTester = function(tester) { - env.addCustomEqualityTester(tester); - }; - - /** - * Add custom matchers for the current scope of specs. - * - * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. - * @name jasmine.addMatchers - * @function - * @param {Object} matchers - Keys from this object will be the new matcher names. - * @see custom_matcher - */ - jasmine.addMatchers = function(matchers) { - return env.addMatchers(matchers); - }; - - /** - * Get the currently booted mock {Clock} for this Jasmine environment. - * @name jasmine.clock - * @function - * @returns {Clock} - */ - jasmine.clock = function() { - return env.clock; - }; - - return jasmineInterface; -}; - -getJasmineRequireObj().Spy = function (j$) { - - var nextOrder = (function() { - var order = 0; - - return function() { - return order++; - }; - })(); - - /** - * _Note:_ Do not construct this directly, use {@link spyOn}, {@link spyOnProperty}, {@link jasmine.createSpy}, or {@link jasmine.createSpyObj} - * @constructor - * @name Spy - */ - function Spy(name, originalFn) { - var numArgs = (typeof originalFn === 'function' ? originalFn.length : 0), - wrapper = makeFunc(numArgs, function () { - return spy.apply(this, Array.prototype.slice.call(arguments)); - }), - spyStrategy = new j$.SpyStrategy({ - name: name, - fn: originalFn, - getSpy: function () { - return wrapper; - } - }), - callTracker = new j$.CallTracker(), - spy = function () { - /** - * @name Spy.callData - * @property {object} object - `this` context for the invocation. - * @property {number} invocationOrder - Order of the invocation. - * @property {Array} args - The arguments passed for this invocation. - */ - var callData = { - object: this, - invocationOrder: nextOrder(), - args: Array.prototype.slice.apply(arguments) - }; - - callTracker.track(callData); - var returnValue = spyStrategy.exec.apply(this, arguments); - callData.returnValue = returnValue; - - return returnValue; - }; - - function makeFunc(length, fn) { - switch (length) { - case 1 : return function (a) { return fn.apply(this, arguments); }; - case 2 : return function (a,b) { return fn.apply(this, arguments); }; - case 3 : return function (a,b,c) { return fn.apply(this, arguments); }; - case 4 : return function (a,b,c,d) { return fn.apply(this, arguments); }; - case 5 : return function (a,b,c,d,e) { return fn.apply(this, arguments); }; - case 6 : return function (a,b,c,d,e,f) { return fn.apply(this, arguments); }; - case 7 : return function (a,b,c,d,e,f,g) { return fn.apply(this, arguments); }; - case 8 : return function (a,b,c,d,e,f,g,h) { return fn.apply(this, arguments); }; - case 9 : return function (a,b,c,d,e,f,g,h,i) { return fn.apply(this, arguments); }; - default : return function () { return fn.apply(this, arguments); }; - } - } - - for (var prop in originalFn) { - if (prop === 'and' || prop === 'calls') { - throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); - } - - wrapper[prop] = originalFn[prop]; - } - - wrapper.and = spyStrategy; - wrapper.calls = callTracker; - - return wrapper; - } - - return Spy; -}; - -getJasmineRequireObj().SpyRegistry = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('<spyOn>', 'spyOn(<object>, <methodName>)'); - - function SpyRegistry(options) { - options = options || {}; - var currentSpies = options.currentSpies || function() { return []; }; - - this.allowRespy = function(allow){ - this.respy = allow; - }; - - this.spyOn = function(obj, methodName) { - - if (j$.util.isUndefined(obj) || obj === null) { - throw new Error(getErrorMsg('could not find an object to spy upon for ' + methodName + '()')); - } - - if (j$.util.isUndefined(methodName) || methodName === null) { - throw new Error(getErrorMsg('No method name supplied')); - } - - if (j$.util.isUndefined(obj[methodName])) { - throw new Error(getErrorMsg(methodName + '() method does not exist')); - } - - if (obj[methodName] && j$.isSpy(obj[methodName]) ) { - if ( !!this.respy ){ - return obj[methodName]; - }else { - throw new Error(getErrorMsg(methodName + ' has already been spied upon')); - } - } - - var descriptor; - try { - descriptor = Object.getOwnPropertyDescriptor(obj, methodName); - } catch(e) { - // IE 8 doesn't support `definePropery` on non-DOM nodes - } - - if (descriptor && !(descriptor.writable || descriptor.set)) { - throw new Error(getErrorMsg(methodName + ' is not declared writable or has no setter')); - } - - var originalMethod = obj[methodName], - spiedMethod = j$.createSpy(methodName, originalMethod), - restoreStrategy; - - if (Object.prototype.hasOwnProperty.call(obj, methodName)) { - restoreStrategy = function() { - obj[methodName] = originalMethod; - }; - } else { - restoreStrategy = function() { - if (!delete obj[methodName]) { - obj[methodName] = originalMethod; - } - }; - } - - currentSpies().push({ - restoreObjectToOriginalState: restoreStrategy - }); - - obj[methodName] = spiedMethod; - - return spiedMethod; - }; - - this.spyOnProperty = function (obj, propertyName, accessType) { - accessType = accessType || 'get'; - - if (j$.util.isUndefined(obj)) { - throw new Error('spyOn could not find an object to spy upon for ' + propertyName + ''); - } - - if (j$.util.isUndefined(propertyName)) { - throw new Error('No property name supplied'); - } - - var descriptor; - try { - descriptor = j$.util.getPropertyDescriptor(obj, propertyName); - } catch(e) { - // IE 8 doesn't support `definePropery` on non-DOM nodes - } - - if (!descriptor) { - throw new Error(propertyName + ' property does not exist'); - } - - if (!descriptor.configurable) { - throw new Error(propertyName + ' is not declared configurable'); - } - - if(!descriptor[accessType]) { - throw new Error('Property ' + propertyName + ' does not have access type ' + accessType); - } - - if (j$.isSpy(descriptor[accessType])) { - //TODO?: should this return the current spy? Downside: may cause user confusion about spy state - throw new Error(propertyName + ' has already been spied upon'); - } - - var originalDescriptor = j$.util.clone(descriptor), - spy = j$.createSpy(propertyName, descriptor[accessType]), - restoreStrategy; - - if (Object.prototype.hasOwnProperty.call(obj, propertyName)) { - restoreStrategy = function() { - Object.defineProperty(obj, propertyName, originalDescriptor); - }; - } else { - restoreStrategy = function() { - delete obj[propertyName]; - }; - } - - currentSpies().push({ - restoreObjectToOriginalState: restoreStrategy - }); - - descriptor[accessType] = spy; - - Object.defineProperty(obj, propertyName, descriptor); - - return spy; - }; - - this.clearSpies = function() { - var spies = currentSpies(); - for (var i = spies.length - 1; i >= 0; i--) { - var spyEntry = spies[i]; - spyEntry.restoreObjectToOriginalState(); - } - }; - } - - return SpyRegistry; -}; - -getJasmineRequireObj().SpyStrategy = function(j$) { - - /** - * @namespace Spy#and - */ - function SpyStrategy(options) { - options = options || {}; - - var identity = options.name || 'unknown', - originalFn = options.fn || function() {}, - getSpy = options.getSpy || function() {}, - plan = function() {}; - - /** - * Return the identifying information for the spy. - * @name Spy#and#identity - * @function - * @returns {String} - */ - this.identity = function() { - return identity; - }; - - /** - * Execute the current spy strategy. - * @name Spy#and#exec - * @function - */ - this.exec = function() { - return plan.apply(this, arguments); - }; - - /** - * Tell the spy to call through to the real implementation when invoked. - * @name Spy#and#callThrough - * @function - */ - this.callThrough = function() { - plan = originalFn; - return getSpy(); - }; - - /** - * Tell the spy to return the value when invoked. - * @name Spy#and#returnValue - * @function - * @param {*} value The value to return. - */ - this.returnValue = function(value) { - plan = function() { - return value; - }; - return getSpy(); - }; - - /** - * Tell the spy to return one of the specified values (sequentially) each time the spy is invoked. - * @name Spy#and#returnValues - * @function - * @param {...*} values - Values to be returned on subsequent calls to the spy. - */ - this.returnValues = function() { - var values = Array.prototype.slice.call(arguments); - plan = function () { - return values.shift(); - }; - return getSpy(); - }; - - /** - * Tell the spy to throw an error when invoked. - * @name Spy#and#throwError - * @function - * @param {Error|String} something Thing to throw - */ - this.throwError = function(something) { - var error = (something instanceof Error) ? something : new Error(something); - plan = function() { - throw error; - }; - return getSpy(); - }; - - /** - * Tell the spy to call a fake implementation when invoked. - * @name Spy#and#callFake - * @function - * @param {Function} fn The function to invoke with the passed parameters. - */ - this.callFake = function(fn) { - if(!j$.isFunction_(fn)) { - throw new Error('Argument passed to callFake should be a function, got ' + fn); - } - plan = fn; - return getSpy(); - }; - - /** - * Tell the spy to do nothing when invoked. This is the default. - * @name Spy#and#stub - * @function - */ - this.stub = function(fn) { - plan = function() {}; - return getSpy(); - }; - } - - return SpyStrategy; -}; - -getJasmineRequireObj().Suite = function(j$) { - function Suite(attrs) { - this.env = attrs.env; - this.id = attrs.id; - this.parentSuite = attrs.parentSuite; - this.description = attrs.description; - this.expectationFactory = attrs.expectationFactory; - this.expectationResultFactory = attrs.expectationResultFactory; - this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; - - this.beforeFns = []; - this.afterFns = []; - this.beforeAllFns = []; - this.afterAllFns = []; - - this.children = []; - - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [] - }; - } - - Suite.prototype.expect = function(actual) { - return this.expectationFactory(actual, this); - }; - - Suite.prototype.getFullName = function() { - var fullName = []; - for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) { - if (parentSuite.parentSuite) { - fullName.unshift(parentSuite.description); - } - } - return fullName.join(' '); - }; - - Suite.prototype.pend = function() { - this.markedPending = true; - }; - - Suite.prototype.beforeEach = function(fn) { - this.beforeFns.unshift(fn); - }; - - Suite.prototype.beforeAll = function(fn) { - this.beforeAllFns.push(fn); - }; - - Suite.prototype.afterEach = function(fn) { - this.afterFns.unshift(fn); - }; - - Suite.prototype.afterAll = function(fn) { - this.afterAllFns.unshift(fn); - }; - - Suite.prototype.addChild = function(child) { - this.children.push(child); - }; - - Suite.prototype.status = function() { - if (this.markedPending) { - return 'pending'; - } - - if (this.result.failedExpectations.length > 0) { - return 'failed'; - } else { - return 'finished'; - } - }; - - Suite.prototype.isExecutable = function() { - return !this.markedPending; - }; - - Suite.prototype.canBeReentered = function() { - return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0; - }; - - Suite.prototype.getResult = function() { - this.result.status = this.status(); - return this.result; - }; - - Suite.prototype.sharedUserContext = function() { - if (!this.sharedContext) { - this.sharedContext = this.parentSuite ? clone(this.parentSuite.sharedUserContext()) : {}; - } - - return this.sharedContext; - }; - - Suite.prototype.clonedSharedUserContext = function() { - return clone(this.sharedUserContext()); - }; - - Suite.prototype.onException = function() { - if (arguments[0] instanceof j$.errors.ExpectationFailed) { - return; - } - - if(isAfterAll(this.children)) { - var data = { - matcherName: '', - passed: false, - expected: '', - actual: '', - error: arguments[0] - }; - this.result.failedExpectations.push(this.expectationResultFactory(data)); - } else { - for (var i = 0; i < this.children.length; i++) { - var child = this.children[i]; - child.onException.apply(child, arguments); - } - } - }; - - Suite.prototype.addExpectationResult = function () { - if(isAfterAll(this.children) && isFailure(arguments)){ - var data = arguments[1]; - this.result.failedExpectations.push(this.expectationResultFactory(data)); - if(this.throwOnExpectationFailure) { - throw new j$.errors.ExpectationFailed(); - } - } else { - for (var i = 0; i < this.children.length; i++) { - var child = this.children[i]; - try { - child.addExpectationResult.apply(child, arguments); - } catch(e) { - // keep going - } - } - } - }; - - function isAfterAll(children) { - return children && children[0].result.status; - } - - function isFailure(args) { - return !args[0]; - } - - function clone(obj) { - var clonedObj = {}; - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - clonedObj[prop] = obj[prop]; - } - } - - return clonedObj; - } - - return Suite; -}; - -if (typeof window == void 0 && typeof exports == 'object') { - exports.Suite = jasmineRequire.Suite; -} - -getJasmineRequireObj().Timer = function() { - var defaultNow = (function(Date) { - return function() { return new Date().getTime(); }; - })(Date); - - function Timer(options) { - options = options || {}; - - var now = options.now || defaultNow, - startTime; - - this.start = function() { - startTime = now(); - }; - - this.elapsed = function() { - return now() - startTime; - }; - } - - return Timer; -}; - -getJasmineRequireObj().TreeProcessor = function() { - function TreeProcessor(attrs) { - var tree = attrs.tree, - runnableIds = attrs.runnableIds, - queueRunnerFactory = attrs.queueRunnerFactory, - nodeStart = attrs.nodeStart || function() {}, - nodeComplete = attrs.nodeComplete || function() {}, - orderChildren = attrs.orderChildren || function(node) { return node.children; }, - stats = { valid: true }, - processed = false, - defaultMin = Infinity, - defaultMax = 1 - Infinity; - - this.processTree = function() { - processNode(tree, false); - processed = true; - return stats; - }; - - this.execute = function(done) { - if (!processed) { - this.processTree(); - } - - if (!stats.valid) { - throw 'invalid order'; - } - - var childFns = wrapChildren(tree, 0); - - queueRunnerFactory({ - queueableFns: childFns, - userContext: tree.sharedUserContext(), - onException: function() { - tree.onException.apply(tree, arguments); - }, - onComplete: done - }); - }; - - function runnableIndex(id) { - for (var i = 0; i < runnableIds.length; i++) { - if (runnableIds[i] === id) { - return i; - } - } - } - - function processNode(node, parentEnabled) { - var executableIndex = runnableIndex(node.id); - - if (executableIndex !== undefined) { - parentEnabled = true; - } - - parentEnabled = parentEnabled && node.isExecutable(); - - if (!node.children) { - stats[node.id] = { - executable: parentEnabled && node.isExecutable(), - segments: [{ - index: 0, - owner: node, - nodes: [node], - min: startingMin(executableIndex), - max: startingMax(executableIndex) - }] - }; - } else { - var hasExecutableChild = false; - - var orderedChildren = orderChildren(node); - - for (var i = 0; i < orderedChildren.length; i++) { - var child = orderedChildren[i]; - - processNode(child, parentEnabled); - - if (!stats.valid) { - return; - } - - var childStats = stats[child.id]; - - hasExecutableChild = hasExecutableChild || childStats.executable; - } - - stats[node.id] = { - executable: hasExecutableChild - }; - - segmentChildren(node, orderedChildren, stats[node.id], executableIndex); - - if (!node.canBeReentered() && stats[node.id].segments.length > 1) { - stats = { valid: false }; - } - } - } - - function startingMin(executableIndex) { - return executableIndex === undefined ? defaultMin : executableIndex; - } - - function startingMax(executableIndex) { - return executableIndex === undefined ? defaultMax : executableIndex; - } - - function segmentChildren(node, orderedChildren, nodeStats, executableIndex) { - var currentSegment = { index: 0, owner: node, nodes: [], min: startingMin(executableIndex), max: startingMax(executableIndex) }, - result = [currentSegment], - lastMax = defaultMax, - orderedChildSegments = orderChildSegments(orderedChildren); - - function isSegmentBoundary(minIndex) { - return lastMax !== defaultMax && minIndex !== defaultMin && lastMax < minIndex - 1; - } - - for (var i = 0; i < orderedChildSegments.length; i++) { - var childSegment = orderedChildSegments[i], - maxIndex = childSegment.max, - minIndex = childSegment.min; - - if (isSegmentBoundary(minIndex)) { - currentSegment = {index: result.length, owner: node, nodes: [], min: defaultMin, max: defaultMax}; - result.push(currentSegment); - } - - currentSegment.nodes.push(childSegment); - currentSegment.min = Math.min(currentSegment.min, minIndex); - currentSegment.max = Math.max(currentSegment.max, maxIndex); - lastMax = maxIndex; - } - - nodeStats.segments = result; - } - - function orderChildSegments(children) { - var specifiedOrder = [], - unspecifiedOrder = []; - - for (var i = 0; i < children.length; i++) { - var child = children[i], - segments = stats[child.id].segments; - - for (var j = 0; j < segments.length; j++) { - var seg = segments[j]; - - if (seg.min === defaultMin) { - unspecifiedOrder.push(seg); - } else { - specifiedOrder.push(seg); - } - } - } - - specifiedOrder.sort(function(a, b) { - return a.min - b.min; - }); - - return specifiedOrder.concat(unspecifiedOrder); - } - - function executeNode(node, segmentNumber) { - if (node.children) { - return { - fn: function(done) { - nodeStart(node); - - queueRunnerFactory({ - onComplete: function() { - nodeComplete(node, node.getResult()); - done(); - }, - queueableFns: wrapChildren(node, segmentNumber), - userContext: node.sharedUserContext(), - onException: function() { - node.onException.apply(node, arguments); - } - }); - } - }; - } else { - return { - fn: function(done) { node.execute(done, stats[node.id].executable); } - }; - } - } - - function wrapChildren(node, segmentNumber) { - var result = [], - segmentChildren = stats[node.id].segments[segmentNumber].nodes; - - for (var i = 0; i < segmentChildren.length; i++) { - result.push(executeNode(segmentChildren[i].owner, segmentChildren[i].index)); - } - - if (!stats[node.id].executable) { - return result; - } - - return node.beforeAllFns.concat(result).concat(node.afterAllFns); - } - } - - return TreeProcessor; -}; - -getJasmineRequireObj().version = function() { - return '2.6.1'; -}; diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/main.js b/plugins/easy_mindmup/assets/javascripts/jasmine/main.js deleted file mode 100644 index db22637..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/main.js +++ /dev/null @@ -1,66 +0,0 @@ -describe("Test framework", function () { - it("should load", function () { - expect(true).toBe(true); - // expect(false).toBe(true); - }); - - it("should start after mindmup is loaded", function () { - expect($("#node_1").length).toBe(1); - }); - it("should handle long tests", function (done) { - setTimeout(function () { - expect(true).toBe(true); - done(); - }, 100); - }); - var getQueryString = function () { - var query_string = {}; - var query = window.location.search.substring(1); - var vars = query.split("&"); - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split("="); - // If first entry with this name - if (typeof query_string[pair[0]] === "undefined") { - query_string[pair[0]] = decodeURIComponent(pair[1]); - // If second entry with this name - } else if (typeof query_string[pair[0]] === "string") { - query_string[pair[0]] = [query_string[pair[0]], decodeURIComponent(pair[1])]; - // If third or later entry with this name - } else { - query_string[pair[0]].push(decodeURIComponent(pair[1])); - } - } - return query_string; - }; - var prefixTestFile = function (file) { - if (file.indexOf("/") > -1) return file; - return "easy_gantt/" + file; - }; - it("should load extra tests if any", function () { - var params = getQueryString(); - var requestedTests = params["run_jasmine_tests"] || params["run_jasmine_tests[]"] || params["run_jasmine_tests%5B%5D"]; - if (requestedTests === "true") { - requestedTests = []; - } else if (typeof(requestedTests) === "string") { - requestedTests = [prefixTestFile(requestedTests)]; - } else if (typeof(requestedTests) === "object" && requestedTests.length !== undefined) { - requestedTests = requestedTests.map(function (requestedTest) { - return prefixTestFile(requestedTest); - }) - } else { - throw "Wrong type of run_jasmine_tests - \""+requestedTests+"\" is not true|string|Array<String>"; - } - var extraTests = jasmine.ysyInstance.tests.extraTestNames; - if (requestedTests.length > extraTests.length) { - for (var i = 0; i < requestedTests.length; i++) { - expect(extraTests).toContain(requestedTests[i], "extraTests missing " + requestedTests[i]); - } - return; - } - expect(requestedTests.length).toBe(extraTests.length, "requested tests !== loaded tests"); - for (i = 0; i < extraTests.length; i++) { - expect(requestedTests).toContain(extraTests[i]); - } - - }); -}); \ No newline at end of file diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/parse_form.js b/plugins/easy_mindmup/assets/javascripts/jasmine/parse_form.js deleted file mode 100644 index 3693e5f..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/parse_form.js +++ /dev/null @@ -1,87 +0,0 @@ -describe("parseForm", function () { - it("should convert FormArray to json", function () { - var formData = [{"name": "utf8", "value": "âś“"}, { - "name": "_method", - "value": "patch" - }, { - "name": "authenticity_token", - "value": "AyUhWxdKyCnB5V5FgkMZecRtDucWOsFRAq+RmxhPjclTrjNy3VngNdHp5tiS+iVqOqp4+7PXyrJNrDAX2rWGmA==" - }, {"name": "form_update_triggered_by", "value": ""}, { - "name": "issue[subject]", - "value": "level 1d" - }, {"name": "issue[tracker_id]", "value": "15"}, { - "name": "issue[author_id]", - "value": "5" - }, {"name": "issue[fixed_version_id]", "value": ""}, { - "name": "issue[old_fixed_version_id]", - "value": "" - }, {"name": "issue[parent_issue_id]", "value": ""}, { - "name": "issue[parent_issue_id]", - "value": "" - }, {"name": "issue[start_date]", "value": "2016-12-08"}, { - "name": "issue[easy_repeat_settings][simple_period]", - "value": "" - }, { - "name": "issue[easy_repeat_settings][end_date]", - "value": "" - }, { - "name": "issue[easy_repeat_settings][endtype_count_x]", - "value": "" - }, {"name": "issue[custom_field_values][29][]", "value": "material2"}, { - "name": "issue[custom_field_values][29][]", - "value": "material4" - }, {"name": "issue[custom_field_values][29][]", "value": ""}, { - "name": "issue[custom_field_values][83]", - "value": "" - }, {"name": "issue[status_id]", "value": "1"}, { - "name": "issue[done_ratio]", - "value": "0" - }, {"name": "issue[priority_id]", "value": "9"}, {"name": "issue[due_date]", "value": ""}, { - "name": "issue[notes]", - "value": "" - }, {"name": "version[project_id]", "value": "118"}, { - "name": "issue[private_notes]", - "value": "0" - }, {"name": "issue[private_notes]", "value": "1"}, { - "name": "issue[update_repeat_entity_attributes]", - "value": "1" - }, {"name": "issue[lock_version]", "value": "4"}]; - var json = { - "utf8": "âś“", - "_method": "patch", - "authenticity_token": "AyUhWxdKyCnB5V5FgkMZecRtDucWOsFRAq+RmxhPjclTrjNy3VngNdHp5tiS+iVqOqp4+7PXyrJNrDAX2rWGmA==", - "form_update_triggered_by": "", - "issue": { - "subject": "level 1d", - "tracker_id": "15", - "author_id": "5", - "fixed_version_id": "", - "old_fixed_version_id": "", - "parent_issue_id": "", - "start_date": "2016-12-08", - "easy_repeat_settings": { - "simple_period": "", - "end_date": "", - "endtype_count_x": "" - }, - "custom_field_values": { - "29": ["material2", "material4", ""], - "83": "" - }, - "status_id": "1", - "done_ratio": "0", - "priority_id": "9", - "due_date": "", - "notes": "", - "private_notes": "1", - "update_repeat_entity_attributes": "1", - "lock_version": "4" - }, - "version": { - "project_id": "118" - } - }; - var result = jasmine.ysyInstance.util.formToJson(formData); - expect(result).toEqual(json); - }); -}); diff --git a/plugins/easy_mindmup/assets/javascripts/jasmine/saver_linearize.js b/plugins/easy_mindmup/assets/javascripts/jasmine/saver_linearize.js deleted file mode 100644 index 92c1073..0000000 --- a/plugins/easy_mindmup/assets/javascripts/jasmine/saver_linearize.js +++ /dev/null @@ -1,43 +0,0 @@ -describe("Saver linearize", function () { - it("should return [] for only project", function () { - var ysy = jasmine.ysyInstance; - var project = new window.easyMindMupClasses.RootIdea(ysy); - project.fromServer(5, "Project A", "project", false, {id: 5}); - var result = []; - ysy.saver.linearizeTree(project, null, null, result, true); - expect(result).toEqual([]); - }); - it("should return 1 unsafe pack for 1 issue if unsafe", function () { - var ysy = jasmine.ysyInstance; - var project = new window.easyMindMupClasses.RootIdea(ysy); - project.fromServer(5, "Project A", "project", false, {id: 5}); - var issue = new window.easyMindMupClasses.ModelEntity(ysy); - issue.fromServer(6, "Issue A", "issue", true, { - id: 6, project_id: 5, - subject: "Issue A" - }); - project.ideas = {5: issue}; - var result = []; - ysy.saver.linearizeTree(project, null, null, result, true); - expect(result.length).toEqual(1); - var pack = result[0]; - expect(pack.node).toBe(issue); - expect(pack.parent).toBe(project); - expect(pack.isSame).toBe(false); - expect(pack.isSafe).toBe(false); - }); - it("should return 0 packs for 1 unchanged issue if safe", function () { - var ysy = jasmine.ysyInstance; - var project = new window.easyMindMupClasses.RootIdea(ysy); - project.fromServer(5, "Project A", "project", false, {id: 5}); - var issue = new window.easyMindMupClasses.ModelEntity(ysy); - issue.fromServer(6, "Issue A", "issue", true, { - id: 6, project_id: 5, - subject: "Issue A" - }); - project.ideas = {5: issue}; - var result = []; - ysy.saver.linearizeTree(project, null, null, result, false); - expect(result.length).toEqual(0); - }); -}); \ No newline at end of file diff --git a/plugins/easy_mindmup/assets/javascripts/layout_patch.js b/plugins/easy_mindmup/assets/javascripts/layout_patch.js index 1f694f0..305a038 100644 --- a/plugins/easy_mindmup/assets/javascripts/layout_patch.js +++ b/plugins/easy_mindmup/assets/javascripts/layout_patch.js @@ -16,7 +16,7 @@ self.preComputeDimensions(contentAggregate, self, ysy); return MAPJS.calculateLayout(contentAggregate, function (idea) { return self.nodeCacheMarks[idea.id] - }); + }, undefined, self.layoutPredicate); }; jQuery.fn.queueFadeOut = function (options) { var element = this; @@ -27,6 +27,27 @@ }, options)); } }; + /** + * function which separate nodes into two categories: + * LeftTree => positive = false + * RightTree => positive = true; + * @param {RootIdea} idea + * @param {boolean} positive + * @return {Function} + */ + LayoutPatch.prototype.layoutPredicate = function (idea, positive) { + if (idea.oneSideOn) { + return function () { + return positive; + } + } + return function (rank, parent) { + if (parent === idea) { + if (rank < 0 === positive) return false; + } + return true; + }; + }; /** * * @param {RootIdea} superIdea @@ -51,8 +72,7 @@ var bigHtml = '<div id="dimension_compute_cont">'; for (var i = 0; i < nodes.length; i++) { var idea = nodes[i]; - var text = ysy.util.escapeHtml(ysy.nodePatch.getNodeText(idea)); - bigHtml += '<div id="compute_node_' + idea.id + '" class="mapjs-node" style="visibility: hidden;position: absolute"><span>' + text + '</span></div>' + bigHtml += '<div id="compute_node_' + idea.id + '" class="mapjs-node" style="visibility: hidden;position: absolute"><span>' + ysy.nodePatch.getNodeText(idea) + '</span></div>' } bigHtml += '</div>'; $(bigHtml).appendTo('body'); diff --git a/plugins/easy_mindmup/assets/javascripts/legend.js b/plugins/easy_mindmup/assets/javascripts/legend.js index 64018a4..0fa54ab 100644 --- a/plugins/easy_mindmup/assets/javascripts/legend.js +++ b/plugins/easy_mindmup/assets/javascripts/legend.js @@ -169,9 +169,7 @@ <div data-item_id="{{item.id}}" class="mindmup-legend-item-cont">\ <div class="mindmup-legend-color-box{{banned}}{{scheme}}"></div>\ {{#avatar}}\ - <span class="avatar-container">\ <img width="64" height="64" alt="{{item.name}}" class="gravatar" src="{{{avatarUrl}}}">\ - </span>\ {{/avatar}}\ {{item.name}}\ </div>'; diff --git a/plugins/easy_mindmup/assets/javascripts/legend_events.js b/plugins/easy_mindmup/assets/javascripts/legend_events.js index 11ef9c1..dcb9a4e 100644 --- a/plugins/easy_mindmup/assets/javascripts/legend_events.js +++ b/plugins/easy_mindmup/assets/javascripts/legend_events.js @@ -35,7 +35,7 @@ ysy.$menu.find(".mindmup__legend-trigger").on("click", function (e) { legend.toggle(); }); - ysy.$menu.find(this.legendHeaderTogglerSelector).on("click", function () { + ysy.$menu.find(this.legendHeaderTogglerSelector).on("click",function () { legend.headerToggle(); }); this.$element @@ -90,9 +90,7 @@ }) .off("click." + this.domain) .on("click." + this.domain, this.filterSelector, function () { - var id = $(this).data("item_id"); - ysy.filter.toggleAllowed(id); - ysy.repainter.redrawMe(ysy.legends); + ysy.util.showUpgradeModal("filtering"); }); ysy.eventBus.register('resize', $.proxy(legend.resize, legend)); }; @@ -104,9 +102,7 @@ */ LegendEvents.prototype.down = function ($sourceElement, x, y) { this.maxDistance = 0; - var $stageElement = this.ysy.$container.children("[data-mapjs-role=\"stage\"]"); - this.stageData = $stageElement.data(); - this.stageOffset = $stageElement.offset(); + this.stageOffset = this.ysy.$container.children("[data-mapjs-role=\"stage\"]").offset(); this.startX = x - window.scrollX; this.startY = y - window.scrollY; // this.startTime = Date.now(); @@ -167,15 +163,12 @@ } if (this.avatar) { this.avatar.moveAvatar(this.actualX, this.actualY); - var oldDropTarget = this.currentDropTarget; + if (this.currentDropTarget) { + this.currentDropTarget.removeClass(this.hoverClass); + } this.currentDropTarget = this.getCurrentTarget(x, y); - if (this.currentDropTarget !== oldDropTarget) { - if (oldDropTarget) { - oldDropTarget.removeClass(this.hoverClass); - } - if (this.currentDropTarget) { - this.currentDropTarget.addClass(this.hoverClass); - } + if (this.currentDropTarget) { + this.currentDropTarget.addClass(this.hoverClass); } } }; @@ -189,11 +182,9 @@ this.actionActive = false; if (this.moveDistance() < this.maxDistanceToClick) { - var id = this.$sourceElement.data("item_id"); - this.ysy.filter.toggleAllowed(id); - this.ysy.repainter.redrawMe(this.legend); + this.ysy.util.showUpgradeModal("filtering"); } else { - this._drop(); + this.ysy.util.showUpgradeModal("dnd_property"); } if (this.$overlay) { if (this.avatar) { @@ -230,29 +221,16 @@ for (var i = 0; i < $possibles.length; i++) { var $possible = $possibles[i]; var data = $possible.data(); - var transformedX = (x - this.stageOffset.left) / this.stageData.scale; + var transformedX = x - this.stageOffset.left; if (transformedX < data.x) continue; if (transformedX > data.x + data.width) continue; - var transformedY = (y - this.stageOffset.top) / this.stageData.scale; + var transformedY = y - this.stageOffset.top; if (transformedY < data.y) continue; if (transformedY > data.y + data.height) continue; return $possible; } return null; }; - /** - * run at the end of drag event - * @private - */ - LegendEvents.prototype._drop = function () { - if (!this.currentDropTarget) return; - var ysy = this.ysy; - var idea = ysy.mapModel.findIdeaById(this.currentDropTarget.attr("id").split("_")[1]); - if (ysy.setData(idea, this.changeObject)) { - ysy.mapModel.selectNode(idea.id); - ysy.idea.dispatchEvent('changed'); - } - }; LegendEvents.prototype.moveDistance = function () { if (this.actualX == null) { return 0; @@ -265,7 +243,6 @@ }; window.easyMindMupClasses.LegendEvents = LegendEvents; - //#################################################################################################################### /** * diff --git a/plugins/easy_mindmup/assets/javascripts/loader.js b/plugins/easy_mindmup/assets/javascripts/loader.js index 0920166..58c9578 100644 --- a/plugins/easy_mindmup/assets/javascripts/loader.js +++ b/plugins/easy_mindmup/assets/javascripts/loader.js @@ -22,10 +22,6 @@ */ Loader.prototype.load = function () { // var self = this; - this.ysy.storage.clear(); - if (this.ysy.idea) { - this.ysy.idea.resetHistory(); - } // var last = this.ysy.storage.lastState.getSavedIdea(); // if (last) { // var storedDeferred = this.openStoredModal(last); @@ -63,8 +59,8 @@ if (this.ysy.idea) { return this._updateIdeaByData(this.ysy.idea, data); } + this.ysy.storage.extra.positionExtract = data["layout"]; // position of all nodes var convertedData = this.convertData(data); - this.ysy.storage.extra.setLayout(data["layout"]); // position of all nodes var enhancedData = this.ysy.storage.extra.enhanceData(convertedData, convertedData[this._rootId]); var links = this.ysy.links.convertRelations(data, enhancedData); var rearranged = this.rearrangeData(enhancedData); @@ -73,6 +69,7 @@ var initedData = MAPJS.content(rearranged); // var diff = ysy.storage.lastState.compareIdea(initedData, 'server'); // this.prepareLastStateMessages(diff, last, initedData); + this.ysy.storage.settings.load(initedData); this.ysy.eventBus.fireEvent("IdeaConstructed", initedData); this.setIdea(initedData); }; @@ -84,12 +81,12 @@ */ Loader.prototype._updateIdeaByData = function (idea, data) { var convertedData = this.convertData(data); - this.ysy.storage.extra.setLayout(data["layout"]); // position of all nodes var enhancedData = this.ysy.storage.extra.enhanceData(convertedData, convertedData[this._rootId]); var links = this.ysy.links.convertRelations(data, enhancedData); var rearranged = this.rearrangeData(enhancedData, idea); var initedData = MAPJS.content(rearranged); idea.ideas = initedData.ideas; + idea.resetHistory(); this.ysy.links.attachLinks(idea, links); this.ysy.eventBus.fireEvent("IdeaConstructed", idea); this.ysy.eventBus.fireEvent("TreeUpdated", idea); @@ -211,14 +208,12 @@ * @return {RootIdea} */ Loader.prototype.rearrangeData = function (convertedEntities, oldIdea) { - /** @type {Array.<ModelEntity>} rootChildren */ - var rootChildren = []; + var detachedIssues = []; + var index = 0; /** @type {ModelEntity} entity */ var entity; /** @type {ModelEntity} */ var parent; - /** @type {RootIdea}*/ - var root = /** @type {RootIdea}*/ convertedEntities[this._rootId]; var entitiesToProcess; if (oldIdea) { var idLookup = {}; @@ -240,104 +235,60 @@ parent = entity.parent; if (!parent) continue; var parentIdeas = parent.ideas; + parent.nChild++; if (entity.rank) { if (parentIdeas[entity.rank]) { - var detachedEntity = parentIdeas[entity.rank]; - parentIdeas[entity.rank] = entity; - while (parent.nextChildIndex === 0 || parentIdeas[parent.nextChildIndex]) { - parent.nextChildIndex++; - } - parentIdeas[parent.nextChildIndex++] = detachedEntity; - delete entity.rank; - continue; - } else { - if (parent.nextChildIndex === 0) parent.nextChildIndex = 1; - parentIdeas[entity.rank] = entity; + detachedIssues.push(parentIdeas[entity.rank]) } + parentIdeas[entity.rank] = entity; delete entity.rank; } else { if (parent.id === this._rootId) { - rootChildren.push(entity); + index = parent.nChild % 2 === 0 ? -parent.nChild / 2 : parent.nChild / 2 + 0.5; + //console.log("index "+index + " nChild "+parent.nChild); + if (parentIdeas[index]) { + detachedIssues.push(entity); + } else { + parentIdeas[index] = entity; + } } else { - while (parent.nextChildIndex === 0 || parentIdeas[parent.nextChildIndex]) { - parent.nextChildIndex++; + if (parentIdeas[parent.nChild]) { + detachedIssues.push(entity); + } else { + parentIdeas[parent.nChild] = entity; } - parentIdeas[parent.nextChildIndex++] = entity; + } + } + if (parent.id === this._rootId) { + var counter = 0; + index = 0; + while (detachedIssues.length > 0) { + counter++; + index = index > 0 ? index - counter : index + counter; + if (parentIdeas[index]) continue; + parentIdeas[index] = detachedIssues.shift(); + } + } else { + index = 0; + while (detachedIssues.length > 0) { + index++; + if (parentIdeas[index]) continue; + parentIdeas[index] = detachedIssues.shift(); } } } - - this._fillRootChildren(root, rootChildren); - for (id = 1; id < convertedEntities.length; id++) { entity = convertedEntities[id]; delete entity.parent; if (entity.attr.collapsed === undefined) { if (id === this._rootId) continue; - if (entity.nextChildIndex) { + if (entity.nChild) { entity.attr.collapsed = true; } - } - delete entity.nextChildIndex; - } - return root; - }; - /** - * - * @param {RootIdea} idea - * @param {Array.<ModelEntity>} children - * @private - */ - Loader.prototype._fillRootChildren = function (idea, children) { - if (children.length === 0){ - this.ysy.contentPatch.updateOneSide(idea); - return; - } - var parentIdeas = idea.ideas; - var ranks = Object.getOwnPropertyNames(idea.ideas).map(function (i) { - return parseInt(i); - }).sort(function (a, b) { - return a - b - }); - var positiveIndex = 1; - var nextPositive = function () { - while (parentIdeas[positiveIndex]) { - positiveIndex++; - } - return positiveIndex; - }; - if(this.ysy.settings.oneSideOn){ - var firstPositive = _.findIndex(ranks, function (rank) { - return rank > 0; - }); - for (i = 0; i < children.length; i++) { - parentIdeas[nextPositive()]=children[i]; - } - for (i = 0; i < firstPositive; i++) { - parentIdeas[nextPositive()] = parentIdeas[ranks[i]]; - delete parentIdeas[ranks[i]]; - } - } - var halfCount = Math.ceil((ranks.length + children.length) / 2); - var positives = 0; - var negativeIndex = -1; - for (var i = 0; i < ranks.length; i++) { - if (ranks[i] > 0) { - positives++; - } - } - for (i = 0; i < children.length; i++) { - if (halfCount >= positives) { - parentIdeas[nextPositive()] = children[i]; - positives++; - } else { - while (parentIdeas[negativeIndex]) { - negativeIndex--; - } - parentIdeas[negativeIndex] = children[i]; - negativeIndex--; + delete entity.nChild; } } + return /** @type {RootIdea}*/ convertedEntities[this._rootId]; }; /** * @@ -347,23 +298,22 @@ */ Loader.prototype.rearrangeByIdea = function (oldIdea, newIdea, idLookup) { if (!_.isEmpty(oldIdea.ideas)) { - var oldRanks = Object.getOwnPropertyNames(oldIdea.ideas); + var ranks = Object.getOwnPropertyNames(oldIdea.ideas); var oldParentId = this.ysy.getData(oldIdea).id; - for (var i = 0; i < oldRanks.length; i++) { - var rank = oldRanks[i]; + for (var i = 0; i < ranks.length; i++) { + var rank = ranks[i]; var oldChild = oldIdea.ideas[rank]; var oldChildId = this.ysy.getData(oldChild).id; if (!oldChildId) continue; var newChild = idLookup[oldChildId]; if (!newChild) continue; var newParentId = this.ysy.getData(newChild.parent).id; - if (oldParentId !== newParentId) continue; + if(oldParentId !== newParentId) continue; if (!newIdea.ideas) { newIdea.ideas = {}; } newIdea.ideas[rank] = newChild; - if (!newIdea.nextChildIndex) newIdea.nextChildIndex++; - newIdea.nextChildIndex++; + newIdea.nChild++; this.rearrangeByIdea(oldChild, newChild, idLookup); } } diff --git a/plugins/easy_mindmup/assets/javascripts/main.js b/plugins/easy_mindmup/assets/javascripts/main.js index ddcdeef..db481e1 100644 --- a/plugins/easy_mindmup/assets/javascripts/main.js +++ b/plugins/easy_mindmup/assets/javascripts/main.js @@ -30,8 +30,6 @@ this.log = new easyMindMupClasses.Logger(this); /** @type {Repainter} */ this.repainter = new easyMindMupClasses.Repainter(this); - /** @type {JasmineTests} */ - this.tests = easyMindMupClasses.JasmineTests && new easyMindMupClasses.JasmineTests(this); }; MindMup.prototype.init = function () { if (!this.helperInited) { diff --git a/plugins/easy_mindmup/assets/javascripts/map_model_patch.js b/plugins/easy_mindmup/assets/javascripts/map_model_patch.js index c36cb5b..8f7a497 100644 --- a/plugins/easy_mindmup/assets/javascripts/map_model_patch.js +++ b/plugins/easy_mindmup/assets/javascripts/map_model_patch.js @@ -56,10 +56,10 @@ }; mapModel.toggleOneSide = function (source) { var idea = ysy.idea; - var targetState = !self.ysy.settings.oneSideOn; - self.ysy.settings.oneSideOn = targetState; - idea.updateOneSide(idea); - self.ysy.eventBus.fireEvent("saveOneSideOn", targetState); + idea.oneSideOn = !idea.oneSideOn; + //idea.toggleOneSide(); + idea.dispatchEvent("changed"); + mapModel.dispatchEvent("saveSettings", idea); }; /** prevent scroll jumping after deselecting node while editing */ mapModel.addEventListener('inputEnabledChanged', function (canInput, holdFocus) { diff --git a/plugins/easy_mindmup/assets/javascripts/mapjs_init.js b/plugins/easy_mindmup/assets/javascripts/mapjs_init.js index 3984ac6..e95a24f 100644 --- a/plugins/easy_mindmup/assets/javascripts/mapjs_init.js +++ b/plugins/easy_mindmup/assets/javascripts/mapjs_init.js @@ -13,11 +13,11 @@ * @param {MindMup} ysy */ MMInitiator.prototype.init = function (ysy) { - // var imageInsertController = new MAPJS.ImageInsertController("http://localhost:4999?u="); + var imageInsertController = new MAPJS.ImageInsertController("http://localhost:4999?u="); var mapModel = new MAPJS.MapModel(ysy.layoutPatch.layoutCalculator, []); - jQuery(ysy.$container).domMapWidget(console, mapModel, false, null, undefined, ysy); + jQuery(ysy.$container).domMapWidget(console, mapModel, false, imageInsertController, undefined, ysy); jQuery(ysy.$menu).mapToolbarWidget(mapModel, ysy); - MAPJS.DOMRender.stageMargin = {top: 300, left: 300, bottom: 300, right: 300}; + MAPJS.DOMRender.stageMargin = {top: 50, left: 50, bottom: 50, right: 50}; MAPJS.DOMRender.linkConnectorPath = ysy.links.outerPath; MAPJS.DOMRender.nodeConnectorPath = ysy.domPatch.curvedPath; ysy.mapModel = mapModel; @@ -31,3 +31,62 @@ window.easyMindMupClasses.MMInitiator = MMInitiator; })(); + + +MAPJS.initAll = function (container) { + //jQuery.fn.attachmentEditorWidget = function (mapModel) { + // 'use strict'; + // return this.each(function () { + // var element = jQuery(this); + // mapModel.addEventListener('attachmentOpened', function (nodeId, attachment) { + // mapModel.setAttachment( + // 'attachmentEditorWidget', + // nodeId, { + // contentType: 'text/html', + // content: prompt('attachment', attachment && attachment.content) + // } + // ); + // }); + // }); + //}; + + + window.onerror = ysy.log.error; + // var container = jQuery('#container'), + //idea = MAPJS.content(test_tree()), + var imageInsertController = new MAPJS.ImageInsertController("http://localhost:4999?u="), + mapModel = new MAPJS.MapModel(MAPJS.DOMRender.layoutCalculator, []); + jQuery(container).domMapWidget(console, mapModel, false, imageInsertController, undefined, ysy); + jQuery('#wbs_menu').mapToolbarWidget(mapModel); + //jQuery('body').attachmentEditorWidget(mapModel); + //$("[data-mm-action='export-image']").click(function () { + // MAPJS.pngExport(idea).then(function (url) { + // window.open(url, '_blank'); + // }); + //}); + //mapModel.setIdea(idea); // < HOSEK + MAPJS.DOMRender.stageMargin = {top: 50, left: 50, bottom: 50, right: 50}; + MAPJS.DOMRender.linkConnectorPath = MAPJS.DOMRender.outerPath; + ysy.mapjs.mapModel = mapModel; + //jQuery('.arrow').click(function () { + // jQuery(this).toggleClass('active'); + //}); + imageInsertController.addEventListener('imageInsertError', function (reason) { + ysy.log.error('image insert error', reason); + }); + //container.on('drop', function (e) { + // var dataTransfer = e.originalEvent.dataTransfer; + // e.stopPropagation(); + // e.preventDefault(); + // if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { + // var fileInfo = dataTransfer.files[0]; + // if (/\.mup$/.test(fileInfo.name)) { + // var oFReader = new FileReader(); + // oFReader.onload = function (oFREvent) { + // mapModel.setIdea(MAPJS.content(JSON.parse(oFREvent.target.result))); + // }; + // oFReader.readAsText(fileInfo, 'UTF-8'); + // } + // } + //}); +}; diff --git a/plugins/easy_mindmup/assets/javascripts/mindmup/dom-map-widget.js b/plugins/easy_mindmup/assets/javascripts/mindmup/dom-map-widget.js index 5f19274..be6b7dd 100644 --- a/plugins/easy_mindmup/assets/javascripts/mindmup/dom-map-widget.js +++ b/plugins/easy_mindmup/assets/javascripts/mindmup/dom-map-widget.js @@ -106,7 +106,7 @@ $.fn.domMapWidget = function (activityLog, mapModel, touchEnabled, imageInsertCo element.css('overflow', 'auto'); } }); - //element.imageDropWidget(imageInsertController); + element.imageDropWidget(imageInsertController); } else { element.on('doubletap', function (event) { if (mapModel.requestContextMenu(event.gesture.center.pageX, event.gesture.center.pageY)) { diff --git a/plugins/easy_mindmup/assets/javascripts/mindmup/layout.js b/plugins/easy_mindmup/assets/javascripts/mindmup/layout.js index eca20aa..080f4f9 100644 --- a/plugins/easy_mindmup/assets/javascripts/mindmup/layout.js +++ b/plugins/easy_mindmup/assets/javascripts/mindmup/layout.js @@ -266,7 +266,7 @@ MAPJS.calculateTree = function (content, dimensionProvider, margin, rankAndParen includedSubIdeaKeys = function () { var allRanks = _.map(_.keys(content.ideas), parseFloat), includedRanks = rankAndParentPredicate ? _.filter(allRanks, function (rank) { - return rankAndParentPredicate(rank, content.id); + return rankAndParentPredicate(rank, content); }) : allRanks; return _.sortBy(includedRanks, Math.abs); }, @@ -320,21 +320,17 @@ MAPJS.calculateTree = function (content, dimensionProvider, margin, rankAndParen return new MAPJS.Tree(options); }; -MAPJS.calculateLayout = function (idea, dimensionProvider, margin) { +MAPJS.calculateLayout = function (idea, dimensionProvider, margin,layoutPredicate) { 'use strict'; - var positiveTree, negativeTree, layout, negativeLayout, + var positiveTree, negativeTree, layout, negativeLayout, positive, negative, setDefaultStyles = function (nodes) { _.each(nodes, function (node) { node.attr = node.attr || {}; node.attr.style = _.extend({}, MAPJS.defaultStyles[(node.level === 1) ? 'root' : 'nonRoot'], node.attr.style); }); - }, - positive = function (rank, parentId) { - return parentId !== idea.id || rank > 0; - }, - negative = function (rank, parentId) { - return parentId !== idea.id || rank < 0; }; + positive = layoutPredicate(idea, true); + negative = layoutPredicate(idea, false); margin = margin || 20; positiveTree = MAPJS.calculateTree(idea, dimensionProvider, margin, positive); negativeTree = MAPJS.calculateTree(idea, dimensionProvider, margin, negative); diff --git a/plugins/easy_mindmup/assets/javascripts/mm_context_menu.js b/plugins/easy_mindmup/assets/javascripts/mm_context_menu.js index 66d1a8c..5b0c73d 100644 --- a/plugins/easy_mindmup/assets/javascripts/mm_context_menu.js +++ b/plugins/easy_mindmup/assets/javascripts/mm_context_menu.js @@ -26,7 +26,7 @@ if ($element.length === 0) { $element = $('<div id="context-menu" class="mindmup__context_menu ' + (this.ysy.settings.easyRedmine ? "easy" : "redmine") + '"></div>') - .appendTo('#content').hide(); + .appendTo('body').hide(); } this.$element = $element; var self = this; @@ -108,52 +108,14 @@ */ ContextMenu.prototype.bindEvents = function (primaryNode, $element) { var ysy = this.ysy; - var self = this; var mapModel = ysy.mapModel; $element.mapToolbarWidget(mapModel, ysy); $element.find(".mindmup-data-value-changer:not(.disabled)").on('tap click', function () { - var $this = $(this); - var obj = {}; - obj[$this.data("key")] = $this.data("value"); - var activatedIds = ysy.mapModel.getActivatedNodeIds(); - for (var i = 0; i < activatedIds.length; i++) { - var node = ysy.idea; - if (activatedIds[i] !== 1) { - node = node.findSubIdeaById(activatedIds[i]); - } - if (node.attr.entityType !== primaryNode.attr.entityType) continue; - ysy.setData(node, obj); - } - //ysy.mapModel.setData(primaryNode, obj); - ysy.idea.dispatchEvent('changed'); + ysy.util.showUpgradeModal("context_menu"); }); $element.find(".mindmup-data-value-input-link:not(.disabled)").on('tap click', function () { - var $this = $(this); - $this.hide(); - var $input = $this.parent().find(".mindmup-data-value-input"); - if ($input.length === 0) return; - $input.show().focus(); - $input.on("change", function () { - var obj = {}; - var newValue = $input.val(); - if ($input.attr("type") === "number") { - newValue = parseFloat(newValue); - } - obj[$input.data("key")] = newValue; - var activatedIds = ysy.mapModel.getActivatedNodeIds(); - for (var i = 0; i < activatedIds.length; i++) { - var node = ysy.mapModel.findIdeaById(activatedIds[i]); - if (node.attr.entityType !== primaryNode.attr.entityType) continue; - ysy.setData(node, obj); - } - self.innerHide(); - ysy.idea.dispatchEvent('changed'); - - }); + ysy.util.showUpgradeModal("context_menu"); return false; - // var obj = {}; - // obj[$this.data("key")] = $this.data("value"); - //ysy.mapModel.setData(primaryNode, obj); }); }; ContextMenu.prototype.window_size = function () { diff --git a/plugins/easy_mindmup/assets/javascripts/model_classes.js b/plugins/easy_mindmup/assets/javascripts/model_classes.js index 25525d7..6f6f86a 100644 --- a/plugins/easy_mindmup/assets/javascripts/model_classes.js +++ b/plugins/easy_mindmup/assets/javascripts/model_classes.js @@ -97,7 +97,7 @@ * Universal entity from which model tree is generated * @property {number} id * @property {String} title - * @property {number} nextChildIndex + * @property {number} nChild * @property {Object.<String, ModelEntity>} ideas * @property {ModelEntityAttr} attr * @property {ModelEntity} parent - temporary = used only for generating of model tree and saving @@ -108,7 +108,7 @@ function ModelEntity() { this.id = 0; this.title = "No title"; - this.nextChildIndex = 0; + this.nChild = 0; this.ideas = {}; this.attr = new ModelEntityAttr(); this.parent = null; @@ -206,8 +206,6 @@ }; RootIdea.prototype.resetHistory = function () { }; - RootIdea.prototype.updateOneSide = function () { - }; window.easyMindMupClasses.RootIdea = RootIdea; diff --git a/plugins/easy_mindmup/assets/javascripts/node_patch.js b/plugins/easy_mindmup/assets/javascripts/node_patch.js index a2b0aa3..eb5fcb0 100644 --- a/plugins/easy_mindmup/assets/javascripts/node_patch.js +++ b/plugins/easy_mindmup/assets/javascripts/node_patch.js @@ -46,7 +46,7 @@ if (assigneeIndex > -1) { var user = users[assigneeIndex]; var avatarUrl = user.avatar_url ? user.avatar_url : "/plugin_assets/easy_extensions/images/avatar.jpg"; - return '<span class="avatar-container"><img width="64" height="64" alt="' + user.name + '" class="gravatar" src="' + avatarUrl + '"></span>'; + return '<img width="64" height="64" alt="' + user.name + '" class="gravatar" src="' + avatarUrl + '">'; } }); }; @@ -70,7 +70,8 @@ var getNodeText = function (nodeContent) { var MAX_URL_LENGTH = 25; var title = nodeContent.title; - var text = title.length < MAX_URL_LENGTH ? title : (title.substring(0, MAX_URL_LENGTH) + '...'); + var text = MAPJS.URLHelper.stripLink(title) || + (title.length < MAX_URL_LENGTH ? title : (title.substring(0, MAX_URL_LENGTH) + '...')); return text.trim(); }; NodePatch.prototype.getNodeText = getNodeText; diff --git a/plugins/easy_mindmup/assets/javascripts/print.js b/plugins/easy_mindmup/assets/javascripts/print.js index 4357270..54127c0 100644 --- a/plugins/easy_mindmup/assets/javascripts/print.js +++ b/plugins/easy_mindmup/assets/javascripts/print.js @@ -30,14 +30,13 @@ window.print(); this.afterPrint(); }; - Print.prototype.beforePrint = function () { + Print.prototype.beforePrint = function (isCompact) { if (this.printReady) return; this.ysy.mapModel.resetView(); - this.$area = this.createPrintArea(); + this.$area = isCompact? this.createCompactArea():this.createPrintArea(); $("body").append(this.$area); $("#wrapper").hide(); this.printReady = true; - return this.$area; }; Print.prototype.afterPrint = function () { if (!this.printReady) return; @@ -53,9 +52,11 @@ var stripWidth = 330; var children = $stage.children(":not(:hidden)"); var dims = this.getStageDims(children); - var $area = $('<div id="mindmup__print-area" class="mindmup__print-area mindmup__print-area--stripped scheme-by-' + this.ysy.styles.setting + '"></div>'); + var $area = $('<div class="mindmup-print-area scheme-by-' + this.ysy.styles.setting + '" \ + style="height:' + (dims.bottom - dims.top + this.margins.top + this.margins.bottom) + 'px"></div>'); for (var p = dims.left - this.margins.left; p < dims.right + this.margins.right; p += stripWidth) { $area.append(this.createStrip(children, dims, p, p + stripWidth)); + //p -= 2; } return $area; }; @@ -63,25 +64,26 @@ /* start can be negative*/ if (end <= start) return null; // var stageOffset = $stage.height(); - var $strip = $('<div class="mindmup__print-strip" style="height:' + (dims.bottom - dims.top + this.margins.top + this.margins.bottom) + 'px;width:' + (end - start) + 'px"></div>'); + var $strip = $('<div class="mindmup-print-strip" style="height:' + (dims.bottom - dims.top + this.margins.top + this.margins.bottom) + 'px;width:' + (end - start) + 'px"></div>'); // var children = $stage.children(":not(:hidden)"); var added = 0; - var topEdge = dims.top - this.margins.top; for (var i = 0; i < children.length; i++) { var child = children[i]; var left = parseInt(child.style.left); var width = child.offsetWidth; - if (left > end + 5) continue; - if (left + width < start - 5) continue; + var top = parseInt(child.style.top) - dims.top + this.margins.top; + var height = child.offsetHeight; + if (left > end) continue; + if (left + width < start) continue; added++; + var $child = $(child); + var transform = $child.css("transform"); + if (transform === "none") transform = ""; + transform = "translate(" + (left - start) + "px," + top + "px) " + transform; $strip.append( - $(child) + $child .clone() - .css({ - left: left - start, - top: parseInt(child.style.top) - topEdge, - width: width - }) + .css({left: 0, top: 0, transform: transform}) ); } if (!added) return null; @@ -107,6 +109,32 @@ } return dims; }; + Print.prototype.createCompactArea = function () { + var $stage = this.ysy.$container.children(); + var children = $stage.children(":not(:hidden)"); + var dims = this.getStageDims(children); + var leftEdge = dims.left - this.margins.left; + var $area = $('<div class="mindmup-pdf-print-area scheme-by-' + this.ysy.styles.setting + '" style="' + + 'height:' + (dims.bottom - dims.top + this.margins.top + this.margins.bottom) + 'px;' + + 'width:' + (dims.right - leftEdge + this.margins.right) + 'px"></div>'); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var left = parseInt(child.style.left); + var width = child.offsetWidth; + var top = parseInt(child.style.top) - dims.top + this.margins.top; + var height = child.offsetHeight; + var $child = $(child); + var transform = $child.css("transform"); + if (transform === "none") transform = ""; + transform = "translate(" + (left - leftEdge) + "px," + top + "px) " + transform; + $area.append( + $child + .clone() + .css({left: 0, top: 0, transform: transform}) + ); + } + return $area; + }; window.easyMindMupClasses.Print = Print; //#################################################################################################################### diff --git a/plugins/easy_mindmup/assets/javascripts/redrawer.js b/plugins/easy_mindmup/assets/javascripts/redrawer.js index 0fcd9a3..b9b4919 100644 --- a/plugins/easy_mindmup/assets/javascripts/redrawer.js +++ b/plugins/easy_mindmup/assets/javascripts/redrawer.js @@ -8,6 +8,14 @@ this.ysy = ysy; this.onRepaint = []; var self = this; + var requestAnimationFrame = (function () { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; + })(); var animationLoop = function () { var queue = self.onRepaint; if (queue.length > 0) { diff --git a/plugins/easy_mindmup/assets/javascripts/saver.js b/plugins/easy_mindmup/assets/javascripts/saver.js index 81222a7..4309313 100644 --- a/plugins/easy_mindmup/assets/javascripts/saver.js +++ b/plugins/easy_mindmup/assets/javascripts/saver.js @@ -238,7 +238,7 @@ Saver.prototype.saveLayout = function () { if (!this.layoutKey) throw "Missing layoutKey"; var self = this; - var layout = this.ysy.storage.extra.getLayout(); + var layout = this.ysy.storage.extra.positionExtract; var requestData = {easy_setting: {}}; requestData.easy_setting[this.layoutKey] = layout; var xhr = $.ajax({ diff --git a/plugins/easy_mindmup/assets/javascripts/storage.js b/plugins/easy_mindmup/assets/javascripts/storage.js index 4162465..cf859b9 100644 --- a/plugins/easy_mindmup/assets/javascripts/storage.js +++ b/plugins/easy_mindmup/assets/javascripts/storage.js @@ -4,7 +4,6 @@ * * @param {MindMup} ysy * @property {StorageExtra} extra - * @property {StorageLastState} lastState * @property {StorageSettings} settings * @constructor */ @@ -12,31 +11,18 @@ this._scope = ysy.id + "-"; this.ysy = ysy; this.extra = new StorageExtra(this); - var lastState = new StorageLastState(this); - var settings = new StorageSettings(this); - this.lastState = lastState; - this.settings = settings; + this.settings = new StorageSettings(this); var self = this; //if (this._scope == null) throw "_scope is not defined! Scope is used for separation of xBS products in localStorage"; ysy.eventBus.register("TreeLoaded", function (idea) { idea.addEventListener('changed', function () { - lastState.remove(); self.save(idea); }); }); } - Storage.prototype.getSessionData = function (key) { - return window.sessionStorage.getItem(this._scope + key); - }; - Storage.prototype.saveSessionData = function (key, value) { - window.sessionStorage.setItem(this._scope + key, value); - }; - Storage.prototype.resetSessionData = function (key) { - window.sessionStorage.removeItem(this._scope + key); - }; Storage.prototype.getPersistentData = function (key) { return window.localStorage.getItem(this._scope + key); }; @@ -48,10 +34,8 @@ }; Storage.prototype.save = function (idea) { this.extra.save(idea); - this.lastState.save(idea); }; Storage.prototype.clear = function () { - this.lastState.remove(); this.extra.positionExtract = null; }; window.easyMindMupClasses = window.easyMindMupClasses || {}; @@ -83,7 +67,6 @@ * @param {RootIdea} idea */ StorageExtra.prototype.save = function (idea) { - if (!idea) return; var extract = this._extractFromNode(idea); this.positionExtract = extract.positions; this.collapseExtract = extract.collapses; @@ -142,79 +125,6 @@ } return data; }; - /** - * @param {{code?:String}} layout - */ - StorageExtra.prototype.setLayout = function (layout) { - this.positionExtract = this._decodeLayout(layout); - }; - /** - * @return {{code?: String}} - */ - StorageExtra.prototype.getLayout = function () { - return this._encodeLayout(this.positionExtract); - }; - /** - * encode layout if it contains more than 50 keys. - * result is capped if it reaches 60000 characters - * @param {{}} decodedLayout - * @return {{code?:String}|null} - */ - StorageExtra.prototype._encodeLayout = function (decodedLayout) { - if(!decodedLayout) return null; - var keys = Object.getOwnPropertyNames(decodedLayout); - if (keys.length <= 50) return decodedLayout; - var i, key, pos, result = ""; - if (keys.length > 2000) { - for (i = 0; i < keys.length; i++) { - key = keys[i]; - pos = decodedLayout[key].position ? ("[" + decodedLayout[key].position + "]") : ""; - result += key + "{" + (decodedLayout[key].rank || "") + pos + "}"; - if (result.length > 60000) return {code: result}; - } - } else { - for (i = 0; i < keys.length; i++) { - key = keys[i]; - pos = decodedLayout[key].position ? ("[" + decodedLayout[key].position + "]") : ""; - result += key + "{" + (decodedLayout[key].rank || "") + pos + "}"; - } - - } - return {code: result}; - }; - /** - * decode layout if contains "code" key - * @param {{code?:String}} encodedLayout - * @return {{}|null} - */ - StorageExtra.prototype._decodeLayout = function (encodedLayout) { - if (!encodedLayout) return null; - if (!encodedLayout.code) return encodedLayout; - var issueStrings = encodedLayout.code.split("}"); - var result = {}; - for (var i = 0; i < issueStrings.length; i++) { - var issueString = issueStrings[i]; - var p = issueString.indexOf("{"); - if (p === -1) continue; - var issueResult = {}; - var key = issueString.substring(0, p); - result[key] = issueResult; - var k = issueString.indexOf("["); - var issueRank; - if (k > -1) { - var posString = issueString.substring(k); - issueResult.position = JSON.parse(posString); - issueRank = issueString.substring(p + 1, k); - } else { - issueRank = issueString.substring(p + 1); - } - if (issueRank) { - issueResult.rank = parseInt(issueRank); - } - } - return result; - }; - /** * * @param {ModelEntity} node @@ -249,177 +159,6 @@ return extract; }; //####################################################################################### - /** - * - * @param {Storage} storage - * @constructor - */ - function StorageLastState(storage) { - this.storage = storage; - this._binded = false; - this.ysy = storage.ysy; - } - - StorageLastState.prototype._dataKey = "last-data"; - StorageLastState.prototype._idKey = "last-id"; - StorageLastState.prototype.save = function (idea) { - // this.storage.savePersistentData(this._dataKey, JSON.stringify(idea)); - // this.storage.savePersistentData(this._idKey, this.ysy.settings.rootID); - // if (this._binded)return; - // this._binded = true; - // var storage = this.storage; - // $(window).unbind('beforeunload').bind('beforeunload', function (e) { - // var message = "Some changes are not saved!"; - // e.returnValue = message; - // return message; - // }).unbind('unload').bind('unload', function () { - // storage.lastState.remove(); - // }); - }; - /** - * - * @return {RootIdea} - */ - StorageLastState.prototype.getSavedIdea = function () { - var oldId = this.storage.getPersistentData(this._idKey); - if (oldId && parseInt(oldId) !== this.ysy.settings.rootID) { - this.remove(); - } - var ideaJson = this.storage.getPersistentData(this._dataKey); - if (!ideaJson) return null; - - var idea = new window.easyMindMupClasses.RootIdea(this.ysy).fromJson(JSON.parse(ideaJson)); - return MAPJS.content(idea); - }; - StorageLastState.prototype.remove = function () { - this.storage.resetPersistentData(this._dataKey); - this.storage.resetPersistentData(this._idKey); - if (this._binded) { - $(window).unbind('beforeunload'); - $(window).unbind('unload'); - this._binded = false; - } - }; - StorageLastState.prototype.compareIdea = function (idea, diffType, savedIdea) { - if (!savedIdea) { - savedIdea = this.getSavedIdea(); - if (savedIdea === null) { - return null; - } - } - return recursiveDiff(savedIdea, idea, { - softParams: diffType === 'all' || diffType === 'soft', - data: diffType === 'all' || diffType === 'data' || diffType === 'server', - structure: diffType === 'all' || diffType === 'structure' || diffType === 'server' - }); - }; - var recursiveDiff = function (oldIdea, newIdea, diffTypes) { - var isDifferent = false; - var oldDiff = {}; - var newDiff = {}; - var keys = ["title"]; - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - if (oldIdea[key] !== newIdea[key]) { - oldDiff[key] = oldIdea[key]; - newDiff[key] = newIdea[key]; - isDifferent = true; - } - } - if (diffTypes.data) { - var oldData = this.ysy.getData(oldIdea); - var newData = this.ysy.getData(newIdea); - keys = _.union(Object.getOwnPropertyNames(oldData), Object.getOwnPropertyNames(newData)); - for (i = 0; i < keys.length; i++) { - key = keys[i]; - if (typeof oldData[key] === "object" || typeof newData[key] === "object") { - continue; - } - if (oldData[key] !== newData[key]) { - if (oldDiff.attr === undefined) { - oldDiff.attr = {data: {}}; - newDiff.attr = {data: {}}; - } - oldDiff.attr.data[key] = oldData[key]; - newDiff.attr.data[key] = newData[key]; - isDifferent = true; - } - } - } - if (diffTypes.softParams) { - keys = ["collapsed", "position"]; - for (i = 0; i < keys.length; i++) { - key = keys[i]; - if (oldIdea.attr[key] !== newIdea.attr[key]) { - if (oldDiff.attr === undefined) { - oldDiff.attr = {}; - newDiff.attr = {}; - } - oldDiff.attr[key] = oldIdea.attr[key]; - newDiff.attr[key] = newIdea.attr[key]; - isDifferent = true; - } - } - } - if (_.isEmpty(oldIdea.ideas) && !_.isEmpty(newIdea.ideas)) { - newDiff.ideas = newIdea.ideas; - isDifferent = true; - } else if (_.isEmpty(newIdea.ideas) && !_.isEmpty(oldIdea.ideas)) { - oldDiff.ideas = oldIdea.ideas; - isDifferent = true; - } else { - var oldIdeas = $.extend({}, oldIdea.ideas); - var newIdeas = $.extend({}, newIdea.ideas); - var oldKeys = Object.getOwnPropertyNames(oldIdeas); - var newKeys = Object.getOwnPropertyNames(newIdeas); - for (var j = 0; j < oldKeys.length; j++) { - var oldKey = oldKeys[j]; - var oldSubIdea = oldIdea.ideas[oldKey]; - var oldSubIdeaId = this.ysy.getData(oldSubIdea).id; - for (var k = 0; k < newKeys.length; k++) { - var newKey = newKeys[k]; - if (!newIdeas.hasOwnProperty(newKey)) continue; - var newSubIdea = newIdea.ideas[newKey]; - var newSubIdeaId = this.ysy.getData(newSubIdea).id; - if (oldSubIdeaId !== newSubIdeaId) continue; - var diff = recursiveDiff(oldSubIdea, newSubIdea, diffTypes); - if (diff && diff.oldDiff) { - if (oldDiff.ideas === undefined) { - oldDiff.ideas = {}; - } - oldDiff.ideas[oldKey] = diff.oldDiff; - isDifferent = true; - } - if (diff && diff.newDiff) { - if (newDiff.ideas === undefined) { - newDiff.ideas = {}; - } - newDiff.ideas[newKey] = diff.newDiff; - isDifferent = true; - } - delete oldIdeas[oldKey]; - delete newIdeas[newKey]; - } - if (oldIdeas[oldKey]) { - if (!oldDiff.ideas) oldDiff.ideas = {}; - oldDiff.ideas[oldKey] = oldIdeas[oldKey]; - isDifferent = true; - } - } - for (newKey in newIdeas) { - if (!newIdeas.hasOwnProperty(newKey)) continue; - if (!newDiff.ideas) newDiff.ideas = {}; - newDiff.ideas[newKey] = newIdeas[newKey]; - isDifferent = true; - } - } - if (!isDifferent) return null; - var ret = {}; - if (!_.isEmpty(oldDiff)) ret.oldDiff = oldDiff; - if (!_.isEmpty(newDiff)) ret.newDiff = newDiff; - return ret; - }; -//###################################################################################### /** * * @param {Storage} storage @@ -427,126 +166,26 @@ * @constructor */ function StorageSettings(storage) { - this.storage = storage; - this.ysy = storage.ysy; - this.init(storage.ysy); } - - StorageSettings.prototype._key = "settings"; /** - * @param {MindMup} ysy + * @param {RootIdea} idea */ - StorageSettings.prototype.init = function (ysy) { - var self = this; - ysy.eventBus.register("saveOneSideOn",function(targetState){ - self.saveOneSide(targetState); - }); - ysy.eventBus.register("nodeStyleChanged", function () { - self.saveStyle(); - }); - ysy.eventBus.register("legendToggled", function (opened) { - self._save({legendHidden: !opened}, false, null); - }); - ysy.eventBus.register("legendHeaderToggled", function (hidden) { - self._save({legendHeaderHidden: hidden}, false, null); - }); - ysy.eventBus.register("BeforeServerClassInit",function () { - self.load(); - }); - }; - StorageSettings.prototype.load = function () { - this.ysy.settings.oneSideOn = this._load("oneSideOn", null); - this.ysy.toolbar.redraw("toggleOneSide"); + StorageSettings.prototype.load = function (idea) { }; StorageSettings.prototype.loadStyle = function () { - return this._load("defaultStyle", this.ysy.settings.rootID); + return undefined; }; StorageSettings.prototype.loadLegendHidden = function () { - return this._load("legendHidden", null); + return undefined }; StorageSettings.prototype.loadLegendHeaderHidden = function () { - return this._load("legendHeaderHidden", null); - }; - /** - * - * @param {String|Array.<String>} keys - * @param {String|null} rootKey - * @private - */ - StorageSettings.prototype._load = function (keys, rootKey) { - if (!rootKey) { - rootKey = this._key; - } else { - rootKey = this._key + '-' + rootKey; - } - var extractString = this.storage.getPersistentData(rootKey); - if (!extractString) return; - var extract = JSON.parse(extractString); - if (typeof keys === "string") { - return extract[keys]; - } - return _.pick(extract, keys); + return undefined }; /** - * @param {boolean} oneSideOn + * @param {RootIdea} idea */ - StorageSettings.prototype.saveOneSide = function (oneSideOn) { - this._save({oneSideOn: oneSideOn}, false, null); + StorageSettings.prototype.saveOneSide = function (idea) { }; StorageSettings.prototype.saveStyle = function () { - var stylesClass = this.ysy.styles; - this._save( - {defaultStyle: stylesClass.setting === stylesClass.defaultStyle ? undefined : stylesClass.setting}, - false, - this.ysy.settings.rootID - ); - }; - /** - * @param {Object} map - * @param {boolean} keepFalse - * @param {(string|null)} rootKey - * @param {boolean} [map.oneSideOn] - * @param {String} [map.defaultStyle] - * @param {boolean} [map.legendToggle] - */ - StorageSettings.prototype._save = function (map, keepFalse, rootKey) { - if (!rootKey) { - rootKey = this._key; - } else { - rootKey = this._key + '-' + rootKey; - } - var extractString = this.storage.getPersistentData(rootKey); - if (!extractString) { - var updatedExtract = {}; - } else { - var extract = JSON.parse(extractString); - updatedExtract = _.clone(extract); - } - var updatingKeys = _.keys(map); - if (keepFalse) { - for (var i = 0; i < updatingKeys.length; i++) { - var key = updatingKeys[i]; - if (map[key] === undefined) { - delete updatedExtract[key]; - } else { - updatedExtract[key] = map[key]; - } - } - } else { - for (i = 0; i < updatingKeys.length; i++) { - key = updatingKeys[i]; - if (!map[key]) { - delete updatedExtract[key]; - } else { - updatedExtract[key] = map[key]; - } - } - } - if (_.isEqual(extract, updatedExtract)) return; - if (_.isEmpty(updatedExtract)) { - this.storage.resetPersistentData(rootKey); - return; - } - this.storage.savePersistentData(rootKey, JSON.stringify(updatedExtract)); }; })(); diff --git a/plugins/easy_mindmup/assets/javascripts/styles.js b/plugins/easy_mindmup/assets/javascripts/styles.js index fc93c4b..40b5a7a 100644 --- a/plugins/easy_mindmup/assets/javascripts/styles.js +++ b/plugins/easy_mindmup/assets/javascripts/styles.js @@ -41,10 +41,12 @@ self.setColor(defaultStyle); }); - $select.change(function () { - var value = $(this).val(); - self.setColor(value); - }); + $select + .attr("title", this.ysy.settings.labels.free.headerNotAvailable) + .on("change", function () { + self.ysy.util.showUpgradeModal("coloring"); + $(this).val(self.defaultStyle); + }); }; /** * diff --git a/plugins/easy_mindmup/assets/javascripts/toolbar.js b/plugins/easy_mindmup/assets/javascripts/toolbar.js index 161d763..6d0e414 100644 --- a/plugins/easy_mindmup/assets/javascripts/toolbar.js +++ b/plugins/easy_mindmup/assets/javascripts/toolbar.js @@ -82,7 +82,7 @@ OneSideButton.prototype.triggerName = "toggleOneSide"; OneSideButton.prototype._render = function () { - var isActive = this.ysy.settings.oneSideOn; + var isActive = this.ysy.idea && this.ysy.idea.oneSideOn; if (!isActive) isActive = false; this.$element.find("a").toggleClass("active", isActive); }; diff --git a/plugins/easy_mindmup/assets/javascripts/utils.js b/plugins/easy_mindmup/assets/javascripts/utils.js index 67255e6..e525b95 100644 --- a/plugins/easy_mindmup/assets/javascripts/utils.js +++ b/plugins/easy_mindmup/assets/javascripts/utils.js @@ -80,7 +80,7 @@ */ Util.prototype.showUpgradeModal= function (feature) { var ysy = this.ysy; - var $target = ysy.util.getModal("form-modal", "auto"); + var $target = ysy.util.getModal("upgrade-modal", "auto"); var template = ysy.settings.templates.upgrade; var freeLabels = ysy.settings.labels.free; var obj = { @@ -90,7 +90,7 @@ obj[feature] = true; var rendered = Mustache.render(template, obj); $target.html(rendered); - showModal("form-modal"); + showModal("upgrade-modal"); $target.dialog({ buttons: [ { @@ -310,23 +310,5 @@ } return null; }; - - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; - - Util.prototype.escapeHtml = function(string) { - return String(string).replace(/[&<>"'`=\/]/g, function (s) { - return entityMap[s]; - }); - }; - window.easyMindMupClasses.Util = Util; })(); \ No newline at end of file diff --git a/plugins/easy_mindmup/assets/stylesheets/easy_mindmup.css b/plugins/easy_mindmup/assets/stylesheets/easy_mindmup.css index 506686d..4bb9515 100644 --- a/plugins/easy_mindmup/assets/stylesheets/easy_mindmup.css +++ b/plugins/easy_mindmup/assets/stylesheets/easy_mindmup.css @@ -43,6 +43,16 @@ vertical-align: sub; } +.mindmup-node-icon-avatar .gravatar, +.mindmup-legend-item-cont .gravatar { + padding: 1px; + border: 1px solid #777; +} + +.mindmup-legend-item-cont .gravatar { + margin-top: -7px; +} + .mindmup-node-icon-avatar .gravatar:hover, .mindmup-legend-item-cont .gravatar:hover { transform: scale(2); @@ -144,7 +154,10 @@ } mindmup-sidebar { + position: absolute; display: block; + top: 0; + right: 0; } .date-picker__date-wrap .input-append { @@ -160,4 +173,3 @@ mindmup-sidebar { easy-lookup .easy-lookup-values-wrapper { height: auto; } - diff --git a/plugins/easy_mindmup/assets/stylesheets/easy_mindmup_sass.scss.erb b/plugins/easy_mindmup/assets/stylesheets/easy_mindmup_sass.scss.erb index 5c00743..366ff0d 100644 --- a/plugins/easy_mindmup/assets/stylesheets/easy_mindmup_sass.scss.erb +++ b/plugins/easy_mindmup/assets/stylesheets/easy_mindmup_sass.scss.erb @@ -1,5 +1,4 @@ <% redmine_sass = false - redmine_sass = true ep_com = Redmine::Plugin.installed?(:easy_project_com) %> <% if ep_com %> diff --git a/plugins/easy_mindmup/assets/stylesheets/jasmine.css b/plugins/easy_mindmup/assets/stylesheets/jasmine.css deleted file mode 100644 index 6319982..0000000 --- a/plugins/easy_mindmup/assets/stylesheets/jasmine.css +++ /dev/null @@ -1,58 +0,0 @@ -body { overflow-y: scroll; } - -.jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; } -.jasmine_html-reporter a { text-decoration: none; } -.jasmine_html-reporter a:hover { text-decoration: underline; } -.jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; } -.jasmine_html-reporter .jasmine-banner, .jasmine_html-reporter .jasmine-symbol-summary, .jasmine_html-reporter .jasmine-summary, .jasmine_html-reporter .jasmine-result-message, .jasmine_html-reporter .jasmine-spec .jasmine-description, .jasmine_html-reporter .jasmine-spec-detail .jasmine-description, .jasmine_html-reporter .jasmine-alert .jasmine-bar, .jasmine_html-reporter .jasmine-stack-trace { padding-left: 9px; padding-right: 9px; } -.jasmine_html-reporter .jasmine-banner { position: relative; } -.jasmine_html-reporter .jasmine-banner .jasmine-title { background: url('data:image/png;base64,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') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } -.jasmine_html-reporter .jasmine-banner .jasmine-version { margin-left: 14px; position: relative; top: 6px; } -.jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } -.jasmine_html-reporter .jasmine-version { color: #aaa; } -.jasmine_html-reporter .jasmine-banner { margin-top: 14px; } -.jasmine_html-reporter .jasmine-duration { color: #fff; float: right; line-height: 28px; padding-right: 9px; } -.jasmine_html-reporter .jasmine-symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } -.jasmine_html-reporter .jasmine-symbol-summary li { display: inline-block; height: 10px; width: 14px; font-size: 16px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { color: #007069; content: "\02022"; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed { line-height: 9px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-disabled { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-disabled:before { color: #bababa; content: "\02022"; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending { line-height: 17px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before { color: #ba9d37; content: "*"; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty { font-size: 14px; } -.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { color: #ba9d37; content: "\02022"; } -.jasmine_html-reporter .jasmine-run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; } -.jasmine_html-reporter .jasmine-run-options .jasmine-trigger { cursor: pointer; padding: 8px 16px; } -.jasmine_html-reporter .jasmine-run-options .jasmine-payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; } -.jasmine_html-reporter .jasmine-run-options .jasmine-payload.jasmine-open { display: block; } -.jasmine_html-reporter .jasmine-bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } -.jasmine_html-reporter .jasmine-bar.jasmine-failed { background-color: #ca3a11; } -.jasmine_html-reporter .jasmine-bar.jasmine-passed { background-color: #007069; } -.jasmine_html-reporter .jasmine-bar.jasmine-skipped { background-color: #bababa; } -.jasmine_html-reporter .jasmine-bar.jasmine-errored { background-color: #ca3a11; } -.jasmine_html-reporter .jasmine-bar.jasmine-menu { background-color: #fff; color: #aaa; } -.jasmine_html-reporter .jasmine-bar.jasmine-menu a { color: #333; } -.jasmine_html-reporter .jasmine-bar a { color: white; } -.jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list, .jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures { display: none; } -.jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list, .jasmine_html-reporter.jasmine-failure-list .jasmine-summary { display: none; } -.jasmine_html-reporter .jasmine-results { margin-top: 14px; } -.jasmine_html-reporter .jasmine-summary { margin-top: 14px; } -.jasmine_html-reporter .jasmine-summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } -.jasmine_html-reporter .jasmine-summary ul.jasmine-suite { margin-top: 7px; margin-bottom: 7px; } -.jasmine_html-reporter .jasmine-summary li.jasmine-passed a { color: #007069; } -.jasmine_html-reporter .jasmine-summary li.jasmine-failed a { color: #ca3a11; } -.jasmine_html-reporter .jasmine-summary li.jasmine-empty a { color: #ba9d37; } -.jasmine_html-reporter .jasmine-summary li.jasmine-pending a { color: #ba9d37; } -.jasmine_html-reporter .jasmine-summary li.jasmine-disabled a { color: #bababa; } -.jasmine_html-reporter .jasmine-description + .jasmine-suite { margin-top: 0; } -.jasmine_html-reporter .jasmine-suite { margin-top: 14px; } -.jasmine_html-reporter .jasmine-suite a { color: #333; } -.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail { margin-bottom: 28px; } -.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description { background-color: #ca3a11; } -.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description a { color: white; } -.jasmine_html-reporter .jasmine-result-message { padding-top: 14px; color: #333; white-space: pre; } -.jasmine_html-reporter .jasmine-result-message span.jasmine-result { display: block; } -.jasmine_html-reporter .jasmine-stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666; border: 1px solid #ddd; background: white; white-space: pre; } diff --git a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_default_variables.scss b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_default_variables.scss index 16f1519..4ef0ca3 100644 --- a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_default_variables.scss +++ b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_default_variables.scss @@ -1,6 +1,6 @@ $form-input-height: 25px !default; $mup-background-menu: white !default; -$mindmup-node-shadow: 0px 3px 3px 0px rgba(0,0,0,0.3) !default; +$mindmup-node-shadow: 5px 5px 5px 0 gray !default; $retrace: .75 * $box-padding !default; $retrace-mod: (0.25)*$box-padding !default; diff --git a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_frame.scss b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_frame.scss index da89cc8..cefc367 100644 --- a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_frame.scss +++ b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_frame.scss @@ -88,12 +88,6 @@ $legend-width: 260px; //@include respond-to(min-medium-screen){ // padding-left: $gap + $box-padding + $legend-width; //} - &_addons{ - position: absolute; - right: 0; - top: 2 * $box-padding + 31; - z-index: 5; - } &-item { display: inline-block; text-align: left; @@ -152,8 +146,8 @@ $legend-width: 260px; text-align: center; font-size: 1.5em; position: absolute; - top: $box-padding; - left: -$box-padding - 90; + bottom: -2*$box-padding; + right: $box-padding; line-height: $box-padding; @include respond-to(max-small-screen) { display: none; diff --git a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_legend.scss b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_legend.scss index 8a4ac23..7d689ac 100644 --- a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_legend.scss +++ b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_legend.scss @@ -31,9 +31,6 @@ &-cont{ cursor: pointer; margin-top: 0.5*$gap; - .avatar-container{ - float: none; - } } } .hotkey_link { diff --git a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_sidebar.scss b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_sidebar.scss index 61e39eb..58466a1 100644 --- a/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_sidebar.scss +++ b/plugins/easy_mindmup/assets/stylesheets/sass/_mindmup_sidebar.scss @@ -1,10 +1,13 @@ &-sidebar { + %info-block { background-color: #ffffaa; border: 1px solid #bfb23f; padding: 5px; text-align: center; } + + display: block; &__empty-title { @extend %info-block; display: block; @@ -25,7 +28,10 @@ border-radius: 5px; } &__root { - display: block; + position: absolute; + right: 0; + bottom: 0; + z-index: 5; } &__toggler { position: absolute; @@ -37,12 +43,6 @@ background: #d94838; color: #fff; } - &.easy-mindmup__icon { - padding-left: 15px; - &:before{ - width: auto; - } - } } &__container { border-left: 1px solid #dfccaf; @@ -105,7 +105,7 @@ } &-label { display: inline-block; - width: 100px; + width: 90px; margin-top: 5px; } .spaceholder { diff --git a/plugins/easy_mindmup/config/locales/ja.yml b/plugins/easy_mindmup/config/locales/ja.yml deleted file mode 100644 index 2f99861..0000000 --- a/plugins/easy_mindmup/config/locales/ja.yml +++ /dev/null @@ -1,155 +0,0 @@ ---- -ja: - easy_mindmup: - button_add_child: ĺăŽăĽă‰ă®čż˝ĺŠ - button_add_parent: 親ăŽăĽă‰ă®čż˝ĺŠ - button_add_sibling: 兄弟ăŽăĽă‰ă®čż˝ĺŠ - button_all_icons: アイコ㳠- button_collapse: ă•ă‚©ă«ă€ă„ăŞăĽćŠă‚Šăźăźăż - button_collapse_all: ă™ăąă¦ćŠă‚Šăźăźăż - button_cut: ĺ‡ă‚ŠĺŹ–ă‚Š - button_edit_data: ă‡ăĽă‚żç·¨é›† - button_expand: ă•ă‚©ă«ă€ă„ăŞăĽĺ±•é–‹ - button_expand_all: ă™ăąă¦ĺ±•é–‹ă™ă‚‹ - button_expand_collapse: 展開/ćŠă‚Šăźăźăż - button_hotkeys: ă›ăăă‚㼠- button_legend: 凡例 - button_one_side: ç‰‡ĺ´ - button_paste: 貼りä»ă‘ - button_project_menu: Easy MindMup - button_redo: ĺ…ă«ć»ă™ - button_remove_node: ăŽăĽă‰ă‚’取り除㏠- button_show_links: ăŞăłă‚Żă‚’表示ă™ă‚‹ - button_undo: 取りć¶ă— - error_create: 作ćă§ăŤăľă›ă‚“ă§ă—ăź - error_delete: 削除ă§ăŤăľă›ă‚“ă§ă—ăź - error_update: ć›´ć–°ă§ăŤăľă›ă‚“ă§ă—ăź - errors: - no_rest_api: REST APIăŚç„ˇĺŠąă«ăŞăŁă¦ă„ăľă™ă€‚管ç†ă§ă“れを有効ă«ă—ă¦ăŹă ă•ă„。 - not_subtaskable: ăケăă種ĺĄă®č¨ĺ®šă®ă›ă„ă§ă€ă‚żă‚ąă‚Ż “%{task_name}”をサă–ă‚żă‚ąă‚Żă§ăŤăľă›ă‚“ă§ă—ăź - free: - button_upgrade: 完全ç‰ă‚’取得ă™ă‚‹ - feature_coloring: ă—ăă‘ă†ă‚Łă«ă‚ă‚‹ăŽăĽă‰ă®č‰˛ä»ă‘ - feature_context_menu: ă‚łăłă†ă‚ă‚ąăă»ăˇă‹ăĄăĽă«ă‚ă‚‹ăŽăĽă‰ă»ă—ăă‘ă†ă‚Łă®ĺ¤‰ć›´ - feature_dnd_property: ă‰ă©ăă‚°ă»ă‚˘ăłă‰ă»ă‰ăăă—ă«ă‚ă‚‹ăŽăĽă‰ă»ă—ăă‘ă†ă‚Łă®ĺ¤‰ć›´ - feature_filtering: ă—ăă‘ă†ă‚Łă«ă‚ă‚‹ăŽăĽă‰ă®ă•ă‚Łă«ă‚żăŞăłă‚° - header_not_available: 完全ç‰ă§ă®ăżćŹäľ›ĺŹŻč˝ - hotkeys: - info_mac_metakey: Mac OS Xă§ăŻă€ä¸‹č¨Ctrlă¨č¨čĽ‰ă•ă‚Śăźă‚·ă§ăĽăă‚«ăăăŻă€Ctrl + Cmdă‚ăĽă¨ăŞă‚Šăľă™ă€‚一é¨ă®ă–ă©ă‚¦ă‚¶ăĽăŻă€ä¸€é¨ă®ă‚ăĽăイăłă‰ă‚’使用ă™ă‚‹ă“ă¨ăŚă§ăŤăľă›ă‚“。ă‚ăŁă¦ă€äľ‹ăă°ă€ăŠĺ®˘ć§ă®ă–ă©ă‚¦ă‚¶ăĽă§Cmd + SpaceăŚć©źč˝ă—ăŞă„ĺ ´ĺăŻă€Ctrl + Spaceを試ă—ă¦ăżă¦ăŹă ă•ă„。 - keyboard: - - title: ăŽăĽă‰ć“Ťä˝ś - hotkeys: - - hotkey: Enter - info: 兄弟ăŽăĽă‰ă®čż˝ĺŠ - - hotkey: Shift + Enter - info: 上č¨ă®ĺ…„弟ăŽăĽă‰čż˝ĺŠ ăľăźăŻć”ąčˇŚďĽăŽăĽă‰ă®ĺŤĺ‰Ťă‚’変更ă™ă‚‹ĺ ´ĺ) - - hotkey: TabăľăźăŻă‚¤ăłă‚µăĽăă»ă‚㼠- info: ĺăŽăĽă‰ă®čż˝ĺŠ - - hotkey: Shift + Tab - info: 親ăŽăĽă‰ă®ćŚżĺ…Ą - - hotkey: Space - info: ăŽăĽă‰ă®ăŞăŤăĽă - - hotkey: Shift + Space - info: ăŽăĽă‰ă»ă‡ăĽă‚żă®ç·¨é›† - - hotkey: ăăă‚Żă‚ąăšăĽă‚ąăľăźăŻĺ‰Šé™¤ - info: ăŽăĽă‰ă‚’取り除㏠- - hotkey: Ctrl + 上/下ă‚㼠- info: 上/下ă‚ăĽă§ăŽăĽă‰ă®ç§»ĺ‹• - - title: 編集 - hotkeys: - - hotkey: Ctrl + S - info: äżťĺ - - hotkey: Ctrl + X ăľăźăŻC - info: ĺ‡ă‚ŠĺŹ–ă‚Š - - hotkey: Ctrl + CăľăźăŻY - info: ă‚łă”㼠- - hotkey: Ctrl + VăľăźăŻP - info: 貼りä»ă‘ - - hotkey: UăľăźăŻCtrl+Z - info: 取りć¶ă— - - hotkey: RăľăźăŻCtrl+YăľăźăŻCtrl+Shift+Z - info: ĺ…ă«ć»ă™ - - title: é¸ćŠž - hotkeys: - - hotkey: 矢印ă‚㼠- info: 現在é¸ćŠžă—ă¦ă„ă‚‹ăŽăĽă‰ă®ä¸Š/下/ĺ·¦/右をé¸ćŠžă™ă‚‹ - - hotkey: Shift + 矢印ă‚㼠- info: é¸ćŠžă—ă¦ă„ă‚‹ăŽăĽă‰ă«ä¸Š/下/ĺ·¦/右ă®ăŽăĽă‰ă‚’čż˝ĺŠ ă™ă‚‹ďĽĺ…„弟ăŽăĽă‰č¤‡ć•°é¸ćŠžă«ăŻäľżĺ©ă§ă™ďĽ‰ - - hotkey: "{" - info: ă‚«ă¬ăłăă»ăŽăĽă‰ă¨ăťă®ä¸‹ă®ă‚µă–ă„ăŞăĽĺ…¨é¨ă®é¸ćŠž - - hotkey: "[" - info: ă‚«ă¬ăłăă»ăŽăĽă‰ă®ä¸‹ă®ă‚µă–ă„ăŞăĽă®ăżé¸ćŠžďĽăťă®ăŽăĽă‰ăťă®ă‚‚ă®ă§ăŻăŞăŹďĽ‰ - - hotkey: "=" - info: ă‚«ă¬ăłăă»ăŽăĽă‰ă®ĺ…„弟ăŽăĽă‰ĺ…¨é¨ă‚’複数é¸ćŠžďĽĺŚă親をćŚă¤ă‚‚ă®ďĽ‰ - - hotkey: . - info: 複数é¸ćŠžă‚’解除ă—ă€ĺ†Ťăłă‚«ă¬ăłăă»ăŽăĽă‰ă®ăżé¸ćŠžă™ă‚‹ - - hotkey: 1 - 9 - info: 特定ă®ć”ąčŁ…ă®ăŽăĽă‰ă™ăąă¦ă‚’é¸ćŠžă™ă‚‹ďĽäľ‹ďĽš1ă§ç¬¬ä¸€éšŽĺ±¤ă™ăąă¦ă‚’é¸ă¶ďĽ‰ - - title: ăŠă“ゲăĽă‚·ă§ăłă¨ç”»éť˘ - hotkeys: - - hotkey: /ăľăźăŻF - info: ăŽăĽă‰ă®ĺ±•é–‹ăľăźăŻćŠă‚ŠăźăźăżďĽĺăŽăĽă‰ă‚’ĺ«ă‚ă‚‹ă€ăľăźăŻĺ«ă‚ăŞă„) - - hotkey: Ctrl + ăľăźăŻZ - info: ă‚şăĽă イ㳠- - hotkey: Ctrl â’ ăľăźăŻShift Z - info: ă‚şăĽă アウă - - hotkey: Escă€0ă€Ctrl + 0 - info: "ăžăă—表示ă®ăŞă‚»ăă - ă«ăĽăă»ăŽăĽă‰ă‚’é¸ćŠžă—ă€ç”»éť˘ă®ä¸ĺ¤®ă«ç§»ĺ‹•ă—ăľă™" - mouse: - - action: ăžăă—を移動ă™ă‚‹ - gesture: ă‚»ăłă‚żăĽă»ăŽăĽă‰ă®ă‚ŻăŞăă‚Żă»ă‚˘ăłă‰ă»ă‰ă©ăă‚°ă™ă‚‹ă€čŚć™Żă®ă‚ŻăŞăă‚Żă»ă‚˘ăłă‰ă»ă‰ă©ăă‚°ă€ăľăźăŻăă©ăă‚Żă‘ăă‰ďĽŹă‚żăăă‘ăă‰ă§ă‚ąă‚ŻăăĽă«ă™ă‚‹ - - action: ăŽăĽă‰ă®é¸ćŠž - gesture: ăŽăĽă‰ă‚’ă‚żăă—ăľăźăŻă‚ŻăŞăă‚Żă™ă‚‹ - - action: 複数ă®ăŽăĽă‰ă®é¸ćŠž - gesture: Shift + ă‚ŻăŞăă‚Ż - - action: ăŽăĽă‰ă®ä¸¦ăąć›żă - gesture: ăŽăĽă‰ă‚’ă‰ă©ăă‚°ă—ă€ĺ…„弟ăŽăĽă‰é–“ă§ć°´ĺąłă«ä¸¦ăąć›żăă«čż‘ă„位置ă«ç§»ĺ‹•ă™ă‚‹ä¸¦ăąć›żăă§ăŻă€é»’ă„アăăĽăťă‚¤ăłăăŚčˇ¨ç¤şă•ă‚Śă‚‹ - - action: 手動ă§ăŽăĽă‰ă®ä˝Ťç˝®ă‚’決ă‚ă‚‹ - gesture: ăŽăĽă‰ă‚’並ăąć›żăă®ăźă‚ă®ă‚˘ăăĽăťă‚¤ăłăăŚć¶ăă‚‹ă¨ă“ă‚Ťčż„ă‰ă©ăă‚°ă™ă‚‹ä¸¦ăąć›żăアăăĽăťă‚¤ăłăăŚčˇ¨ç¤şă•ă‚Śă¦ă„ă‚‹ĺ ´ĺă«ć‰‹ĺ‹•ă§ä˝Ťç˝®ă‚’強ĺ¶ă™ă‚‹ă«ăŻă€ă‰ă©ăă‚°ä¸ă«Shiftă‚ăĽă‚’押ă—続ă‘ă‚‹ă«ăĽăă»ăŽăĽă‰ă®ĺăŽăĽă‰ăŻă©ă®ć–ąĺ‘ă«ă‚‚ă‰ă©ăă‚°ă™ă‚‹ă“ă¨ăŚĺŹŻč˝ă§ă™ăŚă€ăťă‚Śă‚り下ă®éšŽĺ±¤ă®ăŽăĽă‰ăŻă€ă«ăĽăă«ĺŻľă—ă¦ă€ăťă‚Śă‚‰ă®č¦ŞăŽăĽă‰ă®ć–ąĺ‘ă«ă®ăżé…Ťç˝®ă™ă‚‹ă“ă¨ăŚĺŹŻč˝ă ă¨ă„ă†ă“ă¨ă«ç•™ć„ŹăŹă ă•ă„。 - - action: ăŽăĽă‰ă»ăŞăŤăĽă - gesture: ăŽăĽă‰ă‚’ă€ă–ă«ă‚ŻăŞăă‚ŻăľăźăŻă€ă–ă«ă‚żăă—ă™ă‚‹ - - action: 操作ă®ă‚łăłă†ă‚ă‚ąăă»ăˇă‹ăĄăĽă®čˇ¨ç¤ş - gesture: ăŽăĽă‰ă‚’右クăŞăă‚ŻďĽăžă‚¦ă‚ąă§ďĽ‰ă€ăľăźăŻčŚć™Żă‚’ă€ă–ă«ă‚żăă—ă€ăľăźăŻăŽăĽă‰ă‚’押ă—続ă‘ă¦ă€čˇ¨ç¤şă™ă‚‹ďĽă€Ťă‚żăăă‚ąă‚ŻăŞăĽăłä¸Šă§ďĽ‰ - - action: 親ăŽăĽă‰ă®ĺ¤‰ć›´ - gesture: ăŽăĽă‰ă‚’ä»–ă®ăŽăĽă‰ă®ä¸Šă«ă‰ă©ăă‚°ă»ă‚˘ăłă‰ă»ă‰ăăă—ă™ă‚‹ďĽç„ˇé™ĺľŞç’°ă¨ăŞă‚‹ă‰ăăă—ăŻă§ăŤăľă›ă‚“。ă—ăźăŚăŁă¦ă€ĺăŽăĽă‰ăľăźăŻĺĺ«ăŽăĽă‰ä¸Šă«ăŽăĽă‰ă‚’ă‰ăăă—ă™ă‚‹ă“ă¨ăŻă§ăŤăľă›ă‚“) - - action: 課題やă—ăジェクăă‚’ĺĄă‚¦ă‚Łăłă‰ă‚¦ă§é–‹ăŹ - gesture: Alt + ă‚ŻăŞăă‚Żă™ă‚‹ - title_key_shortcuts: ă‚ăĽăśăĽă‰ć“Ťä˝ś - title_mouse_shortcuts: ăžă‚¦ă‚ąă¨ă‚żăăă«ă‚る操作 - title_shortcuts: ă‚·ă§ăĽăă‚«ăă - info_all_saved: ă™ăąă¦ă®äżťĺă«ć功ă—ăľă—ăź - info_any_failed: ăŞă‚Żă‚¨ă‚ąăă®ä¸€é¨ă«ĺ¤±ć•—ă—ăľă—ăź - info_no_permission: ă“れを行ă†ăźă‚ă®ĺż…č¦ăŞć¨©é™ă‚’ćŚăŁă¦ă„ăľă›ă‚“ - label_color_by: ă«ă‚る色ä»ă‘ - label_go_to: ă«é€˛ă‚€ - label_hide_unused: 未使用ă®ă‚‚ă®ă‚’éš ă™ - label_less: 未満 - label_or: ăľăźăŻ - label_save_progress: ă—ă°ă‚‰ăŹăŠĺľ…ăˇăŹă ă•ă„。全エăłă†ă‚Łă†ă‚ŁăĽă®äżťĺä¸ă§ă™ - label_show_unused: 未使用ă®é …目を表示 - last_state_modal: - label_differencies: サăĽăăĽä¸Šă®ç›¸é•ăŻă€ - message_changed: "ç•°ăŞă‚‹ĺ±žć€§ă‚’ćŚă¤ďĽă–ă©ă‚¦ă‚¶ăĽ =>サăĽăăĽ({{changes}})" - message_missing: "親ă‹ă‚‰ć¬ č˝ă—ă¦ă„ăľă™ “{{from}}" - message_moved: "ă®ĺäľ›ă§ă™ “{{to}}”ă€ă§ăŻăŞă„ “{{from}}”" - message_present: "ă®ĺäľ›ă¨ă—ă¦ă‚ă‚Šăľă™ “{{to}}”" - text_reload_appeal: サăĽăăĽă‹ă‚‰ă‚ąă†ăĽăを再čŞčľĽă—ăľă™ă‹ďĽź - title: Mind mapă®ćś€ć–°ă®ă‚Żă©ă‚¤ă‚˘ăłăă»ă‚ąă†ăĽăăŚă‚µăĽăăĽă»ă‚ąă†ăĽăă¨ç•°ăŞăŁă¦ă„ăľă™ă€‚ - reload_modal: - label_errors: エă©ăĽ - text_reload_appeal: äżťĺă•ă‚Śă¦ă„ăŞă„é …ç›®ă‚’ç„ˇč¦–ă—ă¦ă€ă‚µăĽăăĽă‹ă‚‰ă‡ăĽă‚żă®ĺ†ŤčŞčľĽă‚’ă—ăľă™ă‹ďĽź - title: Mind mapăŚćŁă—ăŹäżťĺă•ă‚Śăľă›ă‚“ă§ă—ăź - save_info: - at: ă« - autosaved: 自動保ĺ - loaded: ăăĽă‰çµ‚了 - saved: äżťĺ終了 - stored_modal: - button_local: ăăĽă‚«ă«ăăĽă‚¸ă§ăł - button_server: サăĽăăĽăăĽă‚¸ă§ăł - text_load_appeal: ăăĽă‰ă—ăľă™ă‹ďĽźăťă‚Śă¨ă‚‚ă€ă‚µăĽăăĽă‹ă‚‰ć–°ă—ă„ă‚ąă†ăĽăă‚’ăăĽă‰ă—ăľă™ă‹ďĽź - title: äżťĺă•ă‚Śă¦ă„ăŞă„ăă‚«ăĽă«ăăĽă‚¸ă§ăłăŚč¦‹ă¤ă‹ă‚Šăľă—ăź - title_node_changed: ă“ă®ăŽăĽă‰ăŚĺ¤‰ć›´ă•ă‚Śă€äżťĺă•ă‚Śăľă™ă€‚ - warning_delete_node: ăŽăĽă‰ď˝›ď˝›name}}ă¨ăťă®ă™ăąă¦ă®ĺĺ«ă‚’削除ă—ăľă™ă‹ďĽź - warning_delete_nodes: ă“れらă®ăŽăĽă‰ă¨ăťă®ă™ăąă¦ă®ĺĺ«ă‚’削除ă—ăľă™ă‹ďĽź - warning_not_saved: ăŻćŁă—ăŹäżťĺă•ă‚Śă¦ă„ăľă›ă‚“ \ No newline at end of file diff --git a/plugins/easy_mindmup/init.rb b/plugins/easy_mindmup/init.rb index f9ba9b3..a6b7bd9 100644 --- a/plugins/easy_mindmup/init.rb +++ b/plugins/easy_mindmup/init.rb @@ -2,7 +2,7 @@ Redmine::Plugin.register :easy_mindmup do name 'Easy MindMup plugin' author 'Easy Software Ltd' description 'Mind map view based on MindMup' - version '1.4' + version '1.0' url 'www.easyredmine.com' author_url 'www.easysoftware.cz' diff --git a/plugins/easy_mindmup/lib/easy_mindmup/easy_mindmup.rb b/plugins/easy_mindmup/lib/easy_mindmup/easy_mindmup.rb index 17f7c53..e767709 100644 --- a/plugins/easy_mindmup/lib/easy_mindmup/easy_mindmup.rb +++ b/plugins/easy_mindmup/lib/easy_mindmup/easy_mindmup.rb @@ -4,10 +4,4 @@ module EasyMindmup Redmine::Plugin.installed?(:easy_extensions) end - def self.combine_by_pipeline?(params) - return false unless easy_extensions? - return params[:combine_by_pipeline].to_s.to_boolean if params.key?(:combine_by_pipeline) - Rails.env.production? - end - end diff --git a/plugins/easy_wbs/README.md b/plugins/easy_wbs/README.md deleted file mode 100644 index 7647453..0000000 --- a/plugins/easy_wbs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Easy WBS - -For documentation and requirements, go to https://www.easyredmine.com/redmine-wbs-plugin diff --git a/plugins/easy_wbs/after_init.rb b/plugins/easy_wbs/after_init.rb index 47d281f..51781c2 100644 --- a/plugins/easy_wbs/after_init.rb +++ b/plugins/easy_wbs/after_init.rb @@ -25,7 +25,7 @@ Redmine::AccessControl.map do |map| end end -ActionDispatch::Reloader.to_prepare do +RedmineExtensions::Reloader.to_prepare do require 'easy_wbs/hooks' require 'easy_wbs/easy_wbs' diff --git a/plugins/easy_wbs/app/controllers/easy_wbs_controller.rb b/plugins/easy_wbs/app/controllers/easy_wbs_controller.rb index 48479a0..bf12aea 100644 --- a/plugins/easy_wbs/app/controllers/easy_wbs_controller.rb +++ b/plugins/easy_wbs/app/controllers/easy_wbs_controller.rb @@ -98,7 +98,7 @@ class EasyWbsController < ApplicationController end def load_versions - @versions = @projects.flat_map(&:shared_versions).select(&:open?).uniq + @versions = @projects.flat_map(&:shared_versions).uniq end def load_relations diff --git a/plugins/easy_wbs/app/models/easy_queries/easy_wbs_easy_issue_query.rb b/plugins/easy_wbs/app/models/easy_queries/easy_wbs_easy_issue_query.rb deleted file mode 100644 index 60f320d..0000000 --- a/plugins/easy_wbs/app/models/easy_queries/easy_wbs_easy_issue_query.rb +++ /dev/null @@ -1,53 +0,0 @@ -class EasyWbsEasyIssueQuery < EasyIssueQuery - - def easy_query_entity_controller - 'easy_wbs' - end - - def easy_query_entity_action - 'index' - end - - def columns - [] - end - - def groupable_columns - [] - end - - def project_scope - super.where.not(status: [Project::STATUS_CLOSED, Project::STATUS_ARCHIVED]) - end - - def entity_easy_query_path(options={}) - if project - project_easy_wbs_index_path(project, options) - end - end - - def query_after_initialize - super - - self.display_filter_columns_on_index = false - self.display_filter_group_by_on_index = false - self.display_filter_sort_on_index = false - self.display_filter_settings_on_index = false - - self.display_filter_columns_on_edit = false - self.display_filter_group_by_on_edit = false - self.display_filter_sort_on_edit = false - self.display_filter_settings_on_edit = false - - self.display_show_sum_row = false - self.display_load_groups_opened = false - - self.export_formats = {} - self.is_tagged = true if new_record? - - # self.display_filter_fullscreen_button = - # self.display_save_button = - # self.display_project_column_if_project_missing = - end - -end diff --git a/plugins/easy_wbs/app/views/easy_wbs/_includes.html.erb b/plugins/easy_wbs/app/views/easy_wbs/_includes.html.erb index efc9f7c..321f373 100644 --- a/plugins/easy_wbs/app/views/easy_wbs/_includes.html.erb +++ b/plugins/easy_wbs/app/views/easy_wbs/_includes.html.erb @@ -1,22 +1,14 @@ -<% if EasyMindmup.easy_extensions? %> - <%= stylesheet_link_tag('easy_wbs', media: 'all') %> -<% else %> - <%= stylesheet_link_tag('generated/easy_wbs', plugin: 'easy_wbs', media: 'all') %> -<% end %> +<%= stylesheet_link_tag('easy_wbs', plugin: 'easy_wbs', media: 'all') %> +<%= javascript_include_tag( + :wbs_main, + :wbs_context_menu, + :wbs_loader, + :wbs_node_patch, + :wbs_saver, + :wbs_modals, + :wbs_styles, + :wbs_validator, + plugin: 'easy_wbs' -<% if EasyMindmup.combine_by_pipeline?(params) %> - <%= javascript_include_tag('easy_wbs') %> -<% else %> - <%= javascript_include_tag( - :wbs_main, - :wbs_context_menu, - :wbs_loader, - :wbs_node_patch, - :wbs_saver, - :wbs_modals, - :wbs_styles, - :wbs_validator, - plugin: 'easy_wbs' - ) %> -<% end %> +) %> <% include_galereya_headers_tags if defined?(include_galereya_headers_tags) %> diff --git a/plugins/easy_wbs/app/views/easy_wbs/_test_includes.html.erb b/plugins/easy_wbs/app/views/easy_wbs/_test_includes.html.erb index 7b1f822..54a36eb 100644 --- a/plugins/easy_wbs/app/views/easy_wbs/_test_includes.html.erb +++ b/plugins/easy_wbs/app/views/easy_wbs/_test_includes.html.erb @@ -1,5 +1,4 @@ <%= javascript_include_tag( - 'tests/encode_layout', 'tests/saving_test', 'tests/printing', 'tests/parse_form', diff --git a/plugins/easy_wbs/app/views/easy_wbs/index.html.erb b/plugins/easy_wbs/app/views/easy_wbs/index.html.erb index dd24dab..79c6c54 100644 --- a/plugins/easy_wbs/app/views/easy_wbs/index.html.erb +++ b/plugins/easy_wbs/app/views/easy_wbs/index.html.erb @@ -29,7 +29,7 @@ <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--legend mindmup__legend-trigger"><span class="tip"><%= l(:button_legend, :scope => [:easy_wbs]) %></span></a> </div> </div> - <div class="mindmup-legend mindmup__legend-body"></div> + <div class="mindmup-legend"></div> </div> <div class="mindmup__menu-group mindmup__menu-group--tooltiped mindmup__menu-item mindmup__menu-group-display"> <a href="javascript:void(0)" class="button button-2 easy-mindmup__icon easy-mindmup__icon--display"><span><%= l(:button_display, :scope => [:easy_wbs]) %></span></a> @@ -53,20 +53,44 @@ </ul> </div> </div> - <div class="mindmup__menu_addons"> - <div class="mindmup__menu-group mindmup__menu-group--sizing mindmup__menu-item"> - <ul> - <li class="scaleUp"> - <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--zoom_in"></a> - </li> - <li class="resetView"> - <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--refresh_view"></a> - </li> - <li class="scaleDown"> - <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--zoom_out"></a> - </li> - </ul> - </div> + <% if defined?(EasyExtensions) && !EasySetting.value(:easy_wbs_no_sidebar)%> + <script type="application/javascript"> + window.easyDartLoaders = window.easyDartLoaders || []; + document.write('<ng-root-' + window.easyDartLoaders.length + ' class="mindmup-sidebar__root"></ng-root-' + window.easyDartLoaders.length + '>'); + window.easyDartLoaders.push(<%= { + subtype: :wbs, + id:'WBS classic', + plugin: 'mindmupSidebar', + data: { + lang: wbs_translations, + paths: { + # home_path: (home_path=='/')?home_path:(home_path+'/'), + home_path: home_path, + issue_path: issue_path('__issueId',key: User.current.api_key,format: :json), + project_path: project_path('__projectId',key: User.current.api_key,format: :json), + easy_issue_form_fields: url_for(controller: :easy_issues, action: :form_fields,id: '__issueId',format: :json,project_id: nil,key: User.current.api_key), + easy_issue_project_form_fields: url_for(controller: :easy_issues, action: :form_fields,format: :json,project_id: '__projectId',key: User.current.api_key), + user_json: user_path('__userId',key: User.current.api_key,format: :json), + user_html: user_path('__userId'), + user_profile_html:profile_user_path('__userId') + } + }.to_json.to_s + # there should be String value in key data - it is faster to iterop with dart part + }.to_json.html_safe %>); + </script> + <% end %> + <div class="mindmup__menu-group mindmup__menu-group--sizing mindmup__menu-item"> + <ul> + <li class="scaleUp"> + <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--zoom_in"></a> + </li> + <li class="resetView"> + <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--refresh_view"></a> + </li> + <li class="scaleDown"> + <a href="javascript:void(0)" class="easy-mindmup__icon easy-mindmup__icon--zoom_out"></a> + </li> + </ul> </div> <div class="push-right"> <div class="mindmup__menu-group mindmup__menu-group--tooltiped mindmup__menu-item"> @@ -111,12 +135,10 @@ <% heads_for_wiki_formatter %> <%= content_for :header_tags do %> <%= render 'easy_mindmup/includes' %> - <%= render 'easy_wbs/includes' %> <%= render 'easy_mindmup/js_prepare' %> - <%= render 'easy_mindmup/test_includes' if params[:run_jasmine_tests] || params[:spec] %> <%= render 'easy_wbs/js_prepare' %> - <% # helper functions for user testing %> - <%= render 'easy_wbs/test_includes' if params[:include_tests] %> + <%= render 'easy_wbs/includes' %> + <%= render 'easy_wbs/test_includes' %> <script type="application/javascript"> window.easyMindMupSetting.paths.data = "<%= data_path.html_safe %>"; diff --git a/plugins/easy_wbs/assets/javascripts/easy_wbs.js b/plugins/easy_wbs/assets/javascripts/easy_wbs.js deleted file mode 100644 index e4cb518..0000000 --- a/plugins/easy_wbs/assets/javascripts/easy_wbs.js +++ /dev/null @@ -1,3 +0,0 @@ -/* - * = require_directory . - */ \ No newline at end of file diff --git a/plugins/easy_wbs/assets/javascripts/tests/encode_layout.js b/plugins/easy_wbs/assets/javascripts/tests/encode_layout.js deleted file mode 100644 index 6801f41..0000000 --- a/plugins/easy_wbs/assets/javascripts/tests/encode_layout.js +++ /dev/null @@ -1,5827 +0,0 @@ -/** - * Created by hosekp on 6/12/17. - */ -window.easyTests = $.extend(window.easyTests, { - encodeLayout: function () { - var smallLayout = { - "i2280": {"rank": 1}, - "i2281": {"rank": 2}, - "i2282": {"rank": 3}, - "i2279": {"rank": 1}, - "i2285": {"rank": 1}, - "i2286": {"rank": 2}, - "i2287": {"rank": 3}, - "i2288": {"rank": 4}, - "i2289": {"rank": 5}, - "i2284": {"rank": 1}, - "i2291": {"rank": 1}, - "i2292": {"rank": 2}, - "i2293": {"rank": 3}, - "i2295": {"rank": 1}, - "i2296": {"rank": 2}, - "i2294": {"rank": 4}, - "i2297": {"rank": 5}, - "i2290": {"rank": 2}, - "i2299": {"rank": 1}, - "i2298": {"rank": 3}, - "i2301": {"rank": 1}, - "i2302": {"rank": 2}, - "i2303": {"rank": 3}, - "i2304": {"rank": 4}, - "i2300": {"rank": 4}, - "i2306": {"rank": 1}, - "i2307": {"rank": 2}, - "i2305": {"rank": 5}, - "i2309": {"rank": 1}, - "i2310": {"rank": 2}, - "i2308": {"rank": 6}, - "i2283": {"rank": 2}, - "i2313": {"rank": 1}, - "i2314": {"rank": 2}, - "i2312": {"rank": 1}, - "i2316": {"rank": 1}, - "i2315": {"rank": 2}, - "i2318": {"rank": 1}, - "i2317": {"rank": 3}, - "i2320": {"rank": 1}, - "i2321": {"rank": 2}, - "i2319": {"rank": 4}, - "i2323": {"rank": 1}, - "i2322": {"rank": 5}, - "i2311": {"rank": 3}, - "i2325": {"rank": 1}, - "i2326": {"rank": 2}, - "i2327": {"rank": 3}, - "i2328": {"rank": 4}, - "i2329": {"rank": 5}, - "i2330": {"rank": 6}, - "i2331": {"rank": 7}, - "i2324": {"rank": 4}, - "i2278": {"rank": 1}, - "i2335": {"rank": 1}, - "i2336": {"rank": 2}, - "i2337": {"rank": 3}, - "i2338": {"rank": 4}, - "i2339": {"rank": 5}, - "i2334": {"rank": 1}, - "i2341": {"rank": 1}, - "i2342": {"rank": 2}, - "i2343": {"rank": 3}, - "i2344": {"rank": 4}, - "i2340": {"rank": 2}, - "i2346": {"rank": 1}, - "i2347": {"rank": 2}, - "i2345": {"rank": 3}, - "i2333": {"rank": 1}, - "i2350": {"rank": 1}, - "i2351": {"rank": 2}, - "i2352": {"rank": 3}, - "i2349": {"rank": 1}, - "i2354": {"rank": 1}, - "i2355": {"rank": 2}, - "i2353": {"rank": 2}, - "i2357": {"rank": 1}, - "i2358": {"rank": 2}, - "i2356": {"rank": 3}, - "i2360": {"rank": 1}, - "i2361": {"rank": 2}, - "i2359": {"rank": 4}, - "i2348": {"rank": 2}, - "i2363": {"rank": 1}, - "i2364": {"rank": 2}, - "i2365": {"rank": 3}, - "i2366": {"rank": 4}, - "i2367": {"rank": 5}, - "i2368": {"rank": 6}, - "i2362": {"rank": 3}, - "i2332": {"rank": 2}, - "i2371": {"rank": 1}, - "i2372": {"rank": 2}, - "i2373": {"rank": 3}, - "i2374": {"rank": 4}, - "i2375": {"rank": 5}, - "i2376": {"rank": 6}, - "i2370": {"rank": 1}, - "i2378": {"rank": 1}, - "i2379": {"rank": 2}, - "i2380": {"rank": 3}, - "i2377": {"rank": 2}, - "i2382": {"rank": 1}, - "i2383": {"rank": 2}, - "i2384": {"rank": 3}, - "i2381": {"rank": 3}, - "i2386": {"rank": 1}, - "i2387": {"rank": 2}, - "i2388": {"rank": 3}, - "i2385": {"rank": 4}, - "i2369": {"rank": 3}, - "i2391": {"rank": 1}, - "i2392": {"rank": 2}, - "i2393": {"rank": 3}, - "i2390": {"rank": 1}, - "i2395": {"rank": 1}, - "i2396": {"rank": 2}, - "i2397": {"rank": 3}, - "i2394": {"rank": 2}, - "i2399": {"rank": 1}, - "i2400": {"rank": 2}, - "i2401": {"rank": 3}, - "i2398": {"rank": 3}, - "i2389": {"rank": 4}, - "i2402": {"rank": 5}, - "p85": {"rank": -6}, - "i1655": {"rank": 1}, - "i1656": {"rank": 2}, - "i1657": {"rank": 3}, - "i1654": {"rank": 1}, - "i1660": {"rank": 1}, - "i1661": {"rank": 2}, - "i1662": {"rank": 3}, - "i1663": {"rank": 4}, - "i1664": {"rank": 5}, - "i1659": {"rank": 1}, - "i1666": {"rank": 1}, - "i1667": {"rank": 2}, - "i1668": {"rank": 3}, - "i1670": {"rank": 1}, - "i1671": {"rank": 2}, - "i1669": {"rank": 4}, - "i1672": {"rank": 5}, - "i1665": {"rank": 2}, - "i1674": {"rank": 1}, - "i1673": {"rank": 3}, - "i1676": {"rank": 1}, - "i1677": {"rank": 2}, - "i1678": {"rank": 3}, - "i1679": {"rank": 4}, - "i1675": {"rank": 4}, - "i1681": {"rank": 1}, - "i1682": {"rank": 2}, - "i1680": {"rank": 5}, - "i1684": {"rank": 1}, - "i1685": {"rank": 2}, - "i1683": {"rank": 6}, - "i1658": {"rank": 2}, - "i1688": {"rank": 1}, - "i1689": {"rank": 2}, - "i1687": {"rank": 1}, - "i1691": {"rank": 1}, - "i1690": {"rank": 2}, - "i1693": {"rank": 1}, - "i1692": {"rank": 3}, - "i1695": {"rank": 1}, - "i1696": {"rank": 2}, - "i1694": {"rank": 4}, - "i1698": {"rank": 1}, - "i1697": {"rank": 5}, - "i1686": {"rank": 3}, - "i1700": {"rank": 1}, - "i1701": {"rank": 2}, - "i1702": {"rank": 3}, - "i1703": {"rank": 4}, - "i1704": {"rank": 5}, - "i1705": {"rank": 6}, - "i1706": {"rank": 7}, - "i1699": {"rank": 4}, - "i1653": {"rank": 1}, - "i1710": {"rank": 1}, - "i1711": {"rank": 2}, - "i1712": {"rank": 3}, - "i1713": {"rank": 4}, - "i1714": {"rank": 5}, - "i1709": {"rank": 1}, - "i1716": {"rank": 1}, - "i1717": {"rank": 2}, - "i1718": {"rank": 3}, - "i1719": {"rank": 4}, - "i1715": {"rank": 2}, - "i1721": {"rank": 1}, - "i1722": {"rank": 2}, - "i1720": {"rank": 3}, - "i1708": {"rank": 1}, - "i1725": {"rank": 1}, - "i1726": {"rank": 2}, - "i1727": {"rank": 3}, - "i1724": {"rank": 1}, - "i1729": {"rank": 1}, - "i1730": {"rank": 2}, - "i1728": {"rank": 2}, - "i1732": {"rank": 1}, - "i1733": {"rank": 2}, - "i1731": {"rank": 3}, - "i1735": {"rank": 1}, - "i1736": {"rank": 2}, - "i1734": {"rank": 4}, - "i1723": {"rank": 2}, - "i1738": {"rank": 1}, - "i1739": {"rank": 2}, - "i1740": {"rank": 3}, - "i1741": {"rank": 4}, - "i1742": {"rank": 5}, - "i1743": {"rank": 6}, - "i1737": {"rank": 3}, - "i1707": {"rank": 2}, - "i1746": {"rank": 1}, - "i1747": {"rank": 2}, - "i1748": {"rank": 3}, - "i1749": {"rank": 4}, - "i1750": {"rank": 5}, - "i1751": {"rank": 6}, - "i1745": {"rank": 1}, - "i1753": {"rank": 1}, - "i1754": {"rank": 2}, - "i1755": {"rank": 3}, - "i1752": {"rank": 2}, - "i1757": {"rank": 1}, - "i1758": {"rank": 2}, - "i1759": {"rank": 3}, - "i1756": {"rank": 3}, - "i1761": {"rank": 1}, - "i1762": {"rank": 2}, - "i1763": {"rank": 3}, - "i1760": {"rank": 4}, - "i1744": {"rank": 3}, - "i1766": {"rank": 1}, - "i1767": {"rank": 2}, - "i1768": {"rank": 3}, - "i1765": {"rank": 1}, - "i1770": {"rank": 1}, - "i1771": {"rank": 2}, - "i1772": {"rank": 3}, - "i1769": {"rank": 2}, - "i1774": {"rank": 1}, - "i1775": {"rank": 2}, - "i1776": {"rank": 3}, - "i1773": {"rank": 3}, - "i1764": {"rank": 4}, - "i1777": {"rank": 5}, - "p80": {"rank": -5}, - "i2530": {"rank": 1}, - "i2531": {"rank": 2}, - "i2532": {"rank": 3}, - "i2529": {"rank": 1}, - "i2535": {"rank": 1}, - "i2536": {"rank": 2}, - "i2537": {"rank": 3}, - "i2538": {"rank": 4}, - "i2539": {"rank": 5}, - "i2534": {"rank": 1}, - "i2541": {"rank": 1}, - "i2542": {"rank": 2}, - "i2543": {"rank": 3}, - "i2545": {"rank": 1}, - "i2546": {"rank": 2}, - "i2544": {"rank": 4}, - "i2547": {"rank": 5}, - "i2540": {"rank": 2}, - "i2549": {"rank": 1}, - "i2548": {"rank": 3}, - "i2551": {"rank": 1}, - "i2552": {"rank": 2}, - "i2553": {"rank": 3}, - "i2554": {"rank": 4}, - "i2550": {"rank": 4}, - "i2556": {"rank": 1}, - "i2557": {"rank": 2}, - "i2555": {"rank": 5}, - "i2559": {"rank": 1}, - "i2560": {"rank": 2}, - "i2558": {"rank": 6}, - "i2533": {"rank": 2}, - "i2563": {"rank": 1}, - "i2564": {"rank": 2}, - "i2562": {"rank": 1}, - "i2566": {"rank": 1}, - "i2565": {"rank": 2}, - "i2568": {"rank": 1}, - "i2567": {"rank": 3}, - "i2570": {"rank": 1}, - "i2571": {"rank": 2}, - "i2569": {"rank": 4}, - "i2573": {"rank": 1}, - "i2572": {"rank": 5}, - "i2561": {"rank": 3}, - "i2575": {"rank": 1}, - "i2576": {"rank": 2}, - "i2577": {"rank": 3}, - "i2578": {"rank": 4}, - "i2579": {"rank": 5}, - "i2580": {"rank": 6}, - "i2581": {"rank": 7}, - "i2574": {"rank": 4}, - "i2528": {"rank": 1}, - "i2585": {"rank": 1}, - "i2586": {"rank": 2}, - "i2587": {"rank": 3}, - "i2588": {"rank": 4}, - "i2589": {"rank": 5}, - "i2584": {"rank": 1}, - "i2591": {"rank": 1}, - "i2592": {"rank": 2}, - "i2593": {"rank": 3}, - "i2594": {"rank": 4}, - "i2590": {"rank": 2}, - "i2596": {"rank": 1}, - "i2597": {"rank": 2}, - "i2595": {"rank": 3}, - "i2583": {"rank": 1}, - "i2600": {"rank": 1}, - "i2601": {"rank": 2}, - "i2602": {"rank": 3}, - "i2599": {"rank": 1}, - "i2604": {"rank": 1}, - "i2605": {"rank": 2}, - "i2603": {"rank": 2}, - "i2607": {"rank": 1}, - "i2608": {"rank": 2}, - "i2606": {"rank": 3}, - "i2610": {"rank": 1}, - "i2611": {"rank": 2}, - "i2609": {"rank": 4}, - "i2598": {"rank": 2}, - "i2613": {"rank": 1}, - "i2614": {"rank": 2}, - "i2615": {"rank": 3}, - "i2616": {"rank": 4}, - "i2617": {"rank": 5}, - "i2618": {"rank": 6}, - "i2612": {"rank": 3}, - "i2582": {"rank": 2}, - "i2621": {"rank": 1}, - "i2622": {"rank": 2}, - "i2623": {"rank": 3}, - "i2624": {"rank": 4}, - "i2625": {"rank": 5}, - "i2626": {"rank": 6}, - "i2620": {"rank": 1}, - "i2628": {"rank": 1}, - "i2629": {"rank": 2}, - "i2630": {"rank": 3}, - "i2627": {"rank": 2}, - "i2632": {"rank": 1}, - "i2633": {"rank": 2}, - "i2634": {"rank": 3}, - "i2631": {"rank": 3}, - "i2636": {"rank": 1}, - "i2637": {"rank": 2}, - "i2638": {"rank": 3}, - "i2635": {"rank": 4}, - "i2619": {"rank": 3}, - "i2641": {"rank": 1}, - "i2642": {"rank": 2}, - "i2643": {"rank": 3}, - "i2640": {"rank": 1}, - "i2645": {"rank": 1}, - "i2646": {"rank": 2}, - "i2647": {"rank": 3}, - "i2644": {"rank": 2}, - "i2649": {"rank": 1}, - "i2650": {"rank": 2}, - "i2651": {"rank": 3}, - "i2648": {"rank": 3}, - "i2639": {"rank": 4}, - "i2652": {"rank": 5}, - "p87": {"rank": -4}, - "i2905": {"rank": 1}, - "i2906": {"rank": 2}, - "i2907": {"rank": 3}, - "i2904": {"rank": 1}, - "i2910": {"rank": 1}, - "i2911": {"rank": 2}, - "i2912": {"rank": 3}, - "i2913": {"rank": 4}, - "i2914": {"rank": 5}, - "i2909": {"rank": 1}, - "i2916": {"rank": 1}, - "i2917": {"rank": 2}, - "i2918": {"rank": 3}, - "i2920": {"rank": 1}, - "i2921": {"rank": 2}, - "i2919": {"rank": 4}, - "i2922": {"rank": 5}, - "i2915": {"rank": 2}, - "i2924": {"rank": 1}, - "i2923": {"rank": 3}, - "i2926": {"rank": 1}, - "i2927": {"rank": 2}, - "i2928": {"rank": 3}, - "i2929": {"rank": 4}, - "i2925": {"rank": 4}, - "i2931": {"rank": 1}, - "i2932": {"rank": 2}, - "i2930": {"rank": 5}, - "i2934": {"rank": 1}, - "i2935": {"rank": 2}, - "i2933": {"rank": 6}, - "i2908": {"rank": 2}, - "i2938": {"rank": 1}, - "i2939": {"rank": 2}, - "i2937": {"rank": 1}, - "i2941": {"rank": 1}, - "i2940": {"rank": 2}, - "i2943": {"rank": 1}, - "i2942": {"rank": 3}, - "i2945": {"rank": 1}, - "i2946": {"rank": 2}, - "i2944": {"rank": 4}, - "i2948": {"rank": 1}, - "i2947": {"rank": 5}, - "i2936": {"rank": 3}, - "i2950": {"rank": 1}, - "i2951": {"rank": 2}, - "i2952": {"rank": 3}, - "i2953": {"rank": 4}, - "i2954": {"rank": 5}, - "i2955": {"rank": 6}, - "i2956": {"rank": 7}, - "i2949": {"rank": 4}, - "i2903": {"rank": 1}, - "i2960": {"rank": 1}, - "i2961": {"rank": 2}, - "i2962": {"rank": 3}, - "i2963": {"rank": 4}, - "i2964": {"rank": 5}, - "i2959": {"rank": 1}, - "i2966": {"rank": 1}, - "i2967": {"rank": 2}, - "i2968": {"rank": 3}, - "i2969": {"rank": 4}, - "i2965": {"rank": 2}, - "i2971": {"rank": 1}, - "i2972": {"rank": 2}, - "i2970": {"rank": 3}, - "i2958": {"rank": 1}, - "i2975": {"rank": 1}, - "i2976": {"rank": 2}, - "i2977": {"rank": 3}, - "i2974": {"rank": 1}, - "i2979": {"rank": 1}, - "i2980": {"rank": 2}, - "i2978": {"rank": 2}, - "i2982": {"rank": 1}, - "i2983": {"rank": 2}, - "i2981": {"rank": 3}, - "i2985": {"rank": 1}, - "i2986": {"rank": 2}, - "i2984": {"rank": 4}, - "i2973": {"rank": 2}, - "i2988": {"rank": 1}, - "i2989": {"rank": 2}, - "i2990": {"rank": 3}, - "i2991": {"rank": 4}, - "i2992": {"rank": 5}, - "i2993": {"rank": 6}, - "i2987": {"rank": 3}, - "i2957": {"rank": 2}, - "i2996": {"rank": 1}, - "i2997": {"rank": 2}, - "i2998": {"rank": 3}, - "i2999": {"rank": 4}, - "i3000": {"rank": 5}, - "i3001": {"rank": 6}, - "i2995": {"rank": 1}, - "i3003": {"rank": 1}, - "i3004": {"rank": 2}, - "i3005": {"rank": 3}, - "i3002": {"rank": 2}, - "i3007": {"rank": 1}, - "i3008": {"rank": 2}, - "i3009": {"rank": 3}, - "i3006": {"rank": 3}, - "i3011": {"rank": 1}, - "i3012": {"rank": 2}, - "i3013": {"rank": 3}, - "i3010": {"rank": 4}, - "i2994": {"rank": 3}, - "i3016": {"rank": 1}, - "i3017": {"rank": 2}, - "i3018": {"rank": 3}, - "i3015": {"rank": 1}, - "i3020": {"rank": 1}, - "i3021": {"rank": 2}, - "i3022": {"rank": 3}, - "i3019": {"rank": 2}, - "i3024": {"rank": 1}, - "i3025": {"rank": 2}, - "i3026": {"rank": 3}, - "i3023": {"rank": 3}, - "i3014": {"rank": 4}, - "i3027": {"rank": 5, "position": [808.5, 111.25, 1]}, - "p90": {"rank": -3}, - "i1905": {"rank": 1}, - "i1906": {"rank": 2}, - "i1907": {"rank": 3}, - "i1904": {"rank": 1}, - "i1910": {"rank": 1}, - "i1911": {"rank": 2}, - "i1912": {"rank": 3}, - "i1913": {"rank": 4}, - "i1914": {"rank": 5}, - "i1909": {"rank": 1}, - "i1916": {"rank": 1}, - "i1917": {"rank": 2}, - "i1918": {"rank": 3}, - "i1920": {"rank": 1}, - "i1921": {"rank": 2}, - "i1919": {"rank": 4}, - "i1922": {"rank": 5}, - "i1915": {"rank": 2}, - "i1924": {"rank": 1}, - "i1923": {"rank": 3}, - "i1926": {"rank": 1}, - "i1927": {"rank": 2}, - "i1928": {"rank": 3}, - "i1929": {"rank": 4}, - "i1925": {"rank": 4}, - "i1931": {"rank": 1}, - "i1932": {"rank": 2}, - "i1930": {"rank": 5}, - "i1934": {"rank": 1}, - "i1935": {"rank": 2}, - "i1933": {"rank": 6}, - "i1908": {"rank": 2}, - "i1938": {"rank": 1}, - "i1939": {"rank": 2}, - "i1937": {"rank": 1}, - "i1941": {"rank": 1}, - "i1940": {"rank": 2}, - "i1943": {"rank": 1}, - "i1942": {"rank": 3}, - "i1945": {"rank": 1}, - "i1946": {"rank": 2}, - "i1944": {"rank": 4}, - "i1948": {"rank": 1}, - "i1947": {"rank": 5}, - "i1936": {"rank": 3}, - "i1950": {"rank": 1}, - "i1951": {"rank": 2}, - "i1952": {"rank": 3}, - "i1953": {"rank": 4}, - "i1954": {"rank": 5}, - "i1955": {"rank": 6}, - "i1956": {"rank": 7}, - "i1949": {"rank": 4}, - "i1903": {"rank": 1}, - "i1960": {"rank": 1}, - "i1961": {"rank": 2}, - "i1962": {"rank": 3}, - "i1963": {"rank": 4}, - "i1964": {"rank": 5}, - "i1959": {"rank": 1}, - "i1966": {"rank": 1}, - "i1967": {"rank": 2}, - "i1968": {"rank": 3}, - "i1969": {"rank": 4}, - "i1965": {"rank": 2}, - "i1971": {"rank": 1}, - "i1972": {"rank": 2}, - "i1970": {"rank": 3}, - "i1958": {"rank": 1}, - "i1975": {"rank": 1}, - "i1976": {"rank": 2}, - "i1977": {"rank": 3}, - "i1974": {"rank": 1}, - "i1979": {"rank": 1}, - "i1980": {"rank": 2}, - "i1978": {"rank": 2}, - "i1982": {"rank": 1}, - "i1983": {"rank": 2}, - "i1981": {"rank": 3}, - "i1985": {"rank": 1}, - "i1986": {"rank": 2}, - "i1984": {"rank": 4}, - "i1973": {"rank": 2}, - "i1988": {"rank": 1}, - "i1989": {"rank": 2}, - "i1990": {"rank": 3}, - "i1991": {"rank": 4}, - "i1992": {"rank": 5}, - "i1993": {"rank": 6}, - "i1987": {"rank": 3}, - "i1957": {"rank": 2}, - "i1996": {"rank": 1}, - "i1997": {"rank": 2}, - "i1998": {"rank": 3}, - "i1999": {"rank": 4}, - "i2000": {"rank": 5}, - "i2001": {"rank": 6}, - "i1995": {"rank": 1}, - "i2003": {"rank": 1}, - "i2004": {"rank": 2}, - "i2005": {"rank": 3}, - "i2002": {"rank": 2}, - "i2007": {"rank": 1}, - "i2008": {"rank": 2}, - "i2009": {"rank": 3}, - "i2006": {"rank": 3}, - "i2011": {"rank": 1}, - "i2012": {"rank": 2}, - "i2013": {"rank": 3}, - "i2010": {"rank": 4}, - "i1994": {"rank": 3}, - "i2016": {"rank": 1}, - "i2017": {"rank": 2}, - "i2018": {"rank": 3}, - "i2015": {"rank": 1}, - "i2020": {"rank": 1}, - "i2021": {"rank": 2}, - "i2022": {"rank": 3}, - "i2019": {"rank": 2}, - "i2024": {"rank": 1}, - "i2025": {"rank": 2}, - "i2026": {"rank": 3}, - "i2023": {"rank": 3}, - "i2014": {"rank": 4}, - "i2027": {"rank": 5}, - "p82": {"rank": -2}, - "i2780": {"rank": 1}, - "i2781": {"rank": 2}, - "i2782": {"rank": 3}, - "i2779": {"rank": 1}, - "i2785": {"rank": 1}, - "i2786": {"rank": 2}, - "i2787": {"rank": 3}, - "i2788": {"rank": 4}, - "i2789": {"rank": 5}, - "i2784": {"rank": 1}, - "i2791": {"rank": 1}, - "i2792": {"rank": 2}, - "i2793": {"rank": 3}, - "i2795": {"rank": 1}, - "i2796": {"rank": 2}, - "i2794": {"rank": 4}, - "i2797": {"rank": 5}, - "i2790": {"rank": 2}, - "i2799": {"rank": 1}, - "i2798": {"rank": 3}, - "i2801": {"rank": 1}, - "i2802": {"rank": 2}, - "i2803": {"rank": 3}, - "i2804": {"rank": 4}, - "i2800": {"rank": 4}, - "i2806": {"rank": 1}, - "i2807": {"rank": 2}, - "i2805": {"rank": 5}, - "i2809": {"rank": 1}, - "i2810": {"rank": 2}, - "i2808": {"rank": 6}, - "i2783": {"rank": 2}, - "i2813": {"rank": 1}, - "i2814": {"rank": 2}, - "i2812": {"rank": 1}, - "i2816": {"rank": 1}, - "i2815": {"rank": 2}, - "i2818": {"rank": 1}, - "i2817": {"rank": 3}, - "i2820": {"rank": 1}, - "i2821": {"rank": 2}, - "i2819": {"rank": 4}, - "i2823": {"rank": 1}, - "i2822": {"rank": 5}, - "i2811": {"rank": 3}, - "i2825": {"rank": 1}, - "i2826": {"rank": 2}, - "i2827": {"rank": 3}, - "i2828": {"rank": 4}, - "i2829": {"rank": 5}, - "i2830": {"rank": 6}, - "i2831": {"rank": 7}, - "i2824": {"rank": 4}, - "i2778": {"rank": 1}, - "i2835": {"rank": 1}, - "i2836": {"rank": 2}, - "i2837": {"rank": 3}, - "i2838": {"rank": 4}, - "i2839": {"rank": 5}, - "i2834": {"rank": 1}, - "i2841": {"rank": 1}, - "i2842": {"rank": 2}, - "i2843": {"rank": 3}, - "i2844": {"rank": 4}, - "i2840": {"rank": 2}, - "i2846": {"rank": 1}, - "i2847": {"rank": 2}, - "i2845": {"rank": 3}, - "i2833": {"rank": 1}, - "i2850": {"rank": 1}, - "i2851": {"rank": 2}, - "i2852": {"rank": 3}, - "i2849": {"rank": 1}, - "i2854": {"rank": 1}, - "i2855": {"rank": 2}, - "i2853": {"rank": 2}, - "i2857": {"rank": 1}, - "i2858": {"rank": 2}, - "i2856": {"rank": 3}, - "i2860": {"rank": 1}, - "i2861": {"rank": 2}, - "i2859": {"rank": 4}, - "i2848": {"rank": 2}, - "i2863": {"rank": 1}, - "i2864": {"rank": 2}, - "i2865": {"rank": 3}, - "i2866": {"rank": 4}, - "i2867": {"rank": 5}, - "i2868": {"rank": 6}, - "i2862": {"rank": 3}, - "i2832": {"rank": 2}, - "i2871": {"rank": 1}, - "i2872": {"rank": 2}, - "i2873": {"rank": 3}, - "i2874": {"rank": 4}, - "i2875": {"rank": 5}, - "i2876": {"rank": 6}, - "i2870": {"rank": 1}, - "i2878": {"rank": 1}, - "i2879": {"rank": 2}, - "i2880": {"rank": 3}, - "i2877": {"rank": 2}, - "i2882": {"rank": 1}, - "i2883": {"rank": 2}, - "i2884": {"rank": 3}, - "i2881": {"rank": 3}, - "i2886": {"rank": 1}, - "i2887": {"rank": 2}, - "i2888": {"rank": 3}, - "i2885": {"rank": 4}, - "i2869": {"rank": 3}, - "i2891": {"rank": 1}, - "i2892": {"rank": 2}, - "i2893": {"rank": 3}, - "i2890": {"rank": 1}, - "i2895": {"rank": 1}, - "i2896": {"rank": 2}, - "i2897": {"rank": 3}, - "i2894": {"rank": 2}, - "i2899": {"rank": 1}, - "i2900": {"rank": 2}, - "i2901": {"rank": 3}, - "i2898": {"rank": 3}, - "i2889": {"rank": 4}, - "i2902": {"rank": 5}, - "p89": {"rank": -1}, - "i1530": {"rank": 1}, - "i1531": {"rank": 2}, - "i1532": {"rank": 3}, - "i1529": {"rank": 1}, - "i1535": {"rank": 1}, - "i1536": {"rank": 2}, - "i1537": {"rank": 3}, - "i1538": {"rank": 4}, - "i1539": {"rank": 5}, - "i1534": {"rank": 1}, - "i1541": {"rank": 1}, - "i1542": {"rank": 2}, - "i1543": {"rank": 3}, - "i1545": {"rank": 1}, - "i1546": {"rank": 2}, - "i1544": {"rank": 4}, - "i1547": {"rank": 5}, - "i1540": {"rank": 2}, - "i1549": {"rank": 1}, - "i1548": {"rank": 3}, - "i1551": {"rank": 1}, - "i1552": {"rank": 2}, - "i1553": {"rank": 3}, - "i1554": {"rank": 4}, - "i1550": {"rank": 4}, - "i1556": {"rank": 1}, - "i1557": {"rank": 2}, - "i1555": {"rank": 5}, - "i1559": {"rank": 1}, - "i1560": {"rank": 2}, - "i1558": {"rank": 6}, - "i1533": {"rank": 2}, - "i1563": {"rank": 1}, - "i1564": {"rank": 2}, - "i1562": {"rank": 1}, - "i1566": {"rank": 1}, - "i1565": {"rank": 2}, - "i1568": {"rank": 1}, - "i1567": {"rank": 3}, - "i1570": {"rank": 1}, - "i1571": {"rank": 2}, - "i1569": {"rank": 4}, - "i1573": {"rank": 1}, - "i1572": {"rank": 5}, - "i1561": {"rank": 3}, - "i1575": {"rank": 1}, - "i1576": {"rank": 2}, - "i1577": {"rank": 3}, - "i1578": {"rank": 4}, - "i1579": {"rank": 5}, - "i1580": {"rank": 6}, - "i1581": {"rank": 7}, - "i1574": {"rank": 4}, - "i1528": {"rank": 1}, - "i1585": {"rank": 1}, - "i1586": {"rank": 2}, - "i1587": {"rank": 3}, - "i1588": {"rank": 4}, - "i1589": {"rank": 5}, - "i1584": {"rank": 1}, - "i1591": {"rank": 1}, - "i1592": {"rank": 2}, - "i1593": {"rank": 3}, - "i1594": {"rank": 4}, - "i1590": {"rank": 2}, - "i1596": {"rank": 1}, - "i1597": {"rank": 2}, - "i1595": {"rank": 3}, - "i1583": {"rank": 1}, - "i1600": {"rank": 1}, - "i1601": {"rank": 2}, - "i1602": {"rank": 3}, - "i1599": {"rank": 1}, - "i1604": {"rank": 1}, - "i1605": {"rank": 2}, - "i1603": {"rank": 2}, - "i1607": {"rank": 1}, - "i1608": {"rank": 2}, - "i1606": {"rank": 3}, - "i1610": {"rank": 1}, - "i1611": {"rank": 2}, - "i1609": {"rank": 4}, - "i1598": {"rank": 2}, - "i1613": {"rank": 1}, - "i1614": {"rank": 2}, - "i1615": {"rank": 3}, - "i1616": {"rank": 4}, - "i1617": {"rank": 5}, - "i1618": {"rank": 6}, - "i1612": {"rank": 3}, - "i1582": {"rank": 2}, - "i1621": {"rank": 1}, - "i1622": {"rank": 2}, - "i1623": {"rank": 3}, - "i1624": {"rank": 4}, - "i1625": {"rank": 5}, - "i1626": {"rank": 6}, - "i1620": {"rank": 1}, - "i1628": {"rank": 1}, - "i1629": {"rank": 2}, - "i1630": {"rank": 3}, - "i1627": {"rank": 2}, - "i1632": {"rank": 1}, - "i1633": {"rank": 2}, - "i1634": {"rank": 3}, - "i1631": {"rank": 3}, - "i1636": {"rank": 1}, - "i1637": {"rank": 2}, - "i1638": {"rank": 3}, - "i1635": {"rank": 4}, - "i1619": {"rank": 3}, - "i1641": {"rank": 1}, - "i1642": {"rank": 2}, - "i1643": {"rank": 3}, - "i1640": {"rank": 1}, - "i1645": {"rank": 1}, - "i1646": {"rank": 2}, - "i1647": {"rank": 3}, - "i1644": {"rank": 2}, - "i1649": {"rank": 1}, - "i1650": {"rank": 2}, - "i1651": {"rank": 3}, - "i1648": {"rank": 3}, - "i1639": {"rank": 4}, - "i1652": {"rank": 5, "position": [727.5, 333.5, 1]}, - "p79": {"rank": 1}, - "i1405": {"rank": 1}, - "i1406": {"rank": 2}, - "i1407": {"rank": 3}, - "i1404": {"rank": 1}, - "i1410": {"rank": 1}, - "i1411": {"rank": 2}, - "i1412": {"rank": 3}, - "i1413": {"rank": 4}, - "i1414": {"rank": 5}, - "i1409": {"rank": 1}, - "i1416": {"rank": 1}, - "i1417": {"rank": 2}, - "i1418": {"rank": 3}, - "i1420": {"rank": 1}, - "i1421": {"rank": 2}, - "i1419": {"rank": 4}, - "i1422": {"rank": 5}, - "i1415": {"rank": 2}, - "i1424": {"rank": 1}, - "i1423": {"rank": 3}, - "i1426": {"rank": 1}, - "i1427": {"rank": 2}, - "i1428": {"rank": 3}, - "i1429": {"rank": 4}, - "i1425": {"rank": 4}, - "i1431": {"rank": 1}, - "i1432": {"rank": 2}, - "i1430": {"rank": 5}, - "i1434": {"rank": 1}, - "i1435": {"rank": 2}, - "i1433": {"rank": 6}, - "i1408": {"rank": 2}, - "i1438": {"rank": 1}, - "i1439": {"rank": 2}, - "i1437": {"rank": 1}, - "i1441": {"rank": 1}, - "i1440": {"rank": 2}, - "i1443": {"rank": 1}, - "i1442": {"rank": 3}, - "i1445": {"rank": 1}, - "i1446": {"rank": 2}, - "i1444": {"rank": 4}, - "i1448": {"rank": 1}, - "i1447": {"rank": 5}, - "i1436": {"rank": 3}, - "i1450": {"rank": 1}, - "i1451": {"rank": 2}, - "i1452": {"rank": 3}, - "i1453": {"rank": 4}, - "i1454": {"rank": 5}, - "i1455": {"rank": 6}, - "i1456": {"rank": 7}, - "i1449": {"rank": 4}, - "i1403": {"rank": 1}, - "i1460": {"rank": 1}, - "i1461": {"rank": 2}, - "i1462": {"rank": 3}, - "i1463": {"rank": 4}, - "i1464": {"rank": 5}, - "i1459": {"rank": 1}, - "i1466": {"rank": 1}, - "i1467": {"rank": 2}, - "i1468": {"rank": 3}, - "i1469": {"rank": 4}, - "i1465": {"rank": 2}, - "i1471": {"rank": 1}, - "i1472": {"rank": 2}, - "i1470": {"rank": 3}, - "i1458": {"rank": 1}, - "i1475": {"rank": 1}, - "i1476": {"rank": 2}, - "i1477": {"rank": 3}, - "i1474": {"rank": 1}, - "i1479": {"rank": 1}, - "i1480": {"rank": 2}, - "i1478": {"rank": 2}, - "i1482": {"rank": 1}, - "i1483": {"rank": 2}, - "i1481": {"rank": 3}, - "i1485": {"rank": 1}, - "i1486": {"rank": 2}, - "i1484": {"rank": 4}, - "i1473": {"rank": 2}, - "i1488": {"rank": 1}, - "i1489": {"rank": 2}, - "i1490": {"rank": 3}, - "i1491": {"rank": 4}, - "i1492": {"rank": 5}, - "i1493": {"rank": 6}, - "i1487": {"rank": 3}, - "i1457": {"rank": 2}, - "i1496": {"rank": 1}, - "i1497": {"rank": 2}, - "i1498": {"rank": 3}, - "i1499": {"rank": 4}, - "i1500": {"rank": 5}, - "i1501": {"rank": 6}, - "i1495": {"rank": 1}, - "i1503": {"rank": 1}, - "i1504": {"rank": 2}, - "i1505": {"rank": 3}, - "i1502": {"rank": 2}, - "i1507": {"rank": 1}, - "i1508": {"rank": 2}, - "i1509": {"rank": 3}, - "i1506": {"rank": 3}, - "i1511": {"rank": 1}, - "i1512": {"rank": 2}, - "i1513": {"rank": 3}, - "i1510": {"rank": 4}, - "i1494": {"rank": 3}, - "i1516": {"rank": 1}, - "i1517": {"rank": 2}, - "i1518": {"rank": 3}, - "i1515": {"rank": 1}, - "i1520": {"rank": 1}, - "i1521": {"rank": 2}, - "i1522": {"rank": 3}, - "i1519": {"rank": 2}, - "i1524": {"rank": 1}, - "i1525": {"rank": 2}, - "i1526": {"rank": 3}, - "i1523": {"rank": 3}, - "i1514": {"rank": 4}, - "i1527": {"rank": 5}, - "p78": {"rank": 2}, - "i3363": {"rank": 1}, - "i3364": {"rank": 2}, - "i3365": {"rank": 3}, - "i3366": {"rank": 4}, - "i2155": {"rank": 1}, - "i2156": {"rank": 2}, - "i2157": {"rank": 3}, - "i2154": {"rank": 1}, - "i2160": {"rank": 1}, - "i2161": {"rank": 2}, - "i2162": {"rank": 3}, - "i2163": {"rank": 4}, - "i2164": {"rank": 5}, - "i2159": {"rank": 1}, - "i2166": {"rank": 1}, - "i2167": {"rank": 2}, - "i2168": {"rank": 3}, - "i2170": {"rank": 1}, - "i2171": {"rank": 2}, - "i2169": {"rank": 4}, - "i2172": {"rank": 5}, - "i2165": {"rank": 2}, - "i2174": {"rank": 1}, - "i2173": {"rank": 3}, - "i2176": {"rank": 1}, - "i2177": {"rank": 2}, - "i2178": {"rank": 3}, - "i2179": {"rank": 4}, - "i2175": {"rank": 4}, - "i2181": {"rank": 1}, - "i2182": {"rank": 2}, - "i2180": {"rank": 5}, - "i2184": {"rank": 1}, - "i2185": {"rank": 2}, - "i2183": {"rank": 6}, - "i2158": {"rank": 2}, - "i2188": {"rank": 1}, - "i2189": {"rank": 2}, - "i2187": {"rank": 1}, - "i2191": {"rank": 1}, - "i2190": {"rank": 2}, - "i2193": {"rank": 1}, - "i2192": {"rank": 3}, - "i2195": {"rank": 1}, - "i2196": {"rank": 2}, - "i2194": {"rank": 4}, - "i2198": {"rank": 1}, - "i2197": {"rank": 5}, - "i2186": {"rank": 3}, - "i2200": {"rank": 1}, - "i2201": {"rank": 2}, - "i2202": {"rank": 3}, - "i2203": {"rank": 4}, - "i2204": {"rank": 5}, - "i2205": {"rank": 6}, - "i2206": {"rank": 7}, - "i2199": {"rank": 4}, - "i2153": {"rank": 1}, - "i2210": {"rank": 1}, - "i2211": {"rank": 2}, - "i2212": {"rank": 3}, - "i2213": {"rank": 4}, - "i2214": {"rank": 5}, - "i2209": {"rank": 1}, - "i2216": {"rank": 1}, - "i2217": {"rank": 2}, - "i2218": {"rank": 3}, - "i2219": {"rank": 4}, - "i2215": {"rank": 2}, - "i2221": {"rank": 1}, - "i2222": {"rank": 2}, - "i2220": {"rank": 3}, - "i2208": {"rank": 1}, - "i2225": {"rank": 1}, - "i2226": {"rank": 2}, - "i2227": {"rank": 3}, - "i2224": {"rank": 1}, - "i2229": {"rank": 1}, - "i2230": {"rank": 2}, - "i2228": {"rank": 2}, - "i2232": {"rank": 1}, - "i2233": {"rank": 2}, - "i2231": {"rank": 3}, - "i2235": {"rank": 1}, - "i2236": {"rank": 2}, - "i2234": {"rank": 4}, - "i2223": {"rank": 2}, - "i2238": {"rank": 1}, - "i2239": {"rank": 2}, - "i2240": {"rank": 3}, - "i2241": {"rank": 4}, - "i2242": {"rank": 5}, - "i2243": {"rank": 6}, - "i2237": {"rank": 3}, - "i2207": {"rank": 2}, - "i2246": {"rank": 1}, - "i2247": {"rank": 2}, - "i2248": {"rank": 3}, - "i2249": {"rank": 4}, - "i2250": {"rank": 5}, - "i2251": {"rank": 6}, - "i2245": {"rank": 1}, - "i2253": {"rank": 1}, - "i2254": {"rank": 2}, - "i2255": {"rank": 3}, - "i2252": {"rank": 2}, - "i2257": {"rank": 1}, - "i2258": {"rank": 2}, - "i2259": {"rank": 3}, - "i2256": {"rank": 3}, - "i2261": {"rank": 1}, - "i2262": {"rank": 2}, - "i2263": {"rank": 3}, - "i2260": {"rank": 4}, - "i2244": {"rank": 3}, - "i2266": {"rank": 1}, - "i2267": {"rank": 2}, - "i2268": {"rank": 3}, - "i2265": {"rank": 1}, - "i2270": {"rank": 1}, - "i2271": {"rank": 2}, - "i2272": {"rank": 3}, - "i2269": {"rank": 2}, - "i2274": {"rank": 1}, - "i2275": {"rank": 2}, - "i2276": {"rank": 3}, - "i2273": {"rank": 3}, - "i2264": {"rank": 4}, - "i2277": {"rank": 5}, - "p84": {"rank": 3}, - "i2405": {"rank": 1}, - "i2406": {"rank": 2}, - "i2407": {"rank": 3}, - "i2404": {"rank": 1}, - "i2410": {"rank": 1}, - "i2411": {"rank": 2}, - "i2412": {"rank": 3}, - "i2413": {"rank": 4}, - "i2414": {"rank": 5}, - "i2409": {"rank": 1}, - "i2416": {"rank": 1}, - "i2417": {"rank": 2}, - "i2418": {"rank": 3}, - "i2420": {"rank": 1}, - "i2421": {"rank": 2}, - "i2419": {"rank": 4}, - "i2422": {"rank": 5}, - "i2415": {"rank": 2}, - "i2424": {"rank": 1}, - "i2423": {"rank": 3}, - "i2426": {"rank": 1}, - "i2427": {"rank": 2}, - "i2428": {"rank": 3}, - "i2429": {"rank": 4}, - "i2425": {"rank": 4}, - "i2431": {"rank": 1}, - "i2432": {"rank": 2}, - "i2430": {"rank": 5}, - "i2434": {"rank": 1}, - "i2435": {"rank": 2}, - "i2433": {"rank": 6}, - "i2408": {"rank": 2}, - "i2438": {"rank": 1}, - "i2439": {"rank": 2}, - "i2437": {"rank": 1}, - "i2441": {"rank": 1}, - "i2440": {"rank": 2}, - "i2443": {"rank": 1}, - "i2442": {"rank": 3}, - "i2445": {"rank": 1}, - "i2446": {"rank": 2}, - "i2444": {"rank": 4}, - "i2448": {"rank": 1}, - "i2447": {"rank": 5}, - "i2436": {"rank": 3}, - "i2450": {"rank": 1}, - "i2451": {"rank": 2}, - "i2452": {"rank": 3}, - "i2453": {"rank": 4}, - "i2454": {"rank": 5}, - "i2455": {"rank": 6}, - "i2456": {"rank": 7}, - "i2449": {"rank": 4}, - "i2403": {"rank": 1}, - "i2460": {"rank": 1}, - "i2461": {"rank": 2}, - "i2462": {"rank": 3}, - "i2463": {"rank": 4}, - "i2464": {"rank": 5}, - "i2459": {"rank": 1}, - "i2466": {"rank": 1}, - "i2467": {"rank": 2}, - "i2468": {"rank": 3}, - "i2469": {"rank": 4}, - "i2465": {"rank": 2}, - "i2471": {"rank": 1}, - "i2472": {"rank": 2}, - "i2470": {"rank": 3}, - "i2458": {"rank": 1}, - "i2475": {"rank": 1}, - "i2476": {"rank": 2}, - "i2477": {"rank": 3}, - "i2474": {"rank": 1}, - "i2479": {"rank": 1}, - "i2480": {"rank": 2}, - "i2478": {"rank": 2}, - "i2482": {"rank": 1}, - "i2483": {"rank": 2}, - "i2481": {"rank": 3}, - "i2485": {"rank": 1}, - "i2486": {"rank": 2}, - "i2484": {"rank": 4}, - "i2473": {"rank": 2}, - "i2488": {"rank": 1}, - "i2489": {"rank": 2}, - "i2490": {"rank": 3}, - "i2491": {"rank": 4}, - "i2492": {"rank": 5}, - "i2493": {"rank": 6}, - "i2487": {"rank": 3}, - "i2457": {"rank": 2}, - "i2496": {"rank": 1}, - "i2497": {"rank": 2}, - "i2498": {"rank": 3}, - "i2499": {"rank": 4}, - "i2500": {"rank": 5}, - "i2501": {"rank": 6}, - "i2495": {"rank": 1}, - "i2503": {"rank": 1}, - "i2504": {"rank": 2}, - "i2505": {"rank": 3}, - "i2502": {"rank": 2}, - "i2507": {"rank": 1}, - "i2508": {"rank": 2}, - "i2509": {"rank": 3}, - "i2506": {"rank": 3}, - "i2511": {"rank": 1}, - "i2512": {"rank": 2}, - "i2513": {"rank": 3}, - "i2510": {"rank": 4}, - "i2494": {"rank": 3}, - "i2516": {"rank": 1}, - "i2517": {"rank": 2}, - "i2518": {"rank": 3}, - "i2515": {"rank": 1}, - "i2514": {"rank": 4}, - "i2527": {"rank": 5, "position": [291.99426556054686, 101.83236694335938, 1]}, - "p86": {"rank": 4}, - "i2030": {"rank": 1}, - "i2031": {"rank": 2}, - "i2032": {"rank": 3}, - "i2029": {"rank": 1}, - "i2035": {"rank": 1}, - "i2036": {"rank": 2}, - "i2037": {"rank": 3}, - "i2038": {"rank": 4}, - "i2039": {"rank": 5}, - "i2034": {"rank": 1}, - "i2033": {"rank": 2}, - "i2063": {"rank": 1}, - "i2064": {"rank": 2}, - "i2062": {"rank": 1}, - "i2068": {"rank": 1}, - "i2067": {"rank": 2}, - "i2073": {"rank": 1}, - "i2072": {"rank": 3}, - "i2061": {"rank": 3}, - "i2075": {"rank": 1}, - "i2076": {"rank": 2}, - "i2077": {"rank": 3}, - "i2078": {"rank": 4}, - "i2079": {"rank": 5}, - "i2080": {"rank": 6}, - "i2081": {"rank": 7}, - "i2074": {"rank": 4}, - "i2524": {"rank": 1}, - "i2525": {"rank": 2}, - "i2526": {"rank": 3}, - "i2523": {"rank": 5}, - "i2028": {"rank": 1}, - "i2085": {"rank": 1}, - "i2086": {"rank": 2}, - "i2087": {"rank": 3}, - "i2088": {"rank": 4}, - "i2089": {"rank": 5}, - "i2084": {"rank": 1}, - "i2091": {"rank": 1}, - "i2092": {"rank": 2}, - "i2093": {"rank": 3}, - "i2094": {"rank": 4}, - "i2090": {"rank": 2}, - "i2096": {"rank": 1}, - "i2097": {"rank": 2}, - "i2095": {"rank": 3}, - "i2083": {"rank": 1}, - "i2100": {"rank": 1}, - "i2101": {"rank": 2}, - "i2102": {"rank": 3}, - "i2099": {"rank": 1}, - "i2104": {"rank": 1}, - "i2105": {"rank": 2}, - "i2103": {"rank": 2}, - "i2107": {"rank": 1}, - "i2108": {"rank": 2}, - "i2106": {"rank": 3}, - "i2110": {"rank": 1}, - "i2111": {"rank": 2}, - "i2109": {"rank": 4}, - "i2098": {"rank": 2}, - "i2113": {"rank": 1}, - "i2114": {"rank": 2}, - "i2115": {"rank": 3}, - "i2116": {"rank": 4}, - "i2117": {"rank": 5}, - "i2118": {"rank": 6}, - "i2112": {"rank": 3}, - "i2082": {"rank": 1}, - "i2121": {"rank": 1}, - "i2122": {"rank": 2}, - "i2123": {"rank": 3}, - "i2124": {"rank": 4}, - "i2125": {"rank": 5}, - "i2126": {"rank": 6}, - "i2120": {"rank": 2}, - "i2132": {"rank": 1}, - "i2133": {"rank": 2}, - "i2134": {"rank": 3}, - "i2131": {"rank": 3}, - "i2136": {"rank": 1}, - "i2137": {"rank": 2}, - "i2138": {"rank": 3}, - "i2135": {"rank": 4}, - "i2119": {"rank": 2}, - "i2141": {"rank": 1}, - "i2142": {"rank": 2}, - "i2143": {"rank": 3}, - "i2140": {"rank": 1}, - "i2145": {"rank": 1}, - "i2146": {"rank": 2}, - "i2147": {"rank": 3}, - "i2144": {"rank": 2}, - "i2149": {"rank": 1}, - "i2150": {"rank": 2}, - "i2151": {"rank": 3}, - "i2148": {"rank": 3}, - "i2139": {"rank": 3}, - "i2152": {"rank": 4}, - "i2520": {"rank": 1}, - "i2521": {"rank": 2}, - "i2522": {"rank": 3}, - "i2519": {"rank": 5}, - "p83": {"rank": 5}, - "i1780": {"rank": 1}, - "i1781": {"rank": 2}, - "i1782": {"rank": 3}, - "i1779": {"rank": 1}, - "i1785": {"rank": 1}, - "i1786": {"rank": 2}, - "i1787": {"rank": 3}, - "i1788": {"rank": 4}, - "i1789": {"rank": 5}, - "i1784": {"rank": 1}, - "i1791": {"rank": 1}, - "i1792": {"rank": 2}, - "i1793": {"rank": 3}, - "i1795": {"rank": 1}, - "i1796": {"rank": 2}, - "i1794": {"rank": 4}, - "i1797": {"rank": 5}, - "i1790": {"rank": 2}, - "i1799": {"rank": 1}, - "i1798": {"rank": 3}, - "i1801": {"rank": 1}, - "i1802": {"rank": 2}, - "i1803": {"rank": 3}, - "i1804": {"rank": 4}, - "i1800": {"rank": 4}, - "i1806": {"rank": 1}, - "i1807": {"rank": 2}, - "i1805": {"rank": 5}, - "i1809": {"rank": 1}, - "i1810": {"rank": 2}, - "i1808": {"rank": 6}, - "i1783": {"rank": 2}, - "i1813": {"rank": 1}, - "i1814": {"rank": 2}, - "i1812": {"rank": 1}, - "i1816": {"rank": 1}, - "i1815": {"rank": 2}, - "i1818": {"rank": 1}, - "i1817": {"rank": 3}, - "i1820": {"rank": 1}, - "i1821": {"rank": 2}, - "i1819": {"rank": 4}, - "i1823": {"rank": 1}, - "i1822": {"rank": 5}, - "i1811": {"rank": 3}, - "i1825": {"rank": 1}, - "i1826": {"rank": 2}, - "i1827": {"rank": 3}, - "i1828": {"rank": 4}, - "i1829": {"rank": 5}, - "i1830": {"rank": 6}, - "i1831": {"rank": 7}, - "i1824": {"rank": 4}, - "i1778": {"rank": 1}, - "i1835": {"rank": 1}, - "i1836": {"rank": 2}, - "i1837": {"rank": 3}, - "i1838": {"rank": 4}, - "i1839": {"rank": 5}, - "i1834": {"rank": 1}, - "i1841": {"rank": 1}, - "i1842": {"rank": 2}, - "i1843": {"rank": 3}, - "i1844": {"rank": 4}, - "i1840": {"rank": 2}, - "i1846": {"rank": 1}, - "i1847": {"rank": 2}, - "i1845": {"rank": 3}, - "i1833": {"rank": 1}, - "i1850": {"rank": 1}, - "i1851": {"rank": 2}, - "i1852": {"rank": 3}, - "i1849": {"rank": 1}, - "i1854": {"rank": 1}, - "i1855": {"rank": 2}, - "i1853": {"rank": 2}, - "i1857": {"rank": 1}, - "i1858": {"rank": 2}, - "i1856": {"rank": 3}, - "i1860": {"rank": 1}, - "i1861": {"rank": 2}, - "i1859": {"rank": 4}, - "i1848": {"rank": 2}, - "i1863": {"rank": 1}, - "i1864": {"rank": 2}, - "i1865": {"rank": 3}, - "i1866": {"rank": 4}, - "i1867": {"rank": 5}, - "i1868": {"rank": 6}, - "i1862": {"rank": 3}, - "i1832": {"rank": 2}, - "i1871": {"rank": 1}, - "i1872": {"rank": 2}, - "i1873": {"rank": 3}, - "i1874": {"rank": 4}, - "i1875": {"rank": 5}, - "i1876": {"rank": 6}, - "i1870": {"rank": 1}, - "i1878": {"rank": 1}, - "i1879": {"rank": 2}, - "i1880": {"rank": 3}, - "i1877": {"rank": 2}, - "i1882": {"rank": 1}, - "i1883": {"rank": 2}, - "i1884": {"rank": 3}, - "i1881": {"rank": 3}, - "i1886": {"rank": 1}, - "i1887": {"rank": 2}, - "i1888": {"rank": 3}, - "i1885": {"rank": 4}, - "i1869": {"rank": 3}, - "i1891": {"rank": 1}, - "i1892": {"rank": 2}, - "i1893": {"rank": 3}, - "i1890": {"rank": 1}, - "i1895": {"rank": 1}, - "i1896": {"rank": 2}, - "i1897": {"rank": 3}, - "i1894": {"rank": 2}, - "i1899": {"rank": 1}, - "i1900": {"rank": 2}, - "i1901": {"rank": 3}, - "i1898": {"rank": 3}, - "i1889": {"rank": 4}, - "i1902": {"rank": 5}, - "p81": {"rank": 6}, - "p144": {"rank": 7}, - "p108": {} - }; - var layout = { - - - - - "i29165":{"rank":"1"}, - "i31468":{"rank":"2"}, - "i5672":{"rank":"-8"}, - "i22466":{"rank":"1"}, - "i22467":{"rank":"2"}, - "i22471":{"rank":"3"}, - "i22498":{"rank":"4"}, - "i22499":{"rank":"1"}, - "i22755":{"rank":"2"}, - "i23301":{"rank":"3"}, - "i25078":{"rank":"4"}, - "i26106":{"rank":"5"}, - "i26324":{"rank":"1"}, - "i28175":{"rank":"2"}, - "i26323":{"rank":"6"}, - "i26699":{"rank":"7"}, - "i26735":{"rank":"8"}, - "i26830":{"rank":"9"}, - "i26927":{"rank":"10"}, - "i27825":{"rank":"11"}, - "i27854":{"rank":"12"}, - "i28165":{"rank":"13"}, - "i23300":{"rank":"5"}, - "i22497":{"rank":"1"}, - "i25071":{"rank":"2"}, - "i25072":{"rank":"3"}, - "i25912":{"rank":"4"}, - "i25993":{"rank":"5"}, - "i23348":{"rank":"6"}, - "i25066":{"rank":"1"}, - "i25067":{"rank":"2"}, - "i26292":{"rank":"3"}, - "i23349":{"rank":"7"}, - "i25127":{"rank":"1"}, - "i25146":{"rank":"2"}, - "i26700":{"rank":"3"}, - "i23350":{"rank":"8"}, - "i25068":{"rank":"1"}, - "i25165":{"rank":"2"}, - "i25910":{"rank":"3"}, - "i25932":{"rank":"4"}, - "i26364":{"rank":"5"}, - "i26366":{"rank":"6"}, - "i26371":{"rank":"7"}, - "i27760":{"rank":"8"}, - "i27795":{"rank":"9"}, - "i27796":{"rank":"10"}, - "i28158":{"rank":"11"}, - "i28917":{"rank":"12"}, - "i29009":{"rank":"13"}, - "i29073":{"rank":"14"}, - "i23351":{"rank":"9"}, - "i23367":{"rank":"1"}, - "i23366":{"rank":"10"}, - "i23615":{"rank":"1"}, - "i23620":{"rank":"2"}, - "i23621":{"rank":"3"}, - "i23622":{"rank":"4"}, - "i25166":{"rank":"5"}, - "i25196":{"rank":"6"}, - "i25281":{"rank":"7"}, - "i26734":{"rank":"8"}, - "i27108":{"rank":"9"}, - "i27797":{"rank":"10"}, - "i28912":{"rank":"11"}, - "i23614":{"rank":"11"}, - "i23623":{"rank":"1"}, - "i23624":{"rank":"2"}, - "i25700":{"rank":"3"}, - "i23616":{"rank":"12"}, - "i25161":{"rank":"1"}, - "i25162":{"rank":"2"}, - "i25163":{"rank":"3"}, - "i25164":{"rank":"4"}, - "i25167":{"rank":"5"}, - "i25169":{"rank":"6"}, - "i25973":{"rank":"7"}, - "i25992":{"rank":"8"}, - "i26351":{"rank":"9"}, - "i26738":{"rank":"10"}, - "i23617":{"rank":"13"}, - "i26331":{"rank":"1"}, - "i28176":{"rank":"2"}, - "i23618":{"rank":"14"}, - "i25154":{"rank":"1"}, - "i25155":{"rank":"2"}, - "i25156":{"rank":"3"}, - "i25157":{"rank":"4"}, - "i25158":{"rank":"5"}, - "i25918":{"rank":"6"}, - "i25971":{"rank":"7"}, - "i26828":{"rank":"8"}, - "i23619":{"rank":"15"}, - "i23828":{"rank":"1"}, - "i24957":{"rank":"2"}, - "i25170":{"rank":"3"}, - "i25390":{"rank":"4"}, - "i25402":{"rank":"5"}, - "i25942":{"rank":"6"}, - "i25984":{"rank":"7"}, - "i26291":{"rank":"8"}, - "i26322":{"rank":"9"}, - "i26325":{"rank":"10"}, - "i26330":{"rank":"11"}, - "i27886":{"rank":"12"}, - "i28157":{"rank":"13"}, - "i28875":{"rank":"14"}, - "i23827":{"rank":"16"}, - "i23876":{"rank":"1"}, - "i25032":{"rank":"2"}, - "i27793":{"rank":"3"}, - "i23866":{"rank":"1"}, - "i23878":{"rank":"1"}, - "i24245":{"rank":"2"}, - "i26220":{"rank":"3"}, - "i26221":{"rank":"4"}, - "i27792":{"rank":"5"}, - "i27826":{"rank":"6"}, - "i27950":{"rank":"7"}, - "i28350":{"rank":"8"}, - "i28896":{"rank":"9"}, - "i23867":{"rank":"2"}, - "i23869":{"rank":"3"}, - "i27895":{"rank":"1"}, - "i23870":{"rank":"4"}, - "i23888":{"rank":"5"}, - "i25120":{"rank":"6"}, - "i27893":{"rank":"1"}, - "i27894":{"rank":"2"}, - "i25313":{"rank":"7"}, - "i25349":{"rank":"8"}, - "i27794":{"rank":"9"}, - "i28159":{"rank":"10"}, - "i23865":{"rank":"17"}, - "i23877":{"rank":"18"}, - "i25168":{"rank":"1"}, - "i25233":{"rank":"2"}, - "i25235":{"rank":"3"}, - "i24976":{"rank":"19"}, - "i25160":{"rank":"1"}, - "i25972":{"rank":"2"}, - "i24977":{"rank":"20"}, - "i25258":{"rank":"1"}, - "i26736":{"rank":"2"}, - "i24978":{"rank":"21"}, - "i25159":{"rank":"1"}, - "i24979":{"rank":"22"}, - "i25034":{"rank":"1"}, - "i25036":{"rank":"2"}, - "i25038":{"rank":"3"}, - "i25039":{"rank":"4"}, - "i25040":{"rank":"5"}, - "i25041":{"rank":"6"}, - "i25042":{"rank":"7"}, - "i25043":{"rank":"8"}, - "i25044":{"rank":"9"}, - "i25045":{"rank":"10"}, - "i25046":{"rank":"11"}, - "i25047":{"rank":"12"}, - "i25048":{"rank":"13"}, - "i25049":{"rank":"14"}, - "i25050":{"rank":"15"}, - "i25052":{"rank":"16"}, - "i25053":{"rank":"17"}, - "i25054":{"rank":"18"}, - "i25056":{"rank":"19"}, - "i25058":{"rank":"20"}, - "i25059":{"rank":"21"}, - "i25282":{"rank":"22"}, - "i25312":{"rank":"23"}, - "i26147":{"rank":"24"}, - "i26224":{"rank":"25"}, - "i26225":{"rank":"26"}, - "i27887":{"rank":"27"}, - "i28895":{"rank":"28"}, - "i28949":{"rank":"29"}, - "i25033":{"rank":"23"}, - "i25035":{"rank":"1"}, - "i25037":{"rank":"2"}, - "i25057":{"rank":"3"}, - "i25060":{"rank":"4"}, - "i25061":{"rank":"5"}, - "i25062":{"rank":"6"}, - "i25063":{"rank":"7"}, - "i25064":{"rank":"8"}, - "i25065":{"rank":"9"}, - "i25051":{"rank":"24"}, - "i25070":{"rank":"1"}, - "i25069":{"rank":"25"}, - "i25915":{"rank":"1"}, - "i25914":{"rank":"26"}, - "i25917":{"rank":"1"}, - "i25916":{"rank":"27"}, - "i28943":{"rank":"28"}, - "i28944":{"rank":"29"}, - "i28950":{"rank":"30"}, - "i29044":{"rank":"1"}, - "i29051":{"rank":"2"}, - "i29052":{"rank":"3"}, - "i29050":{"rank":"31"}, - "i29298":{"rank":"32"}, - "i30050":{"rank":"1"}, - "i30051":{"rank":"2"}, - "i30052":{"rank":"3"}, - "i30053":{"rank":"4"}, - "i30054":{"rank":"5"}, - "i30049":{"rank":"33"}, - "p583":{"rank":"1"}, - "i19926":{"rank":"1"}, - "i19937":{"rank":"1"}, - "i19927":{"rank":"1"}, - "i19928":{"rank":"2"}, - "i19929":{"rank":"3"}, - "i19930":{"rank":"4"}, - "i19931":{"rank":"5"}, - "i19932":{"rank":"6"}, - "i19933":{"rank":"7"}, - "i19934":{"rank":"8"}, - "i19935":{"rank":"9"}, - "i19936":{"rank":"10"}, - "i19938":{"rank":"2"}, - "i19925":{"rank":"1"}, - "p554":{"rank":"2"}, - "i26414":{"rank":"1"}, - "i26487":{"rank":"1"}, - "i26488":{"rank":"2"}, - "i26489":{"rank":"3"}, - "i26580":{"rank":"4"}, - "i26415":{"rank":"2"}, - "i26416":{"rank":"3"}, - "i26417":{"rank":"4"}, - "i26398":{"rank":"1"}, - "i26401":{"rank":"1"}, - "i26402":{"rank":"2"}, - "i26404":{"rank":"3"}, - "i26407":{"rank":"4"}, - "i26411":{"rank":"5"}, - "i26412":{"rank":"6"}, - "i26422":{"rank":"7"}, - "i26832":{"rank":"1"}, - "i26833":{"rank":"2"}, - "i26525":{"rank":"8"}, - "i26528":{"rank":"9"}, - "i26556":{"rank":"10"}, - "i26399":{"rank":"2"}, - "i26585":{"rank":"1"}, - "i26586":{"rank":"2"}, - "i27746":{"rank":"3"}, - "i28064":{"rank":"4"}, - "i26400":{"rank":"3"}, - "i26503":{"rank":"1"}, - "i26557":{"rank":"2"}, - "i26449":{"rank":"4"}, - "i27726":{"rank":"1"}, - "i27728":{"rank":"2"}, - "i27730":{"rank":"3"}, - "i27725":{"rank":"5"}, - "p615":{"rank":"3"}, - "i18940":{"rank":"1"}, - "i19177":{"rank":"1"}, - "i19175":{"rank":"1"}, - "i19172":{"rank":"2"}, - "i24714":{"rank":"1"}, - "i24715":{"rank":"2"}, - "i24716":{"rank":"3"}, - "i24717":{"rank":"4"}, - "i24718":{"rank":"5"}, - "i24719":{"rank":"6"}, - "i24720":{"rank":"7"}, - "i24721":{"rank":"8"}, - "i24722":{"rank":"9"}, - "i24723":{"rank":"10"}, - "i24724":{"rank":"11"}, - "i24725":{"rank":"12"}, - "i24726":{"rank":"13"}, - "i24727":{"rank":"14"}, - "i24728":{"rank":"15"}, - "i24729":{"rank":"16"}, - "i24730":{"rank":"17"}, - "i24731":{"rank":"18"}, - "i24732":{"rank":"19"}, - "i24733":{"rank":"20"}, - "i24734":{"rank":"21"}, - "i24735":{"rank":"22"}, - "i24736":{"rank":"23"}, - "i24737":{"rank":"24"}, - "i24738":{"rank":"25"}, - "i24739":{"rank":"26"}, - "i24740":{"rank":"27"}, - "i24713":{"rank":"1"}, - "i24712":{"rank":"3"}, - "i24741":{"rank":"4"}, - "i24745":{"rank":"1"}, - "i24746":{"rank":"2"}, - "i24747":{"rank":"3"}, - "i24744":{"rank":"1"}, - "i24749":{"rank":"1"}, - "i24750":{"rank":"2"}, - "i24751":{"rank":"3"}, - "i24748":{"rank":"2"}, - "i24753":{"rank":"1"}, - "i24754":{"rank":"2"}, - "i24755":{"rank":"3"}, - "i24752":{"rank":"3"}, - "i24743":{"rank":"1"}, - "i24762":{"rank":"1"}, - "i24763":{"rank":"2"}, - "i24764":{"rank":"3"}, - "i24761":{"rank":"1"}, - "i24766":{"rank":"1"}, - "i24767":{"rank":"2"}, - "i24768":{"rank":"3"}, - "i24765":{"rank":"2"}, - "i24770":{"rank":"1"}, - "i24771":{"rank":"2"}, - "i24772":{"rank":"3"}, - "i24773":{"rank":"4"}, - "i24769":{"rank":"3"}, - "i24776":{"rank":"1"}, - "i24777":{"rank":"2"}, - "i24778":{"rank":"3"}, - "i24775":{"rank":"1"}, - "i24779":{"rank":"2"}, - "i24774":{"rank":"4"}, - "i24760":{"rank":"2"}, - "i24784":{"rank":"1"}, - "i24785":{"rank":"2"}, - "i24783":{"rank":"1"}, - "i24782":{"rank":"1"}, - "i24790":{"rank":"1"}, - "i24791":{"rank":"2"}, - "i24792":{"rank":"3"}, - "i24789":{"rank":"2"}, - "i24794":{"rank":"1"}, - "i24795":{"rank":"2"}, - "i24796":{"rank":"3"}, - "i24793":{"rank":"3"}, - "i24798":{"rank":"1"}, - "i24799":{"rank":"2"}, - "i24800":{"rank":"3"}, - "i24797":{"rank":"4"}, - "i24802":{"rank":"1"}, - "i24803":{"rank":"2"}, - "i24804":{"rank":"3"}, - "i24801":{"rank":"5"}, - "i24806":{"rank":"1"}, - "i24807":{"rank":"2"}, - "i24808":{"rank":"3"}, - "i24805":{"rank":"6"}, - "i24810":{"rank":"1"}, - "i24811":{"rank":"2"}, - "i24812":{"rank":"3"}, - "i24809":{"rank":"7"}, - "i24818":{"rank":"1"}, - "i24819":{"rank":"2"}, - "i24820":{"rank":"3"}, - "i24817":{"rank":"8"}, - "i24826":{"rank":"1"}, - "i24827":{"rank":"2"}, - "i24828":{"rank":"3"}, - "i24825":{"rank":"9"}, - "i24830":{"rank":"1"}, - "i24831":{"rank":"2"}, - "i24832":{"rank":"3"}, - "i24829":{"rank":"10"}, - "i24834":{"rank":"1"}, - "i24835":{"rank":"2"}, - "i24836":{"rank":"3"}, - "i24833":{"rank":"11"}, - "i24781":{"rank":"1"}, - "i24844":{"rank":"1"}, - "i24843":{"rank":"1"}, - "i24846":{"rank":"1"}, - "i24848":{"rank":"1"}, - "i24849":{"rank":"2"}, - "i24850":{"rank":"3"}, - "i24847":{"rank":"2"}, - "i24845":{"rank":"2"}, - "i24852":{"rank":"1"}, - "i24851":{"rank":"3"}, - "i24855":{"rank":"4"}, - "i24864":{"rank":"1"}, - "i24865":{"rank":"2"}, - "i24863":{"rank":"5"}, - "i24869":{"rank":"1"}, - "i24868":{"rank":"6"}, - "i24871":{"rank":"1"}, - "i24870":{"rank":"7"}, - "i24873":{"rank":"1"}, - "i24874":{"rank":"2"}, - "i24875":{"rank":"3"}, - "i24872":{"rank":"8"}, - "i24842":{"rank":"2"}, - "i24780":{"rank":"3"}, - "i24877":{"rank":"1"}, - "i24876":{"rank":"4"}, - "i24890":{"rank":"1"}, - "i24891":{"rank":"2"}, - "i24889":{"rank":"1"}, - "i24893":{"rank":"1"}, - "i24892":{"rank":"2"}, - "i24888":{"rank":"1"}, - "i24878":{"rank":"5"}, - "i24903":{"rank":"1"}, - "i26214":{"rank":"1"}, - "i26216":{"rank":"2"}, - "i26217":{"rank":"3"}, - "i26219":{"rank":"1"}, - "i26218":{"rank":"4"}, - "i24904":{"rank":"2"}, - "i24906":{"rank":"1"}, - "i24907":{"rank":"2"}, - "i24908":{"rank":"3"}, - "i24909":{"rank":"4"}, - "i24910":{"rank":"5"}, - "i24912":{"rank":"6"}, - "i24905":{"rank":"3"}, - "i24913":{"rank":"4"}, - "i24902":{"rank":"6"}, - "i24742":{"rank":"5"}, - "i24914":{"rank":"6"}, - "i24917":{"rank":"1"}, - "i24918":{"rank":"2"}, - "i24916":{"rank":"1"}, - "i24919":{"rank":"2"}, - "i24921":{"rank":"1"}, - "i24922":{"rank":"2"}, - "i24923":{"rank":"3"}, - "i24924":{"rank":"4"}, - "i24920":{"rank":"3"}, - "i24926":{"rank":"1"}, - "i24927":{"rank":"2"}, - "i24925":{"rank":"4"}, - "i24929":{"rank":"1"}, - "i24930":{"rank":"2"}, - "i24928":{"rank":"5"}, - "i24915":{"rank":"7"}, - "i24933":{"rank":"1"}, - "i24932":{"rank":"1"}, - "i24935":{"rank":"1"}, - "i24936":{"rank":"2"}, - "i24934":{"rank":"2"}, - "i24941":{"rank":"3"}, - "i24942":{"rank":"4"}, - "i24931":{"rank":"8"}, - "i24944":{"rank":"1"}, - "i24945":{"rank":"2"}, - "i24946":{"rank":"3"}, - "i24943":{"rank":"9"}, - "i24948":{"rank":"1"}, - "i24949":{"rank":"2"}, - "i24950":{"rank":"3"}, - "i24951":{"rank":"4"}, - "i24952":{"rank":"5"}, - "i24953":{"rank":"6"}, - "i24947":{"rank":"10"}, - "i24954":{"rank":"11"}, - "i24955":{"rank":"12"}, - "i24956":{"rank":"13"}, - "p468":{"rank":"4"}, - "i29924":{"rank":"1"}, - "i29925":{"rank":"2"}, - "i29926":{"rank":"3"}, - "i29921":{"rank":"1"}, - "i29928":{"rank":"1"}, - "i29929":{"rank":"2"}, - "i29930":{"rank":"3"}, - "i29931":{"rank":"4"}, - "i29932":{"rank":"5"}, - "i29933":{"rank":"6"}, - "i29934":{"rank":"7"}, - "i29935":{"rank":"8"}, - "i29927":{"rank":"2"}, - "i29937":{"rank":"1"}, - "i29938":{"rank":"2"}, - "i29936":{"rank":"3"}, - "i29940":{"rank":"1"}, - "i29961":{"rank":"2"}, - "i29939":{"rank":"4"}, - "p655":{"rank":"5"}, - "i23242":{"rank":"1"}, - "i23243":{"rank":"2"}, - "i23244":{"rank":"3"}, - "i23245":{"rank":"4"}, - "i23246":{"rank":"5"}, - "i23247":{"rank":"6"}, - "i23248":{"rank":"7"}, - "i23249":{"rank":"8"}, - "i23250":{"rank":"9"}, - "i23251":{"rank":"10"}, - "i23252":{"rank":"11"}, - "i23253":{"rank":"12"}, - "i23254":{"rank":"13"}, - "i23255":{"rank":"14"}, - "i23256":{"rank":"15"}, - "i23257":{"rank":"16"}, - "i23258":{"rank":"17"}, - "i23259":{"rank":"18"}, - "i23260":{"rank":"19"}, - "i23261":{"rank":"20"}, - "i23262":{"rank":"21"}, - "i23263":{"rank":"22"}, - "i23264":{"rank":"23"}, - "i23265":{"rank":"24"}, - "i23266":{"rank":"25"}, - "i23267":{"rank":"26"}, - "i23268":{"rank":"27"}, - "i23241":{"rank":"1"}, - "i22186":{"rank":"1"}, - "i22187":{"rank":"2"}, - "i22407":{"rank":"1"}, - "i22408":{"rank":"2"}, - "i22409":{"rank":"3"}, - "i22217":{"rank":"1"}, - "i22411":{"rank":"1"}, - "i22412":{"rank":"2"}, - "i22413":{"rank":"3"}, - "i22218":{"rank":"2"}, - "i22415":{"rank":"1"}, - "i22416":{"rank":"2"}, - "i22417":{"rank":"3"}, - "i22219":{"rank":"3"}, - "i22418":{"rank":"1"}, - "i22419":{"rank":"2"}, - "i22420":{"rank":"3"}, - "i22220":{"rank":"4"}, - "i22216":{"rank":"1"}, - "i22223":{"rank":"1"}, - "i22421":{"rank":"2"}, - "i22422":{"rank":"3"}, - "i22222":{"rank":"1"}, - "i22423":{"rank":"1"}, - "i22424":{"rank":"2"}, - "i22425":{"rank":"3"}, - "i22224":{"rank":"2"}, - "i22426":{"rank":"1"}, - "i22427":{"rank":"2"}, - "i22428":{"rank":"3"}, - "i22429":{"rank":"4"}, - "i22225":{"rank":"3"}, - "i22430":{"rank":"1"}, - "i22431":{"rank":"2"}, - "i22432":{"rank":"3"}, - "i22227":{"rank":"1"}, - "i22565":{"rank":"2"}, - "i22226":{"rank":"4"}, - "i22221":{"rank":"2"}, - "i22611":{"rank":"1"}, - "i22612":{"rank":"2"}, - "i22610":{"rank":"1"}, - "i22614":{"rank":"1"}, - "i22615":{"rank":"2"}, - "i22613":{"rank":"2"}, - "i22579":{"rank":"1"}, - "i22616":{"rank":"1"}, - "i22617":{"rank":"2"}, - "i22618":{"rank":"3"}, - "i22581":{"rank":"2"}, - "i22619":{"rank":"1"}, - "i22620":{"rank":"2"}, - "i22621":{"rank":"3"}, - "i22583":{"rank":"3"}, - "i22624":{"rank":"1"}, - "i22625":{"rank":"2"}, - "i22626":{"rank":"3"}, - "i22584":{"rank":"4"}, - "i22629":{"rank":"1"}, - "i22630":{"rank":"2"}, - "i22631":{"rank":"3"}, - "i22586":{"rank":"5"}, - "i22636":{"rank":"1"}, - "i22637":{"rank":"2"}, - "i22638":{"rank":"3"}, - "i22587":{"rank":"6"}, - "i22643":{"rank":"1"}, - "i22644":{"rank":"2"}, - "i22645":{"rank":"3"}, - "i22588":{"rank":"7"}, - "i22646":{"rank":"1"}, - "i22647":{"rank":"2"}, - "i22648":{"rank":"3"}, - "i22589":{"rank":"8"}, - "i22649":{"rank":"1"}, - "i22650":{"rank":"2"}, - "i22651":{"rank":"3"}, - "i22590":{"rank":"9"}, - "i22652":{"rank":"1"}, - "i22653":{"rank":"2"}, - "i22654":{"rank":"3"}, - "i22591":{"rank":"10"}, - "i22657":{"rank":"1"}, - "i22658":{"rank":"2"}, - "i22659":{"rank":"3"}, - "i22592":{"rank":"11"}, - "i22671":{"rank":"1"}, - "i22672":{"rank":"2"}, - "i22673":{"rank":"3"}, - "i22593":{"rank":"12"}, - "i22661":{"rank":"1"}, - "i22662":{"rank":"2"}, - "i22663":{"rank":"3"}, - "i22660":{"rank":"13"}, - "i22675":{"rank":"1"}, - "i22676":{"rank":"2"}, - "i22677":{"rank":"3"}, - "i22678":{"rank":"4"}, - "i22674":{"rank":"14"}, - "i22577":{"rank":"1"}, - "i22594":{"rank":"1"}, - "i22189":{"rank":"1"}, - "i22595":{"rank":"1"}, - "i22633":{"rank":"1"}, - "i22634":{"rank":"2"}, - "i22635":{"rank":"3"}, - "i22632":{"rank":"2"}, - "i22231":{"rank":"2"}, - "i22596":{"rank":"1"}, - "i22567":{"rank":"3"}, - "i22597":{"rank":"1"}, - "i22568":{"rank":"4"}, - "i22569":{"rank":"5"}, - "i22598":{"rank":"1"}, - "i22599":{"rank":"2"}, - "i22570":{"rank":"6"}, - "i22602":{"rank":"1"}, - "i22571":{"rank":"7"}, - "i22603":{"rank":"1"}, - "i22572":{"rank":"8"}, - "i22605":{"rank":"1"}, - "i22606":{"rank":"2"}, - "i22573":{"rank":"9"}, - "i22607":{"rank":"1"}, - "i22574":{"rank":"10"}, - "i22608":{"rank":"1"}, - "i22575":{"rank":"11"}, - "i22609":{"rank":"1"}, - "i22576":{"rank":"12"}, - "i22640":{"rank":"1"}, - "i22641":{"rank":"2"}, - "i22642":{"rank":"3"}, - "i22639":{"rank":"13"}, - "i22578":{"rank":"2"}, - "i22229":{"rank":"3"}, - "i22688":{"rank":"1"}, - "i22414":{"rank":"4"}, - "i22505":{"rank":"1"}, - "i22506":{"rank":"2"}, - "i22507":{"rank":"3"}, - "i22516":{"rank":"4"}, - "i22228":{"rank":"1"}, - "i22510":{"rank":"1"}, - "i22511":{"rank":"2"}, - "i22518":{"rank":"3"}, - "i22509":{"rank":"2"}, - "i22523":{"rank":"1"}, - "i22524":{"rank":"2"}, - "i22520":{"rank":"1"}, - "i22543":{"rank":"1"}, - "i22544":{"rank":"2"}, - "i22521":{"rank":"2"}, - "i22539":{"rank":"1"}, - "i22540":{"rank":"2"}, - "i22522":{"rank":"3"}, - "i22541":{"rank":"1"}, - "i22542":{"rank":"2"}, - "i22525":{"rank":"4"}, - "i22527":{"rank":"5"}, - "i22519":{"rank":"3"}, - "i22508":{"rank":"5"}, - "i22547":{"rank":"1"}, - "i22548":{"rank":"2"}, - "i22551":{"rank":"1"}, - "i22553":{"rank":"2"}, - "i22554":{"rank":"3"}, - "i22559":{"rank":"4"}, - "i22560":{"rank":"5"}, - "i22561":{"rank":"6"}, - "i22562":{"rank":"7"}, - "i22550":{"rank":"3"}, - "i22564":{"rank":"4"}, - "i22546":{"rank":"6"}, - "i22188":{"rank":"3"}, - "i22549":{"rank":"4"}, - "i22722":{"rank":"1"}, - "i22724":{"rank":"2"}, - "i22696":{"rank":"1"}, - "i22697":{"rank":"2"}, - "i22725":{"rank":"1"}, - "i22726":{"rank":"2"}, - "i22727":{"rank":"3"}, - "i22747":{"rank":"4"}, - "i22698":{"rank":"3"}, - "i22748":{"rank":"1"}, - "i22757":{"rank":"2"}, - "i22719":{"rank":"4"}, - "i22759":{"rank":"1"}, - "i22760":{"rank":"2"}, - "i22758":{"rank":"5"}, - "i22694":{"rank":"5"}, - "i22765":{"rank":"1"}, - "i22764":{"rank":"1"}, - "i22767":{"rank":"1"}, - "i22768":{"rank":"2"}, - "i22766":{"rank":"2"}, - "i22771":{"rank":"1"}, - "i22772":{"rank":"2"}, - "i22773":{"rank":"3"}, - "i22770":{"rank":"3"}, - "i22774":{"rank":"4"}, - "i22775":{"rank":"5"}, - "i22762":{"rank":"6"}, - "i22780":{"rank":"1"}, - "i22781":{"rank":"2"}, - "i22782":{"rank":"3"}, - "i27785":{"rank":"4"}, - "i22779":{"rank":"7"}, - "i22784":{"rank":"1"}, - "i22785":{"rank":"2"}, - "i22786":{"rank":"3"}, - "i22787":{"rank":"4"}, - "i22788":{"rank":"5"}, - "i22789":{"rank":"6"}, - "i22783":{"rank":"8"}, - "i22790":{"rank":"9"}, - "i22791":{"rank":"10"}, - "i22792":{"rank":"11"}, - "p576":{"rank":"6"}, - "i3585":{"rank":"1"}, - "i3587":{"rank":"2"}, - "i3589":{"rank":"3"}, - "i3590":{"rank":"4"}, - "i3591":{"rank":"5"}, - "i3593":{"rank":"6"}, - "i3594":{"rank":"7"}, - "i3595":{"rank":"8"}, - "i9547":{"rank":"9"}, - "i18173":{"rank":"10"}, - "i3584":{"rank":"1"}, - "i3597":{"rank":"1"}, - "i3598":{"rank":"2"}, - "i3599":{"rank":"3"}, - "i3596":{"rank":"2"}, - "i3602":{"rank":"1"}, - "i3603":{"rank":"2"}, - "i3604":{"rank":"3"}, - "i3601":{"rank":"3"}, - "i3606":{"rank":"1"}, - "i3607":{"rank":"2"}, - "i3608":{"rank":"3"}, - "i3609":{"rank":"4"}, - "i3605":{"rank":"4"}, - "i3611":{"rank":"1"}, - "i3613":{"rank":"2"}, - "i3618":{"rank":"3"}, - "i3645":{"rank":"4"}, - "i3648":{"rank":"5"}, - "i3653":{"rank":"6"}, - "i3654":{"rank":"7"}, - "i3655":{"rank":"8"}, - "i11635":{"rank":"9"}, - "i11826":{"rank":"10"}, - "i11827":{"rank":"11"}, - "i11828":{"rank":"12"}, - "i11829":{"rank":"13"}, - "i11830":{"rank":"14"}, - "i11831":{"rank":"15"}, - "i11832":{"rank":"16"}, - "i11833":{"rank":"17"}, - "i11834":{"rank":"18"}, - "i11835":{"rank":"19"}, - "i11836":{"rank":"20"}, - "i11837":{"rank":"21"}, - "i11838":{"rank":"22"}, - "i11839":{"rank":"23"}, - "i11840":{"rank":"24"}, - "i11841":{"rank":"25"}, - "i11842":{"rank":"26"}, - "i11843":{"rank":"27"}, - "i11844":{"rank":"28"}, - "i11845":{"rank":"29"}, - "i11846":{"rank":"30"}, - "i11847":{"rank":"31"}, - "i11848":{"rank":"32"}, - "i11849":{"rank":"33"}, - "i11850":{"rank":"34"}, - "i11851":{"rank":"35"}, - "i11852":{"rank":"36"}, - "i11853":{"rank":"37"}, - "i11854":{"rank":"38"}, - "i11855":{"rank":"39"}, - "i11856":{"rank":"40"}, - "i11857":{"rank":"41"}, - "i11858":{"rank":"42"}, - "i11859":{"rank":"43"}, - "i11860":{"rank":"44"}, - "i11861":{"rank":"45"}, - "i11862":{"rank":"46"}, - "i19793":{"rank":"47"}, - "i19794":{"rank":"48"}, - "i3610":{"rank":"5"}, - "i3658":{"rank":"1"}, - "i3659":{"rank":"2"}, - "i3660":{"rank":"3"}, - "i3661":{"rank":"4"}, - "i3662":{"rank":"5"}, - "i3663":{"rank":"6"}, - "i4096":{"rank":"7"}, - "i4749":{"rank":"8"}, - "i4750":{"rank":"9"}, - "i4751":{"rank":"10"}, - "i4752":{"rank":"11"}, - "i3657":{"rank":"6"}, - "i3666":{"rank":"1"}, - "i3669":{"rank":"2"}, - "i3671":{"rank":"3"}, - "i3672":{"rank":"4"}, - "i3674":{"rank":"5"}, - "i3675":{"rank":"6"}, - "i3676":{"rank":"7"}, - "i3677":{"rank":"8"}, - "i3679":{"rank":"9"}, - "i3680":{"rank":"10"}, - "i3681":{"rank":"11"}, - "i3682":{"rank":"12"}, - "i3684":{"rank":"13"}, - "i3685":{"rank":"14"}, - "i3686":{"rank":"15"}, - "i3687":{"rank":"16"}, - "i3688":{"rank":"17"}, - "i3689":{"rank":"18"}, - "i3691":{"rank":"19"}, - "i3693":{"rank":"20"}, - "i3694":{"rank":"21"}, - "i3695":{"rank":"22"}, - "i3696":{"rank":"23"}, - "i3697":{"rank":"24"}, - "i3698":{"rank":"25"}, - "i3700":{"rank":"26"}, - "i3701":{"rank":"27"}, - "i3702":{"rank":"28"}, - "i3703":{"rank":"29"}, - "i3704":{"rank":"30"}, - "i3705":{"rank":"31"}, - "i3706":{"rank":"32"}, - "i3707":{"rank":"33"}, - "i3710":{"rank":"34"}, - "i3711":{"rank":"35"}, - "i3712":{"rank":"36"}, - "i3713":{"rank":"37"}, - "i3714":{"rank":"38"}, - "i3715":{"rank":"39"}, - "i3716":{"rank":"40"}, - "i3718":{"rank":"41"}, - "i3719":{"rank":"42"}, - "i3724":{"rank":"43"}, - "i3726":{"rank":"44"}, - "i3728":{"rank":"45"}, - "i3729":{"rank":"46"}, - "i3730":{"rank":"47"}, - "i3731":{"rank":"48"}, - "i3732":{"rank":"49"}, - "i3733":{"rank":"50"}, - "i3734":{"rank":"51"}, - "i3735":{"rank":"52"}, - "i3736":{"rank":"53"}, - "i3737":{"rank":"54"}, - "i3739":{"rank":"55"}, - "i3740":{"rank":"56"}, - "i3742":{"rank":"57"}, - "i3744":{"rank":"58"}, - "i3746":{"rank":"59"}, - "i3747":{"rank":"60"}, - "i3748":{"rank":"61"}, - "i3750":{"rank":"62"}, - "i3752":{"rank":"63"}, - "i3753":{"rank":"64"}, - "i3754":{"rank":"65"}, - "i3756":{"rank":"66"}, - "i3757":{"rank":"67"}, - "i3758":{"rank":"68"}, - "i3759":{"rank":"69"}, - "i3760":{"rank":"70"}, - "i3762":{"rank":"71"}, - "i3764":{"rank":"72"}, - "i3765":{"rank":"73"}, - "i3766":{"rank":"74"}, - "i3767":{"rank":"75"}, - "i3768":{"rank":"76"}, - "i3769":{"rank":"77"}, - "i3771":{"rank":"78"}, - "i3772":{"rank":"79"}, - "i3773":{"rank":"80"}, - "i3774":{"rank":"81"}, - "i3775":{"rank":"82"}, - "i3776":{"rank":"83"}, - "i3777":{"rank":"84"}, - "i3778":{"rank":"85"}, - "i3780":{"rank":"86"}, - "i3782":{"rank":"87"}, - "i3783":{"rank":"88"}, - "i3784":{"rank":"89"}, - "i3785":{"rank":"90"}, - "i3788":{"rank":"91"}, - "i3789":{"rank":"92"}, - "i3790":{"rank":"93"}, - "i3792":{"rank":"94"}, - "i3793":{"rank":"95"}, - "i9997":{"rank":"96"}, - "i10016":{"rank":"97"}, - "i21223":{"rank":"98"}, - "i23354":{"rank":"99"}, - "i26298":{"rank":"100"}, - "i3664":{"rank":"7"}, - "i9998":{"rank":"1"}, - "i9999":{"rank":"2"}, - "i10000":{"rank":"3"}, - "i10001":{"rank":"4"}, - "i10002":{"rank":"5"}, - "i10003":{"rank":"6"}, - "i10004":{"rank":"7"}, - "i10005":{"rank":"8"}, - "i10006":{"rank":"9"}, - "i10007":{"rank":"10"}, - "i10008":{"rank":"11"}, - "i10009":{"rank":"12"}, - "i10010":{"rank":"13"}, - "i10011":{"rank":"14"}, - "i10012":{"rank":"15"}, - "i10013":{"rank":"16"}, - "i10014":{"rank":"17"}, - "i10015":{"rank":"18"}, - "i10017":{"rank":"19"}, - "i10018":{"rank":"20"}, - "i10019":{"rank":"21"}, - "i10020":{"rank":"22"}, - "i10021":{"rank":"23"}, - "i10022":{"rank":"24"}, - "i10023":{"rank":"25"}, - "i10024":{"rank":"26"}, - "i10025":{"rank":"27"}, - "i10026":{"rank":"28"}, - "i10027":{"rank":"29"}, - "i10028":{"rank":"30"}, - "i14166":{"rank":"31"}, - "i14167":{"rank":"32"}, - "i15672":{"rank":"33"}, - "i9996":{"rank":"8"}, - "p321":{"rank":"7"}, - "i20253":{"rank":"1"}, - "i20252":{"rank":"8"}, - "p428":{"rank":"-7"}, - "i21368":{"rank":"1"}, - "i21369":{"rank":"1"}, - "i31897":{"rank":"2"}, - "i31905":{"rank":"1"}, - "i31904":{"rank":"3"}, - "i32316":{"rank":"4"}, - "p570":{"rank":"1"}, - "i21834":{"rank":"1"}, - "i21832":{"rank":"1"}, - "i32307":{"rank":"1"}, - "i32309":{"rank":"2"}, - "i33457":{"rank":"3"}, - "i32123":{"rank":"2"}, - "i15004":{"rank":"1"}, - "i33466":{"rank":"1"}, - "i18101":{"rank":"2"}, - "p482":{"rank":"2"}, - "p520":{"rank":"1"}, - "i26973":{"rank":"1"}, - "i13839":{"rank":"1"}, - "i31032":{"rank":"1"}, - "i29171":{"rank":"1"}, - "i31839":{"rank":"1"}, - "i31108":{"rank":"2"}, - "i32085":{"rank":"3"}, - "i32358":{"rank":"4"}, - "i22212":{"rank":"2"}, - "i30932":{"rank":"1"}, - "i25149":{"rank":"3"}, - "i27930":{"rank":"1"}, - "i29322":{"rank":"1"}, - "i29949":{"rank":"2"}, - "i32147":{"rank":"3"}, - "i29008":{"rank":"2"}, - "i31031":{"rank":"3"}, - "i31450":{"rank":"4"}, - "i32097":{"rank":"5"}, - "i32098":{"rank":"6"}, - "i32099":{"rank":"7"}, - "i32100":{"rank":"8"}, - "i32101":{"rank":"9"}, - "i32306":{"rank":"10"}, - "i32334":{"rank":"11"}, - "i26957":{"rank":"4"}, - "i31399":{"rank":"1"}, - "i31953":{"rank":"2"}, - "i31949":{"rank":"5"}, - "i14221":{"rank":"2"}, - "p432":{"rank":"3"}, - "i17788":{"rank":"1"}, - "i16682":{"rank":"1"}, - "i17865":{"rank":"2"}, - "i14986":{"rank":"1"}, - "i14988":{"rank":"2"}, - "p479":{"rank":"4"}, - "i30734":{"rank":"1"}, - "i31849":{"rank":"1"}, - "i31850":{"rank":"2"}, - "i31851":{"rank":"3"}, - "i31852":{"rank":"4"}, - "i31842":{"rank":"1"}, - "i32418":{"rank":"2"}, - "i31536":{"rank":"2"}, - "i31537":{"rank":"3"}, - "p658":{"rank":"5"}, - "p452":{"rank":"6"}, - "i23364":{"rank":"1"}, - "i23700":{"rank":"1"}, - "i23691":{"rank":"1"}, - "i23693":{"rank":"2"}, - "i23763":{"rank":"1"}, - "i30916":{"rank":"1"}, - "i30917":{"rank":"2"}, - "i30919":{"rank":"3"}, - "i29354":{"rank":"1"}, - "i31790":{"rank":"2"}, - "i23766":{"rank":"2"}, - "i32421":{"rank":"3"}, - "i32422":{"rank":"4"}, - "i32423":{"rank":"5"}, - "i32424":{"rank":"6"}, - "i32426":{"rank":"1"}, - "i32427":{"rank":"2"}, - "i32428":{"rank":"3"}, - "i32429":{"rank":"4"}, - "i32430":{"rank":"5"}, - "i32431":{"rank":"6"}, - "i32425":{"rank":"7"}, - "i23694":{"rank":"3"}, - "i23697":{"rank":"4"}, - "i30735":{"rank":"5"}, - "p587":{"rank":"7"}, - "p592":{"rank":"8"}, - "i30925":{"rank":"1"}, - "i30926":{"rank":"2"}, - "i30927":{"rank":"1"}, - "i30928":{"rank":"2"}, - "i32324":{"rank":"1"}, - "i32323":{"rank":"3"}, - "i32326":{"rank":"1"}, - "i32325":{"rank":"4"}, - "i30922":{"rank":"1"}, - "i30923":{"rank":"2"}, - "i30929":{"rank":"1"}, - "i32072":{"rank":"2"}, - "i32321":{"rank":"3"}, - "i32322":{"rank":"4"}, - "i32395":{"rank":"5"}, - "i30924":{"rank":"3"}, - "p668":{"rank":"9"}, - "i29053":{"rank":"1"}, - "i29058":{"rank":"1"}, - "i29059":{"rank":"2"}, - "i29054":{"rank":"2"}, - "i29082":{"rank":"1"}, - "i31948":{"rank":"2"}, - "i29055":{"rank":"3"}, - "i29131":{"rank":"1"}, - "i29056":{"rank":"4"}, - "i29345":{"rank":"1"}, - "i29348":{"rank":"1"}, - "i29350":{"rank":"2"}, - "i32339":{"rank":"3"}, - "i29347":{"rank":"2"}, - "i30005":{"rank":"3"}, - "i30006":{"rank":"4"}, - "i29339":{"rank":"5"}, - "i30736":{"rank":"6"}, - "p644":{"rank":"10"}, - "p391":{"rank":"-6"}, - "i30978":{"rank":"1"}, - "i30979":{"rank":"2"}, - "i30976":{"rank":"1"}, - "i30982":{"rank":"2"}, - "i30974":{"rank":"1"}, - "i30984":{"rank":"1"}, - "i30985":{"rank":"2"}, - "i30986":{"rank":"3"}, - "i30987":{"rank":"4"}, - "i30988":{"rank":"5"}, - "i30989":{"rank":"6"}, - "i30990":{"rank":"7"}, - "i30992":{"rank":"1"}, - "i30993":{"rank":"2"}, - "i30991":{"rank":"8"}, - "i30994":{"rank":"9"}, - "i30995":{"rank":"10"}, - "i30983":{"rank":"2"}, - "i30997":{"rank":"1"}, - "i30998":{"rank":"2"}, - "i30999":{"rank":"3"}, - "i31000":{"rank":"4"}, - "i30996":{"rank":"3"}, - "i31005":{"rank":"1"}, - "i31006":{"rank":"2"}, - "i31007":{"rank":"3"}, - "i31004":{"rank":"4"}, - "p602":{"rank":"1"}, - "p674":{"rank":"-5"}, - "i32168":{"rank":"1"}, - "i32169":{"rank":"2"}, - "i32165":{"rank":"1"}, - "i32171":{"rank":"2"}, - "i32163":{"rank":"1"}, - "i32173":{"rank":"1"}, - "i32174":{"rank":"2"}, - "i32176":{"rank":"3"}, - "i32177":{"rank":"4"}, - "i32178":{"rank":"5"}, - "i32179":{"rank":"6"}, - "i32181":{"rank":"1"}, - "i32182":{"rank":"2"}, - "i32180":{"rank":"7"}, - "i32184":{"rank":"8"}, - "i32172":{"rank":"2"}, - "i32186":{"rank":"1"}, - "i32187":{"rank":"2"}, - "i32188":{"rank":"3"}, - "i32189":{"rank":"4"}, - "i32185":{"rank":"3"}, - "i32191":{"rank":"1"}, - "i32192":{"rank":"2"}, - "i32190":{"rank":"4"}, - "i32194":{"rank":"1"}, - "i32195":{"rank":"2"}, - "i32196":{"rank":"3"}, - "i32193":{"rank":"5"}, - "p663":{"rank":"1"}, - "p619":{"rank":"2"}, - "p694":{"rank":"3"}, - "i28502":{"rank":"1"}, - "i28503":{"rank":"2"}, - "i28504":{"rank":"3"}, - "i28505":{"rank":"4"}, - "i28501":{"rank":"1"}, - "i28510":{"rank":"1"}, - "i28511":{"rank":"2"}, - "i28512":{"rank":"3"}, - "i28509":{"rank":"2"}, - "p620":{"rank":"4"}, - "i30344":{"rank":"1"}, - "i30345":{"rank":"2"}, - "i30346":{"rank":"3"}, - "i30347":{"rank":"4"}, - "i30348":{"rank":"5"}, - "i30349":{"rank":"6"}, - "i30350":{"rank":"7"}, - "i30352":{"rank":"1"}, - "i30353":{"rank":"2"}, - "i30351":{"rank":"8"}, - "i30354":{"rank":"9"}, - "i30355":{"rank":"10"}, - "i30343":{"rank":"1"}, - "i30357":{"rank":"1"}, - "i30358":{"rank":"2"}, - "i30359":{"rank":"3"}, - "i30360":{"rank":"4"}, - "i30356":{"rank":"2"}, - "i30362":{"rank":"1"}, - "i30363":{"rank":"2"}, - "i30361":{"rank":"3"}, - "i30365":{"rank":"1"}, - "i30366":{"rank":"2"}, - "i30367":{"rank":"3"}, - "i30364":{"rank":"4"}, - "i30335":{"rank":"1"}, - "i30337":{"rank":"1"}, - "i30338":{"rank":"2"}, - "i30339":{"rank":"3"}, - "i30340":{"rank":"4"}, - "i30336":{"rank":"2"}, - "i30341":{"rank":"3"}, - "i30342":{"rank":"4"}, - "i30334":{"rank":"5"}, - "p614":{"rank":"5"}, - "p693":{"rank":"6"}, - "p695":{"rank":"7"}, - "p609":{"rank":"8"}, - "p696":{"rank":"9"}, - "p698":{"rank":"10"}, - "p691":{"rank":"11"}, - "i31964":{"rank":"1"}, - "i31965":{"rank":"2"}, - "i31967":{"rank":"3"}, - "i31968":{"rank":"4"}, - "i31969":{"rank":"5"}, - "i31970":{"rank":"6"}, - "i31972":{"rank":"1"}, - "i31973":{"rank":"2"}, - "i31971":{"rank":"7"}, - "i31975":{"rank":"8"}, - "i31963":{"rank":"1"}, - "i31977":{"rank":"1"}, - "i31978":{"rank":"2"}, - "i31979":{"rank":"3"}, - "i31980":{"rank":"4"}, - "i31976":{"rank":"2"}, - "i31982":{"rank":"1"}, - "i31983":{"rank":"2"}, - "i31981":{"rank":"3"}, - "i31985":{"rank":"1"}, - "i31986":{"rank":"2"}, - "i31987":{"rank":"3"}, - "i31984":{"rank":"4"}, - "p662":{"rank":"12"}, - "p697":{"rank":"13"}, - "i31423":{"rank":"1"}, - "i31424":{"rank":"2"}, - "i31425":{"rank":"3"}, - "i31426":{"rank":"4"}, - "i31427":{"rank":"5"}, - "i31428":{"rank":"6"}, - "i31429":{"rank":"7"}, - "i31431":{"rank":"1"}, - "i31432":{"rank":"2"}, - "i31430":{"rank":"8"}, - "i31433":{"rank":"9"}, - "i31434":{"rank":"10"}, - "i31422":{"rank":"1"}, - "i31436":{"rank":"1"}, - "i31437":{"rank":"2"}, - "i31438":{"rank":"3"}, - "i31439":{"rank":"4"}, - "i31435":{"rank":"2"}, - "i31444":{"rank":"1"}, - "i31445":{"rank":"2"}, - "i31446":{"rank":"3"}, - "i31443":{"rank":"3"}, - "p664":{"rank":"14"}, - "p692":{"rank":"15"}, - "i27375":{"rank":"1"}, - "i29215":{"rank":"2"}, - "i29216":{"rank":"3"}, - "i27353":{"rank":"1"}, - "i27346":{"rank":"1"}, - "i28588":{"rank":"1"}, - "i30828":{"rank":"2"}, - "i28586":{"rank":"2"}, - "p632":{"rank":"16"}, - "i32212":{"rank":"1"}, - "i32215":{"rank":"1"}, - "i32216":{"rank":"2"}, - "i32214":{"rank":"1"}, - "i32217":{"rank":"2"}, - "i32219":{"rank":"1"}, - "i32220":{"rank":"2"}, - "i32221":{"rank":"3"}, - "i32218":{"rank":"3"}, - "i32223":{"rank":"1"}, - "i32224":{"rank":"2"}, - "i32225":{"rank":"3"}, - "i32226":{"rank":"4"}, - "i32227":{"rank":"5"}, - "i32228":{"rank":"6"}, - "i32229":{"rank":"7"}, - "i32230":{"rank":"8"}, - "i32231":{"rank":"9"}, - "i32222":{"rank":"4"}, - "i32213":{"rank":"2"}, - "i32232":{"rank":"3"}, - "i32233":{"rank":"4"}, - "i32211":{"rank":"1"}, - "i32235":{"rank":"1"}, - "i32236":{"rank":"2"}, - "i32237":{"rank":"3"}, - "i32238":{"rank":"4"}, - "i32239":{"rank":"5"}, - "i32240":{"rank":"6"}, - "i32241":{"rank":"7"}, - "i32243":{"rank":"1"}, - "i32244":{"rank":"2"}, - "i32242":{"rank":"8"}, - "i32245":{"rank":"9"}, - "i32246":{"rank":"10"}, - "i32234":{"rank":"2"}, - "i32248":{"rank":"1"}, - "i32251":{"rank":"1"}, - "i32253":{"rank":"2"}, - "i32250":{"rank":"1"}, - "i32254":{"rank":"2"}, - "i32256":{"rank":"1"}, - "i32257":{"rank":"2"}, - "i32258":{"rank":"3"}, - "i32255":{"rank":"3"}, - "i32260":{"rank":"1"}, - "i32261":{"rank":"2"}, - "i32262":{"rank":"3"}, - "i32263":{"rank":"4"}, - "i32264":{"rank":"5"}, - "i32265":{"rank":"6"}, - "i32266":{"rank":"7"}, - "i32267":{"rank":"8"}, - "i32268":{"rank":"9"}, - "i32301":{"rank":"10"}, - "i32302":{"rank":"11"}, - "i32259":{"rank":"4"}, - "i32249":{"rank":"2"}, - "i32269":{"rank":"3"}, - "i32270":{"rank":"4"}, - "i32247":{"rank":"3"}, - "i32271":{"rank":"1"}, - "i32272":{"rank":"2"}, - "i32273":{"rank":"3"}, - "i32274":{"rank":"4"}, - "i32252":{"rank":"4"}, - "i32277":{"rank":"1"}, - "i32278":{"rank":"2"}, - "i32279":{"rank":"3"}, - "i32280":{"rank":"4"}, - "i32281":{"rank":"5"}, - "i32282":{"rank":"6"}, - "i32283":{"rank":"7"}, - "i32285":{"rank":"1"}, - "i32286":{"rank":"2"}, - "i32284":{"rank":"8"}, - "i32287":{"rank":"9"}, - "i32288":{"rank":"10"}, - "i32276":{"rank":"5"}, - "i32290":{"rank":"1"}, - "i32291":{"rank":"2"}, - "i32292":{"rank":"3"}, - "i32293":{"rank":"4"}, - "i32289":{"rank":"6"}, - "i32295":{"rank":"1"}, - "i32296":{"rank":"2"}, - "i32294":{"rank":"7"}, - "i32298":{"rank":"1"}, - "i32299":{"rank":"2"}, - "i32300":{"rank":"3"}, - "i32297":{"rank":"8"}, - "p682":{"rank":"17"}, - "p618":{"rank":"-4"}, - "p666":{"rank":"1"}, - "p665":{"rank":"1"}, - "p667":{"rank":"2"}, - "i6349":{"rank":"3"}, - "i6352":{"rank":"4"}, - "i6353":{"rank":"5"}, - "i6354":{"rank":"6"}, - "i7295":{"rank":"7"}, - "i7753":{"rank":"8"}, - "p366":{"rank":"1"}, - "i31043":{"rank":"1"}, - "i31484":{"rank":"2"}, - "i31660":{"rank":"1"}, - "i31485":{"rank":"3"}, - "i31486":{"rank":"4"}, - "i31487":{"rank":"5"}, - "i31488":{"rank":"6"}, - "i31489":{"rank":"7"}, - "i31543":{"rank":"8"}, - "i31663":{"rank":"1"}, - "i31664":{"rank":"2"}, - "i31665":{"rank":"3"}, - "i31666":{"rank":"4"}, - "i31662":{"rank":"9"}, - "i31885":{"rank":"10"}, - "p640":{"rank":"2"}, - "i15863":{"rank":"1"}, - "i32373":{"rank":"2"}, - "i32374":{"rank":"3"}, - "i32387":{"rank":"4"}, - "i32453":{"rank":"5"}, - "i32554":{"rank":"6"}, - "p442":{"rank":"3"}, - "p673":{"rank":"1"}, - "i32506":{"rank":"1"}, - "i33512":{"rank":"2"}, - "i29237":{"rank":"2"}, - "i29280":{"rank":"3"}, - "i29279":{"rank":"1"}, - "i29294":{"rank":"2"}, - "i29292":{"rank":"4"}, - "i31866":{"rank":"1"}, - "i32088":{"rank":"2"}, - "i30941":{"rank":"5"}, - "i31907":{"rank":"1"}, - "i31753":{"rank":"6"}, - "i31814":{"rank":"7"}, - "i31899":{"rank":"1"}, - "i31901":{"rank":"2"}, - "i31902":{"rank":"3"}, - "i31900":{"rank":"8"}, - "i31903":{"rank":"9"}, - "i31989":{"rank":"1"}, - "i31988":{"rank":"10"}, - "i31992":{"rank":"1"}, - "i31991":{"rank":"11"}, - "i32118":{"rank":"1"}, - "i32119":{"rank":"2"}, - "i32120":{"rank":"3"}, - "i32117":{"rank":"12"}, - "i32124":{"rank":"13"}, - "i33458":{"rank":"14"}, - "p635":{"rank":"4"}, - "i16844":{"rank":"1"}, - "i32159":{"rank":"2"}, - "p501":{"rank":"5"}, - "i9617":{"rank":"1"}, - "i9620":{"rank":"2"}, - "i9621":{"rank":"3"}, - "i9622":{"rank":"4"}, - "i9819":{"rank":"1"}, - "i9822":{"rank":"2"}, - "i9824":{"rank":"3"}, - "i9827":{"rank":"4"}, - "i9828":{"rank":"5"}, - "i9829":{"rank":"6"}, - "i9830":{"rank":"7"}, - "i9831":{"rank":"8"}, - "i9832":{"rank":"9"}, - "i9833":{"rank":"10"}, - "i9834":{"rank":"11"}, - "i9835":{"rank":"12"}, - "i9836":{"rank":"13"}, - "i9837":{"rank":"14"}, - "i9838":{"rank":"15"}, - "i9839":{"rank":"16"}, - "i9840":{"rank":"17"}, - "i9841":{"rank":"18"}, - "i9842":{"rank":"19"}, - "i9843":{"rank":"20"}, - "i9844":{"rank":"21"}, - "i9845":{"rank":"22"}, - "i32468":{"rank":"1"}, - "i9846":{"rank":"23"}, - "i9847":{"rank":"24"}, - "i32452":{"rank":"1"}, - "i9848":{"rank":"25"}, - "i9849":{"rank":"26"}, - "i9850":{"rank":"27"}, - "i9851":{"rank":"28"}, - "i9852":{"rank":"29"}, - "i9853":{"rank":"30"}, - "i9854":{"rank":"31"}, - "i9855":{"rank":"32"}, - "i9856":{"rank":"33"}, - "i9857":{"rank":"34"}, - "i9858":{"rank":"35"}, - "i9859":{"rank":"36"}, - "i9860":{"rank":"37"}, - "i9861":{"rank":"38"}, - "i9862":{"rank":"39"}, - "i9863":{"rank":"40"}, - "i9864":{"rank":"41"}, - "i9865":{"rank":"42"}, - "i9866":{"rank":"43"}, - "i9867":{"rank":"44"}, - "i9868":{"rank":"45"}, - "i9869":{"rank":"46"}, - "i9870":{"rank":"47"}, - "i9871":{"rank":"48"}, - "i9872":{"rank":"49"}, - "i9873":{"rank":"50"}, - "i9874":{"rank":"51"}, - "i9875":{"rank":"52"}, - "i9876":{"rank":"53"}, - "i9877":{"rank":"54"}, - "i9878":{"rank":"55"}, - "i9879":{"rank":"56"}, - "i9880":{"rank":"57"}, - "i9881":{"rank":"58"}, - "i9882":{"rank":"59"}, - "i9883":{"rank":"60"}, - "i9884":{"rank":"61"}, - "i9885":{"rank":"62"}, - "i9886":{"rank":"63"}, - "i9887":{"rank":"64"}, - "i9888":{"rank":"65"}, - "i9889":{"rank":"66"}, - "i9890":{"rank":"67"}, - "i9891":{"rank":"68"}, - "i9892":{"rank":"69"}, - "i9893":{"rank":"70"}, - "i17567":{"rank":"1"}, - "i9894":{"rank":"71"}, - "i9895":{"rank":"72"}, - "i9896":{"rank":"73"}, - "i9897":{"rank":"74"}, - "i9898":{"rank":"75"}, - "i9899":{"rank":"76"}, - "i9900":{"rank":"77"}, - "i9901":{"rank":"78"}, - "i9902":{"rank":"79"}, - "i9903":{"rank":"80"}, - "i9904":{"rank":"81"}, - "i9905":{"rank":"82"}, - "i9906":{"rank":"83"}, - "i16497":{"rank":"1"}, - "i9907":{"rank":"84"}, - "i9908":{"rank":"85"}, - "i9909":{"rank":"86"}, - "i9910":{"rank":"87"}, - "i9911":{"rank":"88"}, - "i9912":{"rank":"89"}, - "i9913":{"rank":"90"}, - "i9914":{"rank":"91"}, - "i9915":{"rank":"92"}, - "i9916":{"rank":"93"}, - "i9917":{"rank":"94"}, - "i9918":{"rank":"95"}, - "i9919":{"rank":"96"}, - "i9920":{"rank":"97"}, - "i9921":{"rank":"98"}, - "i9922":{"rank":"99"}, - "i9923":{"rank":"100"}, - "i9924":{"rank":"101"}, - "i9925":{"rank":"102"}, - "i9926":{"rank":"103"}, - "i9927":{"rank":"104"}, - "i9928":{"rank":"105"}, - "i9929":{"rank":"106"}, - "i9930":{"rank":"107"}, - "i9931":{"rank":"108"}, - "i18541":{"rank":"1"}, - "i19723":{"rank":"2"}, - "i19725":{"rank":"3"}, - "i19727":{"rank":"4"}, - "i9932":{"rank":"109"}, - "i9933":{"rank":"110"}, - "i9934":{"rank":"111"}, - "i9935":{"rank":"112"}, - "i9936":{"rank":"113"}, - "i9937":{"rank":"114"}, - "i9938":{"rank":"115"}, - "i9939":{"rank":"116"}, - "i9940":{"rank":"117"}, - "i9941":{"rank":"118"}, - "i9942":{"rank":"119"}, - "i9943":{"rank":"120"}, - "i9944":{"rank":"121"}, - "i9945":{"rank":"122"}, - "i9946":{"rank":"123"}, - "i9947":{"rank":"124"}, - "i13091":{"rank":"125"}, - "i9818":{"rank":"5"}, - "i10699":{"rank":"1"}, - "i10310":{"rank":"6"}, - "i11864":{"rank":"1"}, - "i11865":{"rank":"2"}, - "i11866":{"rank":"3"}, - "i11867":{"rank":"4"}, - "i11868":{"rank":"5"}, - "i11869":{"rank":"6"}, - "i11870":{"rank":"7"}, - "i11871":{"rank":"8"}, - "i11872":{"rank":"9"}, - "i11873":{"rank":"10"}, - "i11874":{"rank":"11"}, - "i11875":{"rank":"12"}, - "i11876":{"rank":"13"}, - "i11877":{"rank":"14"}, - "i11878":{"rank":"15"}, - "i11879":{"rank":"16"}, - "i11880":{"rank":"17"}, - "i11881":{"rank":"18"}, - "i11882":{"rank":"19"}, - "i11883":{"rank":"20"}, - "i11884":{"rank":"21"}, - "i11885":{"rank":"22"}, - "i11886":{"rank":"23"}, - "i11887":{"rank":"24"}, - "i11888":{"rank":"25"}, - "i11889":{"rank":"26"}, - "i11890":{"rank":"27"}, - "i11891":{"rank":"28"}, - "i11892":{"rank":"29"}, - "i11893":{"rank":"30"}, - "i11895":{"rank":"31"}, - "i11896":{"rank":"32"}, - "i11897":{"rank":"33"}, - "i11898":{"rank":"34"}, - "i11899":{"rank":"35"}, - "i11900":{"rank":"36"}, - "i11901":{"rank":"37"}, - "i11902":{"rank":"38"}, - "i11903":{"rank":"39"}, - "i11904":{"rank":"40"}, - "i11905":{"rank":"41"}, - "i11906":{"rank":"42"}, - "i11907":{"rank":"43"}, - "i11908":{"rank":"44"}, - "i11909":{"rank":"45"}, - "i12355":{"rank":"1"}, - "i11894":{"rank":"46"}, - "i11863":{"rank":"7"}, - "i11924":{"rank":"1"}, - "i11925":{"rank":"2"}, - "i11926":{"rank":"3"}, - "i11927":{"rank":"4"}, - "i11923":{"rank":"8"}, - "i11929":{"rank":"1"}, - "i11930":{"rank":"2"}, - "i11931":{"rank":"3"}, - "i11928":{"rank":"9"}, - "i11933":{"rank":"1"}, - "i11934":{"rank":"2"}, - "i11935":{"rank":"3"}, - "i11932":{"rank":"10"}, - "i11937":{"rank":"1"}, - "i11938":{"rank":"2"}, - "i11939":{"rank":"3"}, - "i11940":{"rank":"4"}, - "i11941":{"rank":"5"}, - "i11942":{"rank":"6"}, - "i11943":{"rank":"7"}, - "i11944":{"rank":"8"}, - "i11945":{"rank":"9"}, - "i12764":{"rank":"10"}, - "i11936":{"rank":"11"}, - "i15032":{"rank":"1"}, - "i15031":{"rank":"12"}, - "i21266":{"rank":"1"}, - "i15493":{"rank":"13"}, - "i22309":{"rank":"1"}, - "i22308":{"rank":"14"}, - "i32444":{"rank":"15"}, - "i32445":{"rank":"16"}, - "i32450":{"rank":"17"}, - "i33401":{"rank":"1"}, - "i33402":{"rank":"2"}, - "i33454":{"rank":"3"}, - "i33459":{"rank":"4"}, - "i33460":{"rank":"5"}, - "i33465":{"rank":"6"}, - "i33510":{"rank":"7"}, - "i33400":{"rank":"18"}, - "i33456":{"rank":"19"}, - "i9624":{"rank":"1"}, - "i32432":{"rank":"1"}, - "i32433":{"rank":"2"}, - "i9626":{"rank":"2"}, - "i9627":{"rank":"3"}, - "i9618":{"rank":"20"}, - "p399":{"rank":"6"}, - "i31081":{"rank":"1"}, - "i31088":{"rank":"2"}, - "i31089":{"rank":"3"}, - "i31090":{"rank":"4"}, - "i31091":{"rank":"5"}, - "i31110":{"rank":"6"}, - "i31111":{"rank":"7"}, - "i31073":{"rank":"1"}, - "i31125":{"rank":"1"}, - "i31126":{"rank":"2"}, - "i31127":{"rank":"3"}, - "i31128":{"rank":"4"}, - "i31129":{"rank":"5"}, - "i31130":{"rank":"6"}, - "i31131":{"rank":"7"}, - "i31118":{"rank":"1"}, - "i31132":{"rank":"1"}, - "i31133":{"rank":"2"}, - "i31134":{"rank":"3"}, - "i31135":{"rank":"4"}, - "i31136":{"rank":"5"}, - "i31137":{"rank":"6"}, - "i31138":{"rank":"7"}, - "i31631":{"rank":"8"}, - "i31119":{"rank":"2"}, - "i31139":{"rank":"1"}, - "i31140":{"rank":"2"}, - "i31141":{"rank":"3"}, - "i31142":{"rank":"4"}, - "i31143":{"rank":"5"}, - "i31144":{"rank":"6"}, - "i31145":{"rank":"7"}, - "i31632":{"rank":"8"}, - "i31120":{"rank":"3"}, - "i31146":{"rank":"1"}, - "i31633":{"rank":"2"}, - "i31634":{"rank":"3"}, - "i31636":{"rank":"4"}, - "i31121":{"rank":"4"}, - "i31116":{"rank":"2"}, - "i31122":{"rank":"1"}, - "i31123":{"rank":"2"}, - "i31124":{"rank":"3"}, - "i31117":{"rank":"3"}, - "i31345":{"rank":"1"}, - "i31343":{"rank":"4"}, - "i33376":{"rank":"5"}, - "p642":{"rank":"7"}, - "p393":{"rank":"-3"}, - "i21927":{"rank":"1"}, - "i22736":{"rank":"2"}, - "i21712":{"rank":"1"}, - "i21711":{"rank":"1"}, - "i22750":{"rank":"2"}, - "i28992":{"rank":"3"}, - "i22769":{"rank":"1"}, - "i21715":{"rank":"1"}, - "i21714":{"rank":"4"}, - "p573":{"rank":"1"}, - "i30292":{"rank":"1"}, - "i30295":{"rank":"2"}, - "i30330":{"rank":"3"}, - "i30381":{"rank":"4"}, - "i30421":{"rank":"5"}, - "i30457":{"rank":"6"}, - "i31245":{"rank":"1"}, - "i30458":{"rank":"7"}, - "i30459":{"rank":"8"}, - "i30460":{"rank":"9"}, - "p659":{"rank":"2"}, - "i31562":{"rank":"1"}, - "i31563":{"rank":"2"}, - "i31576":{"rank":"3"}, - "i31577":{"rank":"4"}, - "i28862":{"rank":"1"}, - "i28888":{"rank":"2"}, - "i30840":{"rank":"1"}, - "i30839":{"rank":"1"}, - "i28916":{"rank":"1"}, - "i31567":{"rank":"2"}, - "i31568":{"rank":"3"}, - "i31569":{"rank":"4"}, - "i31570":{"rank":"5"}, - "i31571":{"rank":"6"}, - "i31572":{"rank":"7"}, - "i31573":{"rank":"8"}, - "i31574":{"rank":"9"}, - "i32476":{"rank":"10"}, - "i31566":{"rank":"2"}, - "i28890":{"rank":"3"}, - "i28332":{"rank":"1"}, - "i28891":{"rank":"4"}, - "i28909":{"rank":"1"}, - "i28892":{"rank":"5"}, - "i30835":{"rank":"1"}, - "i31148":{"rank":"2"}, - "i31198":{"rank":"3"}, - "i28893":{"rank":"6"}, - "i28894":{"rank":"7"}, - "i33387":{"rank":"1"}, - "i28897":{"rank":"8"}, - "i29997":{"rank":"1"}, - "i30370":{"rank":"2"}, - "i31607":{"rank":"3"}, - "i32507":{"rank":"4"}, - "i29334":{"rank":"9"}, - "i30742":{"rank":"1"}, - "i30743":{"rank":"2"}, - "i30744":{"rank":"3"}, - "i30745":{"rank":"4"}, - "i31203":{"rank":"5"}, - "i30739":{"rank":"1"}, - "i30740":{"rank":"2"}, - "i30741":{"rank":"3"}, - "i30738":{"rank":"10"}, - "i30760":{"rank":"1"}, - "i30759":{"rank":"11"}, - "i30830":{"rank":"1"}, - "i30829":{"rank":"12"}, - "i29950":{"rank":"1"}, - "i28889":{"rank":"1"}, - "i30833":{"rank":"2"}, - "i30832":{"rank":"13"}, - "i31335":{"rank":"14"}, - "i32356":{"rank":"15"}, - "p636":{"rank":"3"}, - "i29075":{"rank":"1"}, - "p420":{"rank":"4"}, - "i32416":{"rank":"1"}, - "i30864":{"rank":"1"}, - "i29089":{"rank":"1"}, - "i29090":{"rank":"2"}, - "i29091":{"rank":"3"}, - "i29321":{"rank":"1"}, - "i29092":{"rank":"4"}, - "i32018":{"rank":"1"}, - "i32467":{"rank":"2"}, - "i29093":{"rank":"5"}, - "i29094":{"rank":"6"}, - "i29095":{"rank":"7"}, - "i31812":{"rank":"1"}, - "i32475":{"rank":"2"}, - "i29096":{"rank":"8"}, - "i29097":{"rank":"9"}, - "i29098":{"rank":"10"}, - "i29297":{"rank":"1"}, - "i29099":{"rank":"11"}, - "i29100":{"rank":"12"}, - "i29101":{"rank":"13"}, - "i29102":{"rank":"14"}, - "i29105":{"rank":"15"}, - "i29181":{"rank":"16"}, - "i29187":{"rank":"17"}, - "i30392":{"rank":"18"}, - "i31411":{"rank":"1"}, - "i32474":{"rank":"2"}, - "i30815":{"rank":"19"}, - "i30854":{"rank":"20"}, - "i30869":{"rank":"21"}, - "i31310":{"rank":"22"}, - "i31822":{"rank":"1"}, - "i31552":{"rank":"23"}, - "p645":{"rank":"5"}, - "i31323":{"rank":"1"}, - "i31348":{"rank":"2"}, - "i31385":{"rank":"3"}, - "i31398":{"rank":"4"}, - "i31319":{"rank":"1"}, - "i31362":{"rank":"1"}, - "i31590":{"rank":"2"}, - "i31608":{"rank":"3"}, - "i31609":{"rank":"4"}, - "i31637":{"rank":"5"}, - "i31678":{"rank":"6"}, - "i31320":{"rank":"2"}, - "i31520":{"rank":"1"}, - "i31321":{"rank":"3"}, - "i31525":{"rank":"1"}, - "i31324":{"rank":"1"}, - "i31528":{"rank":"1"}, - "i31325":{"rank":"2"}, - "i31529":{"rank":"1"}, - "i31326":{"rank":"3"}, - "i31350":{"rank":"1"}, - "i31526":{"rank":"2"}, - "i31542":{"rank":"3"}, - "i31327":{"rank":"4"}, - "i31527":{"rank":"1"}, - "i31541":{"rank":"2"}, - "i31328":{"rank":"5"}, - "i31524":{"rank":"1"}, - "i31329":{"rank":"6"}, - "i31349":{"rank":"1"}, - "i31330":{"rank":"7"}, - "i31331":{"rank":"8"}, - "i31322":{"rank":"4"}, - "i31351":{"rank":"5"}, - "p671":{"rank":"6"}, - "i22119":{"rank":"1"}, - "i28288":{"rank":"2"}, - "i21295":{"rank":"1"}, - "i21296":{"rank":"2"}, - "i21556":{"rank":"1"}, - "i21969":{"rank":"2"}, - "i21555":{"rank":"3"}, - "i21294":{"rank":"1"}, - "i27104":{"rank":"1"}, - "i21302":{"rank":"1"}, - "i21305":{"rank":"2"}, - "i21301":{"rank":"2"}, - "i22405":{"rank":"1"}, - "i21313":{"rank":"1"}, - "i21315":{"rank":"2"}, - "i28918":{"rank":"3"}, - "i21312":{"rank":"3"}, - "i21814":{"rank":"1"}, - "i21342":{"rank":"1"}, - "i21549":{"rank":"2"}, - "i21552":{"rank":"3"}, - "i21544":{"rank":"4"}, - "i28351":{"rank":"5"}, - "i28353":{"rank":"6"}, - "i29130":{"rank":"7"}, - "i30368":{"rank":"8"}, - "i30819":{"rank":"9"}, - "p568":{"rank":"7"}, - "i31594":{"rank":"1"}, - "i32415":{"rank":"2"}, - "i32466":{"rank":"3"}, - "i32469":{"rank":"4"}, - "i32470":{"rank":"5"}, - "i30953":{"rank":"1"}, - "i33452":{"rank":"1"}, - "i30954":{"rank":"2"}, - "i33399":{"rank":"1"}, - "i30955":{"rank":"3"}, - "i30956":{"rank":"4"}, - "i31819":{"rank":"1"}, - "i32107":{"rank":"2"}, - "i30957":{"rank":"5"}, - "i32073":{"rank":"1"}, - "i32074":{"rank":"2"}, - "i30958":{"rank":"6"}, - "i30959":{"rank":"7"}, - "i30965":{"rank":"1"}, - "i30961":{"rank":"1"}, - "i30960":{"rank":"8"}, - "i30966":{"rank":"9"}, - "i31591":{"rank":"10"}, - "i31829":{"rank":"11"}, - "i32477":{"rank":"12"}, - "p670":{"rank":"8"}, - "p657":{"rank":"1"}, - "i28266":{"rank":"1"}, - "i28342":{"rank":"1"}, - "i28341":{"rank":"2"}, - "i28901":{"rank":"1"}, - "i28903":{"rank":"2"}, - "i28902":{"rank":"3"}, - "i28946":{"rank":"4"}, - "i29308":{"rank":"1"}, - "i29316":{"rank":"2"}, - "i30320":{"rank":"3"}, - "i29302":{"rank":"5"}, - "i30004":{"rank":"6"}, - "i30045":{"rank":"1"}, - "i30046":{"rank":"2"}, - "i30047":{"rank":"3"}, - "i30044":{"rank":"1"}, - "i30043":{"rank":"7"}, - "i30285":{"rank":"8"}, - "i30319":{"rank":"9"}, - "i29047":{"rank":"1"}, - "i30726":{"rank":"10"}, - "i27415":{"rank":"1"}, - "i30818":{"rank":"2"}, - "i30817":{"rank":"11"}, - "i30935":{"rank":"12"}, - "p608":{"rank":"2"}, - "i26066":{"rank":"1"}, - "i26067":{"rank":"2"}, - "i26070":{"rank":"3"}, - "i26071":{"rank":"4"}, - "i25997":{"rank":"1"}, - "i26029":{"rank":"1"}, - "i26009":{"rank":"1"}, - "i26030":{"rank":"1"}, - "i26010":{"rank":"2"}, - "i26011":{"rank":"3"}, - "i26012":{"rank":"4"}, - "i26031":{"rank":"1"}, - "i26013":{"rank":"5"}, - "i26032":{"rank":"1"}, - "i26014":{"rank":"6"}, - "i26016":{"rank":"7"}, - "i26069":{"rank":"1"}, - "i26068":{"rank":"8"}, - "i25998":{"rank":"2"}, - "i26033":{"rank":"1"}, - "i26034":{"rank":"2"}, - "i26035":{"rank":"3"}, - "i25999":{"rank":"3"}, - "i26036":{"rank":"1"}, - "i26037":{"rank":"2"}, - "i26038":{"rank":"3"}, - "i26088":{"rank":"4"}, - "i26089":{"rank":"5"}, - "i26000":{"rank":"4"}, - "i26039":{"rank":"1"}, - "i26040":{"rank":"2"}, - "i26041":{"rank":"3"}, - "i26087":{"rank":"4"}, - "i26001":{"rank":"5"}, - "i26042":{"rank":"1"}, - "i26002":{"rank":"6"}, - "i26043":{"rank":"1"}, - "i26003":{"rank":"7"}, - "i26044":{"rank":"1"}, - "i26045":{"rank":"2"}, - "i26004":{"rank":"8"}, - "i26055":{"rank":"1"}, - "i26056":{"rank":"2"}, - "i26046":{"rank":"1"}, - "i26057":{"rank":"1"}, - "i26047":{"rank":"2"}, - "i26005":{"rank":"9"}, - "i26048":{"rank":"1"}, - "i26049":{"rank":"2"}, - "i26006":{"rank":"10"}, - "i26050":{"rank":"1"}, - "i26007":{"rank":"11"}, - "i26051":{"rank":"1"}, - "i26052":{"rank":"2"}, - "i26053":{"rank":"3"}, - "i26093":{"rank":"4"}, - "i26008":{"rank":"12"}, - "i26054":{"rank":"1"}, - "i26015":{"rank":"13"}, - "i26017":{"rank":"14"}, - "i26018":{"rank":"15"}, - "i26058":{"rank":"1"}, - "i26059":{"rank":"2"}, - "i26060":{"rank":"3"}, - "i26075":{"rank":"4"}, - "i26076":{"rank":"5"}, - "i26077":{"rank":"6"}, - "i26078":{"rank":"7"}, - "i26079":{"rank":"8"}, - "i26019":{"rank":"16"}, - "i26020":{"rank":"17"}, - "i26021":{"rank":"18"}, - "i26072":{"rank":"1"}, - "i26073":{"rank":"2"}, - "i26074":{"rank":"3"}, - "i26022":{"rank":"19"}, - "i26080":{"rank":"1"}, - "i26023":{"rank":"20"}, - "i26061":{"rank":"1"}, - "i26062":{"rank":"2"}, - "i26063":{"rank":"3"}, - "i26064":{"rank":"4"}, - "i26065":{"rank":"5"}, - "i26085":{"rank":"6"}, - "i26086":{"rank":"7"}, - "i26024":{"rank":"21"}, - "i26081":{"rank":"1"}, - "i26082":{"rank":"2"}, - "i26083":{"rank":"3"}, - "i26084":{"rank":"4"}, - "i26025":{"rank":"22"}, - "i26092":{"rank":"1"}, - "i26026":{"rank":"23"}, - "i26090":{"rank":"1"}, - "i26027":{"rank":"24"}, - "i26091":{"rank":"1"}, - "i26028":{"rank":"25"}, - "p607":{"rank":"3"}, - "i27050":{"rank":"1"}, - "i27051":{"rank":"2"}, - "i27052":{"rank":"3"}, - "i27053":{"rank":"4"}, - "i27049":{"rank":"4"}, - "i27055":{"rank":"1"}, - "i27054":{"rank":"5"}, - "i30048":{"rank":"6"}, - "p439":{"rank":"9"}, - "i11703":{"rank":"10"}, - "p419":{"rank":"-2"}, - "i30314":{"rank":"1"}, - "p649":{"rank":"1"}, - "i18178":{"rank":"1"}, - "i22193":{"rank":"1"}, - "i22192":{"rank":"1"}, - "i21198":{"rank":"2"}, - "p371":{"rank":"2"}, - "i31994":{"rank":"1"}, - "i28224":{"rank":"1"}, - "i28215":{"rank":"1"}, - "i24456":{"rank":"2"}, - "i13676":{"rank":"1"}, - "p478":{"rank":"3"}, - "p546":{"rank":"4"}, - "i25413":{"rank":"1"}, - "i25442":{"rank":"2"}, - "i25456":{"rank":"1"}, - "i25453":{"rank":"1"}, - "i25444":{"rank":"1"}, - "i25546":{"rank":"1"}, - "i25543":{"rank":"1"}, - "i25481":{"rank":"2"}, - "i25578":{"rank":"1"}, - "i25577":{"rank":"3"}, - "i25443":{"rank":"3"}, - "i25615":{"rank":"4"}, - "i25622":{"rank":"1"}, - "i25623":{"rank":"2"}, - "i25624":{"rank":"3"}, - "i25625":{"rank":"4"}, - "i25621":{"rank":"1"}, - "i25616":{"rank":"5"}, - "i25644":{"rank":"6"}, - "i25649":{"rank":"1"}, - "i25650":{"rank":"2"}, - "i25651":{"rank":"3"}, - "i25652":{"rank":"4"}, - "i25653":{"rank":"5"}, - "i25654":{"rank":"6"}, - "i25648":{"rank":"7"}, - "i25655":{"rank":"8"}, - "i25656":{"rank":"9"}, - "i25657":{"rank":"10"}, - "i25641":{"rank":"1"}, - "i25638":{"rank":"1"}, - "i25632":{"rank":"11"}, - "p599":{"rank":"5"}, - "i15869":{"rank":"1"}, - "i18410":{"rank":"2"}, - "i15866":{"rank":"1"}, - "i16286":{"rank":"1"}, - "i15874":{"rank":"2"}, - "i15875":{"rank":"3"}, - "i16086":{"rank":"4"}, - "i16519":{"rank":"5"}, - "i17496":{"rank":"6"}, - "i22388":{"rank":"1"}, - "i17940":{"rank":"7"}, - "i18411":{"rank":"8"}, - "i28346":{"rank":"1"}, - "i28956":{"rank":"2"}, - "i18412":{"rank":"9"}, - "i18559":{"rank":"10"}, - "i16084":{"rank":"1"}, - "i23365":{"rank":"1"}, - "i23370":{"rank":"1"}, - "i23371":{"rank":"2"}, - "i23372":{"rank":"3"}, - "i23373":{"rank":"4"}, - "i23374":{"rank":"5"}, - "i23375":{"rank":"6"}, - "i23376":{"rank":"7"}, - "i23377":{"rank":"8"}, - "i23378":{"rank":"9"}, - "i23379":{"rank":"10"}, - "i23380":{"rank":"11"}, - "i23381":{"rank":"12"}, - "i23382":{"rank":"13"}, - "i23383":{"rank":"14"}, - "i23384":{"rank":"15"}, - "i23385":{"rank":"16"}, - "i23386":{"rank":"17"}, - "i23387":{"rank":"18"}, - "i23388":{"rank":"19"}, - "i23389":{"rank":"20"}, - "i23390":{"rank":"21"}, - "i23391":{"rank":"22"}, - "i23392":{"rank":"23"}, - "i23393":{"rank":"24"}, - "i23394":{"rank":"25"}, - "i23395":{"rank":"26"}, - "i23396":{"rank":"27"}, - "i23369":{"rank":"1"}, - "i23368":{"rank":"2"}, - "i23397":{"rank":"3"}, - "i23401":{"rank":"1"}, - "i23402":{"rank":"2"}, - "i23403":{"rank":"3"}, - "i23400":{"rank":"1"}, - "i15867":{"rank":"1"}, - "i23405":{"rank":"2"}, - "i23406":{"rank":"3"}, - "i23407":{"rank":"4"}, - "i23404":{"rank":"2"}, - "i15879":{"rank":"1"}, - "i23409":{"rank":"2"}, - "i23410":{"rank":"3"}, - "i23411":{"rank":"4"}, - "i23408":{"rank":"3"}, - "i15873":{"rank":"1"}, - "i23413":{"rank":"2"}, - "i23414":{"rank":"3"}, - "i23415":{"rank":"4"}, - "i23412":{"rank":"4"}, - "i23399":{"rank":"1"}, - "i23418":{"rank":"1"}, - "i23419":{"rank":"2"}, - "i23420":{"rank":"3"}, - "i23417":{"rank":"1"}, - "i23422":{"rank":"1"}, - "i23423":{"rank":"2"}, - "i23424":{"rank":"3"}, - "i23421":{"rank":"2"}, - "i23426":{"rank":"1"}, - "i23427":{"rank":"2"}, - "i23428":{"rank":"3"}, - "i23429":{"rank":"4"}, - "i23425":{"rank":"3"}, - "i23432":{"rank":"1"}, - "i23433":{"rank":"2"}, - "i23434":{"rank":"3"}, - "i23431":{"rank":"1"}, - "i23435":{"rank":"2"}, - "i23430":{"rank":"4"}, - "i23416":{"rank":"2"}, - "i23440":{"rank":"1"}, - "i23441":{"rank":"2"}, - "i23439":{"rank":"1"}, - "i23443":{"rank":"1"}, - "i23444":{"rank":"2"}, - "i23442":{"rank":"2"}, - "i23438":{"rank":"1"}, - "i23446":{"rank":"1"}, - "i23447":{"rank":"2"}, - "i23448":{"rank":"3"}, - "i23445":{"rank":"2"}, - "i23450":{"rank":"1"}, - "i23451":{"rank":"2"}, - "i23452":{"rank":"3"}, - "i23449":{"rank":"3"}, - "i23454":{"rank":"1"}, - "i23455":{"rank":"2"}, - "i23456":{"rank":"3"}, - "i23453":{"rank":"4"}, - "i23458":{"rank":"1"}, - "i23459":{"rank":"2"}, - "i23460":{"rank":"3"}, - "i23457":{"rank":"5"}, - "i23462":{"rank":"1"}, - "i23463":{"rank":"2"}, - "i23464":{"rank":"3"}, - "i23461":{"rank":"6"}, - "i23466":{"rank":"1"}, - "i23467":{"rank":"2"}, - "i23468":{"rank":"3"}, - "i23465":{"rank":"7"}, - "i23470":{"rank":"1"}, - "i23471":{"rank":"2"}, - "i23472":{"rank":"3"}, - "i23469":{"rank":"8"}, - "i23474":{"rank":"1"}, - "i23475":{"rank":"2"}, - "i23476":{"rank":"3"}, - "i23473":{"rank":"9"}, - "i23478":{"rank":"1"}, - "i23479":{"rank":"2"}, - "i23480":{"rank":"3"}, - "i23477":{"rank":"10"}, - "i23482":{"rank":"1"}, - "i23483":{"rank":"2"}, - "i23484":{"rank":"3"}, - "i23481":{"rank":"11"}, - "i23486":{"rank":"1"}, - "i23487":{"rank":"2"}, - "i23488":{"rank":"3"}, - "i23485":{"rank":"12"}, - "i23490":{"rank":"1"}, - "i23491":{"rank":"2"}, - "i23492":{"rank":"3"}, - "i23489":{"rank":"13"}, - "i23494":{"rank":"1"}, - "i23495":{"rank":"2"}, - "i23496":{"rank":"3"}, - "i23497":{"rank":"4"}, - "i23493":{"rank":"14"}, - "i23437":{"rank":"1"}, - "i21333":{"rank":"1"}, - "i22387":{"rank":"2"}, - "i15871":{"rank":"1"}, - "i23500":{"rank":"1"}, - "i23499":{"rank":"2"}, - "i15872":{"rank":"1"}, - "i23502":{"rank":"2"}, - "i23504":{"rank":"1"}, - "i23505":{"rank":"2"}, - "i23506":{"rank":"3"}, - "i23503":{"rank":"3"}, - "i23501":{"rank":"3"}, - "i23508":{"rank":"1"}, - "i23507":{"rank":"4"}, - "i23510":{"rank":"1"}, - "i23509":{"rank":"5"}, - "i23511":{"rank":"6"}, - "i23513":{"rank":"1"}, - "i23514":{"rank":"2"}, - "i23512":{"rank":"7"}, - "i23516":{"rank":"1"}, - "i23515":{"rank":"8"}, - "i23518":{"rank":"1"}, - "i23517":{"rank":"9"}, - "i23520":{"rank":"1"}, - "i23521":{"rank":"2"}, - "i23519":{"rank":"10"}, - "i23523":{"rank":"1"}, - "i23522":{"rank":"11"}, - "i23525":{"rank":"1"}, - "i23524":{"rank":"12"}, - "i23527":{"rank":"1"}, - "i23526":{"rank":"13"}, - "i23529":{"rank":"1"}, - "i23530":{"rank":"2"}, - "i23531":{"rank":"3"}, - "i23528":{"rank":"14"}, - "i23498":{"rank":"2"}, - "i23436":{"rank":"3"}, - "i16062":{"rank":"1"}, - "i23533":{"rank":"1"}, - "i23532":{"rank":"4"}, - "i23537":{"rank":"1"}, - "i23538":{"rank":"2"}, - "i23539":{"rank":"3"}, - "i23535":{"rank":"1"}, - "i23541":{"rank":"1"}, - "i23542":{"rank":"2"}, - "i23543":{"rank":"3"}, - "i23540":{"rank":"2"}, - "i23546":{"rank":"1"}, - "i23547":{"rank":"2"}, - "i23545":{"rank":"1"}, - "i23549":{"rank":"1"}, - "i23550":{"rank":"2"}, - "i23548":{"rank":"2"}, - "i23552":{"rank":"1"}, - "i23553":{"rank":"2"}, - "i23551":{"rank":"3"}, - "i23555":{"rank":"1"}, - "i23556":{"rank":"2"}, - "i23554":{"rank":"4"}, - "i23557":{"rank":"5"}, - "i23544":{"rank":"3"}, - "i23534":{"rank":"5"}, - "i23559":{"rank":"1"}, - "i23560":{"rank":"2"}, - "i23562":{"rank":"1"}, - "i23563":{"rank":"2"}, - "i23564":{"rank":"3"}, - "i23565":{"rank":"4"}, - "i23566":{"rank":"5"}, - "i23567":{"rank":"6"}, - "i15870":{"rank":"1"}, - "i23568":{"rank":"7"}, - "i23561":{"rank":"3"}, - "i23569":{"rank":"4"}, - "i23558":{"rank":"6"}, - "i23398":{"rank":"4"}, - "i23570":{"rank":"5"}, - "i23574":{"rank":"1"}, - "i23572":{"rank":"1"}, - "i23575":{"rank":"2"}, - "i23577":{"rank":"1"}, - "i23578":{"rank":"2"}, - "i23579":{"rank":"3"}, - "i23580":{"rank":"4"}, - "i23576":{"rank":"3"}, - "i23582":{"rank":"1"}, - "i23583":{"rank":"2"}, - "i23581":{"rank":"4"}, - "i23585":{"rank":"1"}, - "i23586":{"rank":"2"}, - "i23584":{"rank":"5"}, - "i23571":{"rank":"6"}, - "i23589":{"rank":"1"}, - "i23588":{"rank":"1"}, - "i23591":{"rank":"1"}, - "i23592":{"rank":"2"}, - "i23590":{"rank":"2"}, - "i23594":{"rank":"1"}, - "i23595":{"rank":"2"}, - "i23596":{"rank":"3"}, - "i23593":{"rank":"3"}, - "i23597":{"rank":"4"}, - "i23598":{"rank":"5"}, - "i23587":{"rank":"7"}, - "i23600":{"rank":"1"}, - "i23601":{"rank":"2"}, - "i23602":{"rank":"3"}, - "i23599":{"rank":"8"}, - "i23604":{"rank":"1"}, - "i23605":{"rank":"2"}, - "i23606":{"rank":"3"}, - "i23607":{"rank":"4"}, - "i23608":{"rank":"5"}, - "i23609":{"rank":"6"}, - "i23603":{"rank":"9"}, - "i23610":{"rank":"10"}, - "i23611":{"rank":"11"}, - "i23612":{"rank":"12"}, - "i27819":{"rank":"1"}, - "i27821":{"rank":"2"}, - "i27822":{"rank":"3"}, - "i27823":{"rank":"4"}, - "i27824":{"rank":"5"}, - "i27818":{"rank":"13"}, - "p494":{"rank":"6"}, - "i27410":{"rank":"1"}, - "i22667":{"rank":"1"}, - "i23758":{"rank":"1"}, - "i23759":{"rank":"2"}, - "i27413":{"rank":"3"}, - "p590":{"rank":"7"}, - "i27857":{"rank":"1"}, - "i27956":{"rank":"2"}, - "i27958":{"rank":"1"}, - "i27959":{"rank":"2"}, - "i27957":{"rank":"3"}, - "i26954":{"rank":"1"}, - "i27981":{"rank":"1"}, - "i26955":{"rank":"2"}, - "i27974":{"rank":"1"}, - "i27976":{"rank":"2"}, - "i27977":{"rank":"3"}, - "i27978":{"rank":"4"}, - "i27807":{"rank":"3"}, - "i27827":{"rank":"4"}, - "i28045":{"rank":"1"}, - "i28046":{"rank":"2"}, - "i28047":{"rank":"3"}, - "i28048":{"rank":"4"}, - "i28049":{"rank":"5"}, - "i28050":{"rank":"6"}, - "i28051":{"rank":"7"}, - "i28052":{"rank":"8"}, - "i28053":{"rank":"9"}, - "i28054":{"rank":"10"}, - "i28055":{"rank":"11"}, - "i28056":{"rank":"12"}, - "i27984":{"rank":"1"}, - "i27985":{"rank":"2"}, - "i28058":{"rank":"1"}, - "i28059":{"rank":"2"}, - "i28060":{"rank":"3"}, - "i27987":{"rank":"3"}, - "i28061":{"rank":"1"}, - "i27992":{"rank":"4"}, - "i27993":{"rank":"5"}, - "i27994":{"rank":"6"}, - "i28062":{"rank":"1"}, - "i27995":{"rank":"7"}, - "i27850":{"rank":"1"}, - "i27936":{"rank":"1"}, - "i29268":{"rank":"2"}, - "i29269":{"rank":"3"}, - "i27851":{"rank":"2"}, - "i28036":{"rank":"1"}, - "i28071":{"rank":"2"}, - "i28166":{"rank":"3"}, - "i28349":{"rank":"4"}, - "i27979":{"rank":"3"}, - "i28044":{"rank":"4"}, - "i27849":{"rank":"5"}, - "i27968":{"rank":"1"}, - "i27973":{"rank":"2"}, - "i27806":{"rank":"1"}, - "i27955":{"rank":"6"}, - "i27980":{"rank":"7"}, - "i29258":{"rank":"1"}, - "i29257":{"rank":"8"}, - "p621":{"rank":"8"}, - "i27451":{"rank":"1"}, - "i27453":{"rank":"2"}, - "i27455":{"rank":"3"}, - "i27456":{"rank":"4"}, - "i27457":{"rank":"5"}, - "i27458":{"rank":"6"}, - "i27459":{"rank":"7"}, - "i27462":{"rank":"8"}, - "i27463":{"rank":"9"}, - "i27464":{"rank":"10"}, - "i27465":{"rank":"11"}, - "i27466":{"rank":"12"}, - "i27467":{"rank":"13"}, - "i27469":{"rank":"14"}, - "i27471":{"rank":"15"}, - "i27472":{"rank":"16"}, - "i27473":{"rank":"17"}, - "i27475":{"rank":"18"}, - "i27450":{"rank":"1"}, - "i27449":{"rank":"1"}, - "i27478":{"rank":"2"}, - "i27486":{"rank":"1"}, - "i27488":{"rank":"2"}, - "i27485":{"rank":"1"}, - "i27492":{"rank":"1"}, - "i27489":{"rank":"2"}, - "i27493":{"rank":"3"}, - "i27480":{"rank":"1"}, - "i27497":{"rank":"2"}, - "i27578":{"rank":"1"}, - "i27574":{"rank":"1"}, - "i27518":{"rank":"1"}, - "i27601":{"rank":"1"}, - "i27602":{"rank":"2"}, - "i27600":{"rank":"1"}, - "i27610":{"rank":"1"}, - "i27611":{"rank":"2"}, - "i27612":{"rank":"3"}, - "i27609":{"rank":"2"}, - "i27579":{"rank":"2"}, - "i27517":{"rank":"3"}, - "i27614":{"rank":"1"}, - "i27613":{"rank":"4"}, - "i27479":{"rank":"3"}, - "i27651":{"rank":"4"}, - "i27786":{"rank":"1"}, - "i27680":{"rank":"5"}, - "i27685":{"rank":"1"}, - "i27686":{"rank":"2"}, - "i27687":{"rank":"3"}, - "i27688":{"rank":"4"}, - "i27689":{"rank":"5"}, - "i27684":{"rank":"6"}, - "i27691":{"rank":"7"}, - "i27692":{"rank":"8"}, - "i27693":{"rank":"9"}, - "i30267":{"rank":"1"}, - "i30268":{"rank":"2"}, - "i30265":{"rank":"10"}, - "p633":{"rank":"9"}, - "i32834":{"rank":"1"}, - "i32835":{"rank":"2"}, - "i32836":{"rank":"3"}, - "i32837":{"rank":"4"}, - "i32838":{"rank":"5"}, - "i32839":{"rank":"6"}, - "i32840":{"rank":"7"}, - "i32841":{"rank":"8"}, - "i32842":{"rank":"9"}, - "i32843":{"rank":"10"}, - "i32844":{"rank":"11"}, - "i32845":{"rank":"12"}, - "i32846":{"rank":"13"}, - "i32847":{"rank":"14"}, - "i32848":{"rank":"15"}, - "i32849":{"rank":"16"}, - "i32850":{"rank":"17"}, - "i32851":{"rank":"18"}, - "i32852":{"rank":"19"}, - "i32853":{"rank":"20"}, - "i32854":{"rank":"21"}, - "i32855":{"rank":"22"}, - "i32856":{"rank":"23"}, - "i32857":{"rank":"24"}, - "i32858":{"rank":"25"}, - "i32859":{"rank":"26"}, - "i32860":{"rank":"27"}, - "i32833":{"rank":"1"}, - "i32832":{"rank":"1"}, - "i32861":{"rank":"2"}, - "i32865":{"rank":"1"}, - "i32866":{"rank":"2"}, - "i32867":{"rank":"3"}, - "i32864":{"rank":"1"}, - "i32869":{"rank":"1"}, - "i32870":{"rank":"2"}, - "i32871":{"rank":"3"}, - "i32868":{"rank":"2"}, - "i32873":{"rank":"1"}, - "i32874":{"rank":"2"}, - "i32875":{"rank":"3"}, - "i32872":{"rank":"3"}, - "i32877":{"rank":"1"}, - "i32878":{"rank":"2"}, - "i32879":{"rank":"3"}, - "i32876":{"rank":"4"}, - "i32863":{"rank":"1"}, - "i32882":{"rank":"1"}, - "i32883":{"rank":"2"}, - "i32884":{"rank":"3"}, - "i32881":{"rank":"1"}, - "i32886":{"rank":"1"}, - "i32887":{"rank":"2"}, - "i32888":{"rank":"3"}, - "i32885":{"rank":"2"}, - "i32890":{"rank":"1"}, - "i32891":{"rank":"2"}, - "i32892":{"rank":"3"}, - "i32893":{"rank":"4"}, - "i32889":{"rank":"3"}, - "i32896":{"rank":"1"}, - "i32897":{"rank":"2"}, - "i32898":{"rank":"3"}, - "i32895":{"rank":"1"}, - "i32899":{"rank":"2"}, - "i32894":{"rank":"4"}, - "i32880":{"rank":"2"}, - "i32904":{"rank":"1"}, - "i32905":{"rank":"2"}, - "i32903":{"rank":"1"}, - "i32907":{"rank":"1"}, - "i32908":{"rank":"2"}, - "i32906":{"rank":"2"}, - "i32902":{"rank":"1"}, - "i32911":{"rank":"1"}, - "i32912":{"rank":"2"}, - "i32909":{"rank":"2"}, - "i32914":{"rank":"1"}, - "i32915":{"rank":"2"}, - "i32916":{"rank":"3"}, - "i32913":{"rank":"3"}, - "i32918":{"rank":"1"}, - "i32919":{"rank":"2"}, - "i32920":{"rank":"3"}, - "i32917":{"rank":"4"}, - "i32922":{"rank":"1"}, - "i32923":{"rank":"2"}, - "i32924":{"rank":"3"}, - "i32921":{"rank":"5"}, - "i32926":{"rank":"1"}, - "i32927":{"rank":"2"}, - "i32928":{"rank":"3"}, - "i32925":{"rank":"6"}, - "i32930":{"rank":"1"}, - "i32931":{"rank":"2"}, - "i32932":{"rank":"3"}, - "i32929":{"rank":"7"}, - "i32934":{"rank":"1"}, - "i32935":{"rank":"2"}, - "i32936":{"rank":"3"}, - "i32933":{"rank":"8"}, - "i32938":{"rank":"1"}, - "i32939":{"rank":"2"}, - "i32940":{"rank":"3"}, - "i32937":{"rank":"9"}, - "i32942":{"rank":"1"}, - "i32943":{"rank":"2"}, - "i32944":{"rank":"3"}, - "i32941":{"rank":"10"}, - "i32946":{"rank":"1"}, - "i32947":{"rank":"2"}, - "i32948":{"rank":"3"}, - "i32945":{"rank":"11"}, - "i32950":{"rank":"1"}, - "i32951":{"rank":"2"}, - "i32952":{"rank":"3"}, - "i32949":{"rank":"12"}, - "i32954":{"rank":"1"}, - "i32955":{"rank":"2"}, - "i32956":{"rank":"3"}, - "i32953":{"rank":"13"}, - "i32958":{"rank":"1"}, - "i32959":{"rank":"2"}, - "i32960":{"rank":"3"}, - "i32961":{"rank":"4"}, - "i32957":{"rank":"14"}, - "i32901":{"rank":"1"}, - "i32964":{"rank":"1"}, - "i32963":{"rank":"1"}, - "i32966":{"rank":"1"}, - "i32968":{"rank":"1"}, - "i32969":{"rank":"2"}, - "i32970":{"rank":"3"}, - "i32967":{"rank":"2"}, - "i32965":{"rank":"2"}, - "i32972":{"rank":"1"}, - "i32971":{"rank":"3"}, - "i32974":{"rank":"1"}, - "i32973":{"rank":"4"}, - "i32975":{"rank":"5"}, - "i32977":{"rank":"1"}, - "i32978":{"rank":"2"}, - "i32976":{"rank":"6"}, - "i32980":{"rank":"1"}, - "i32979":{"rank":"7"}, - "i32982":{"rank":"1"}, - "i32981":{"rank":"8"}, - "i32984":{"rank":"1"}, - "i32985":{"rank":"2"}, - "i32983":{"rank":"9"}, - "i32987":{"rank":"1"}, - "i32986":{"rank":"10"}, - "i32989":{"rank":"1"}, - "i32988":{"rank":"11"}, - "i32991":{"rank":"1"}, - "i32990":{"rank":"12"}, - "i32993":{"rank":"1"}, - "i32994":{"rank":"2"}, - "i32995":{"rank":"3"}, - "i32992":{"rank":"13"}, - "i32962":{"rank":"2"}, - "i32900":{"rank":"3"}, - "i32997":{"rank":"1"}, - "i32996":{"rank":"4"}, - "i33000":{"rank":"1"}, - "i33001":{"rank":"2"}, - "i33002":{"rank":"3"}, - "i33003":{"rank":"4"}, - "i32999":{"rank":"1"}, - "i33005":{"rank":"1"}, - "i33006":{"rank":"2"}, - "i33007":{"rank":"3"}, - "i33004":{"rank":"2"}, - "i33010":{"rank":"1"}, - "i33011":{"rank":"2"}, - "i33009":{"rank":"1"}, - "i33013":{"rank":"1"}, - "i33014":{"rank":"2"}, - "i33012":{"rank":"2"}, - "i33016":{"rank":"1"}, - "i33017":{"rank":"2"}, - "i33015":{"rank":"3"}, - "i33019":{"rank":"1"}, - "i33020":{"rank":"2"}, - "i33018":{"rank":"4"}, - "i33021":{"rank":"5"}, - "i33008":{"rank":"3"}, - "i32998":{"rank":"5"}, - "i33023":{"rank":"1"}, - "i33024":{"rank":"2"}, - "i33026":{"rank":"1"}, - "i33027":{"rank":"2"}, - "i33028":{"rank":"3"}, - "i33029":{"rank":"4"}, - "i33030":{"rank":"5"}, - "i33031":{"rank":"6"}, - "i33032":{"rank":"7"}, - "i33025":{"rank":"3"}, - "i33033":{"rank":"4"}, - "i33022":{"rank":"6"}, - "i32862":{"rank":"3"}, - "i33034":{"rank":"4"}, - "i33037":{"rank":"1"}, - "i33038":{"rank":"2"}, - "i33036":{"rank":"1"}, - "i33039":{"rank":"2"}, - "i33041":{"rank":"1"}, - "i33042":{"rank":"2"}, - "i33043":{"rank":"3"}, - "i33044":{"rank":"4"}, - "i33040":{"rank":"3"}, - "i33046":{"rank":"1"}, - "i33047":{"rank":"2"}, - "i33045":{"rank":"4"}, - "i33049":{"rank":"1"}, - "i33050":{"rank":"2"}, - "i33048":{"rank":"5"}, - "i33035":{"rank":"5"}, - "i33053":{"rank":"1"}, - "i33052":{"rank":"1"}, - "i33055":{"rank":"1"}, - "i33056":{"rank":"2"}, - "i33054":{"rank":"2"}, - "i33058":{"rank":"1"}, - "i33059":{"rank":"2"}, - "i33060":{"rank":"3"}, - "i33057":{"rank":"3"}, - "i33061":{"rank":"4"}, - "i33062":{"rank":"5"}, - "i33051":{"rank":"6"}, - "i33064":{"rank":"1"}, - "i33065":{"rank":"2"}, - "i33066":{"rank":"3"}, - "i33067":{"rank":"4"}, - "i33063":{"rank":"7"}, - "i33069":{"rank":"1"}, - "i33070":{"rank":"2"}, - "i33071":{"rank":"3"}, - "i33072":{"rank":"4"}, - "i33073":{"rank":"5"}, - "i33074":{"rank":"6"}, - "i33068":{"rank":"8"}, - "i33075":{"rank":"9"}, - "i33076":{"rank":"10"}, - "i33077":{"rank":"11"}, - "p688":{"rank":"10"}, - "i32588":{"rank":"1"}, - "i32589":{"rank":"2"}, - "i32590":{"rank":"3"}, - "i32591":{"rank":"4"}, - "i32592":{"rank":"5"}, - "i32593":{"rank":"6"}, - "i32594":{"rank":"7"}, - "i32595":{"rank":"8"}, - "i32596":{"rank":"9"}, - "i32597":{"rank":"10"}, - "i32598":{"rank":"11"}, - "i32599":{"rank":"12"}, - "i32600":{"rank":"13"}, - "i32601":{"rank":"14"}, - "i32602":{"rank":"15"}, - "i32603":{"rank":"16"}, - "i32604":{"rank":"17"}, - "i32605":{"rank":"18"}, - "i32606":{"rank":"19"}, - "i32607":{"rank":"20"}, - "i32608":{"rank":"21"}, - "i32609":{"rank":"22"}, - "i32610":{"rank":"23"}, - "i32611":{"rank":"24"}, - "i32612":{"rank":"25"}, - "i32613":{"rank":"26"}, - "i32614":{"rank":"27"}, - "i32587":{"rank":"1"}, - "i32586":{"rank":"1"}, - "i32615":{"rank":"2"}, - "i32619":{"rank":"1"}, - "i32620":{"rank":"2"}, - "i32621":{"rank":"3"}, - "i32618":{"rank":"1"}, - "i32623":{"rank":"1"}, - "i32624":{"rank":"2"}, - "i32625":{"rank":"3"}, - "i32622":{"rank":"2"}, - "i32627":{"rank":"1"}, - "i32628":{"rank":"2"}, - "i32629":{"rank":"3"}, - "i32626":{"rank":"3"}, - "i32631":{"rank":"1"}, - "i32632":{"rank":"2"}, - "i32633":{"rank":"3"}, - "i32630":{"rank":"4"}, - "i32617":{"rank":"1"}, - "i32636":{"rank":"1"}, - "i32637":{"rank":"2"}, - "i32638":{"rank":"3"}, - "i32635":{"rank":"1"}, - "i32640":{"rank":"1"}, - "i32641":{"rank":"2"}, - "i32642":{"rank":"3"}, - "i32639":{"rank":"2"}, - "i32644":{"rank":"1"}, - "i32645":{"rank":"2"}, - "i32646":{"rank":"3"}, - "i32647":{"rank":"4"}, - "i32643":{"rank":"3"}, - "i32650":{"rank":"1"}, - "i32651":{"rank":"2"}, - "i32652":{"rank":"3"}, - "i32649":{"rank":"1"}, - "i32653":{"rank":"2"}, - "i32648":{"rank":"4"}, - "i32634":{"rank":"2"}, - "i32658":{"rank":"1"}, - "i32659":{"rank":"2"}, - "i32657":{"rank":"1"}, - "i32661":{"rank":"1"}, - "i32662":{"rank":"2"}, - "i32660":{"rank":"2"}, - "i32656":{"rank":"1"}, - "i32665":{"rank":"1"}, - "i32666":{"rank":"2"}, - "i32663":{"rank":"2"}, - "i32668":{"rank":"1"}, - "i32669":{"rank":"2"}, - "i32670":{"rank":"3"}, - "i32667":{"rank":"3"}, - "i32672":{"rank":"1"}, - "i32673":{"rank":"2"}, - "i32674":{"rank":"3"}, - "i32671":{"rank":"4"}, - "i32676":{"rank":"1"}, - "i32677":{"rank":"2"}, - "i32678":{"rank":"3"}, - "i32675":{"rank":"5"}, - "i32680":{"rank":"1"}, - "i32681":{"rank":"2"}, - "i32682":{"rank":"3"}, - "i32679":{"rank":"6"}, - "i32684":{"rank":"1"}, - "i32685":{"rank":"2"}, - "i32686":{"rank":"3"}, - "i32683":{"rank":"7"}, - "i32688":{"rank":"1"}, - "i32689":{"rank":"2"}, - "i32690":{"rank":"3"}, - "i32687":{"rank":"8"}, - "i32692":{"rank":"1"}, - "i32693":{"rank":"2"}, - "i32694":{"rank":"3"}, - "i32691":{"rank":"9"}, - "i32696":{"rank":"1"}, - "i32697":{"rank":"2"}, - "i32698":{"rank":"3"}, - "i32695":{"rank":"10"}, - "i32700":{"rank":"1"}, - "i32701":{"rank":"2"}, - "i32702":{"rank":"3"}, - "i32699":{"rank":"11"}, - "i32704":{"rank":"1"}, - "i32705":{"rank":"2"}, - "i32706":{"rank":"3"}, - "i32703":{"rank":"12"}, - "i32708":{"rank":"1"}, - "i32709":{"rank":"2"}, - "i32710":{"rank":"3"}, - "i32707":{"rank":"13"}, - "i32712":{"rank":"1"}, - "i32713":{"rank":"2"}, - "i32714":{"rank":"3"}, - "i32715":{"rank":"4"}, - "i32711":{"rank":"14"}, - "i32655":{"rank":"1"}, - "i32718":{"rank":"1"}, - "i32717":{"rank":"1"}, - "i32720":{"rank":"1"}, - "i32722":{"rank":"1"}, - "i32723":{"rank":"2"}, - "i32724":{"rank":"3"}, - "i32721":{"rank":"2"}, - "i32719":{"rank":"2"}, - "i32726":{"rank":"1"}, - "i32725":{"rank":"3"}, - "i32728":{"rank":"1"}, - "i32727":{"rank":"4"}, - "i32729":{"rank":"5"}, - "i32731":{"rank":"1"}, - "i32732":{"rank":"2"}, - "i32730":{"rank":"6"}, - "i32734":{"rank":"1"}, - "i32733":{"rank":"7"}, - "i32736":{"rank":"1"}, - "i32735":{"rank":"8"}, - "i32738":{"rank":"1"}, - "i32739":{"rank":"2"}, - "i32737":{"rank":"9"}, - "i32741":{"rank":"1"}, - "i32740":{"rank":"10"}, - "i32743":{"rank":"1"}, - "i32742":{"rank":"11"}, - "i32745":{"rank":"1"}, - "i32744":{"rank":"12"}, - "i32747":{"rank":"1"}, - "i32748":{"rank":"2"}, - "i32749":{"rank":"3"}, - "i32746":{"rank":"13"}, - "i32716":{"rank":"2"}, - "i32654":{"rank":"3"}, - "i32751":{"rank":"1"}, - "i32750":{"rank":"4"}, - "i32754":{"rank":"1"}, - "i32755":{"rank":"2"}, - "i32756":{"rank":"3"}, - "i32757":{"rank":"4"}, - "i32753":{"rank":"1"}, - "i32759":{"rank":"1"}, - "i32760":{"rank":"2"}, - "i32761":{"rank":"3"}, - "i32758":{"rank":"2"}, - "i32764":{"rank":"1"}, - "i32765":{"rank":"2"}, - "i32763":{"rank":"1"}, - "i32767":{"rank":"1"}, - "i32768":{"rank":"2"}, - "i32766":{"rank":"2"}, - "i32770":{"rank":"1"}, - "i32771":{"rank":"2"}, - "i32769":{"rank":"3"}, - "i32773":{"rank":"1"}, - "i32774":{"rank":"2"}, - "i32772":{"rank":"4"}, - "i32775":{"rank":"5"}, - "i32762":{"rank":"3"}, - "i32752":{"rank":"5"}, - "i32777":{"rank":"1"}, - "i32778":{"rank":"2"}, - "i32780":{"rank":"1"}, - "i32781":{"rank":"2"}, - "i32782":{"rank":"3"}, - "i32783":{"rank":"4"}, - "i32784":{"rank":"5"}, - "i32785":{"rank":"6"}, - "i32786":{"rank":"7"}, - "i32779":{"rank":"3"}, - "i32787":{"rank":"4"}, - "i32776":{"rank":"6"}, - "i32616":{"rank":"3"}, - "i32788":{"rank":"4"}, - "i32791":{"rank":"1"}, - "i32792":{"rank":"2"}, - "i32790":{"rank":"1"}, - "i32793":{"rank":"2"}, - "i32795":{"rank":"1"}, - "i32796":{"rank":"2"}, - "i32797":{"rank":"3"}, - "i32798":{"rank":"4"}, - "i32794":{"rank":"3"}, - "i32800":{"rank":"1"}, - "i32801":{"rank":"2"}, - "i32799":{"rank":"4"}, - "i32803":{"rank":"1"}, - "i32804":{"rank":"2"}, - "i32802":{"rank":"5"}, - "i32789":{"rank":"5"}, - "i32807":{"rank":"1"}, - "i32806":{"rank":"1"}, - "i32809":{"rank":"1"}, - "i32810":{"rank":"2"}, - "i32808":{"rank":"2"}, - "i32812":{"rank":"1"}, - "i32813":{"rank":"2"}, - "i32814":{"rank":"3"}, - "i32811":{"rank":"3"}, - "i32815":{"rank":"4"}, - "i32816":{"rank":"5"}, - "i32805":{"rank":"6"}, - "i32818":{"rank":"1"}, - "i32819":{"rank":"2"}, - "i32820":{"rank":"3"}, - "i32821":{"rank":"4"}, - "i32817":{"rank":"7"}, - "i32823":{"rank":"1"}, - "i32824":{"rank":"2"}, - "i32825":{"rank":"3"}, - "i32826":{"rank":"4"}, - "i32827":{"rank":"5"}, - "i32828":{"rank":"6"}, - "i32822":{"rank":"8"}, - "i32829":{"rank":"9"}, - "i32830":{"rank":"10"}, - "i32831":{"rank":"11"}, - "p689":{"rank":"11"}, - "i33080":{"rank":"1"}, - "i33081":{"rank":"2"}, - "i33082":{"rank":"3"}, - "i33083":{"rank":"4"}, - "i33084":{"rank":"5"}, - "i33085":{"rank":"6"}, - "i33086":{"rank":"7"}, - "i33087":{"rank":"8"}, - "i33088":{"rank":"9"}, - "i33089":{"rank":"10"}, - "i33090":{"rank":"11"}, - "i33091":{"rank":"12"}, - "i33092":{"rank":"13"}, - "i33093":{"rank":"14"}, - "i33094":{"rank":"15"}, - "i33095":{"rank":"16"}, - "i33096":{"rank":"17"}, - "i33097":{"rank":"18"}, - "i33098":{"rank":"19"}, - "i33099":{"rank":"20"}, - "i33100":{"rank":"21"}, - "i33101":{"rank":"22"}, - "i33102":{"rank":"23"}, - "i33103":{"rank":"24"}, - "i33104":{"rank":"25"}, - "i33105":{"rank":"26"}, - "i33106":{"rank":"27"}, - "i33079":{"rank":"1"}, - "i33078":{"rank":"1"}, - "i33107":{"rank":"2"}, - "i33111":{"rank":"1"}, - "i33112":{"rank":"2"}, - "i33113":{"rank":"3"}, - "i33110":{"rank":"1"}, - "i33115":{"rank":"1"}, - "i33116":{"rank":"2"}, - "i33117":{"rank":"3"}, - "i33114":{"rank":"2"}, - "i33119":{"rank":"1"}, - "i33120":{"rank":"2"}, - "i33121":{"rank":"3"}, - "i33118":{"rank":"3"}, - "i33123":{"rank":"1"}, - "i33124":{"rank":"2"}, - "i33125":{"rank":"3"}, - "i33122":{"rank":"4"}, - "i33109":{"rank":"1"}, - "i33128":{"rank":"1"}, - "i33129":{"rank":"2"}, - "i33130":{"rank":"3"}, - "i33127":{"rank":"1"}, - "i33132":{"rank":"1"}, - "i33133":{"rank":"2"}, - "i33134":{"rank":"3"}, - "i33131":{"rank":"2"}, - "i33136":{"rank":"1"}, - "i33137":{"rank":"2"}, - "i33138":{"rank":"3"}, - "i33139":{"rank":"4"}, - "i33135":{"rank":"3"}, - "i33142":{"rank":"1"}, - "i33143":{"rank":"2"}, - "i33144":{"rank":"3"}, - "i33141":{"rank":"1"}, - "i33145":{"rank":"2"}, - "i33140":{"rank":"4"}, - "i33126":{"rank":"2"}, - "i33150":{"rank":"1"}, - "i33151":{"rank":"2"}, - "i33149":{"rank":"1"}, - "i33153":{"rank":"1"}, - "i33154":{"rank":"2"}, - "i33152":{"rank":"2"}, - "i33148":{"rank":"1"}, - "i33157":{"rank":"1"}, - "i33158":{"rank":"2"}, - "i33155":{"rank":"2"}, - "i33160":{"rank":"1"}, - "i33161":{"rank":"2"}, - "i33162":{"rank":"3"}, - "i33159":{"rank":"3"}, - "i33164":{"rank":"1"}, - "i33165":{"rank":"2"}, - "i33166":{"rank":"3"}, - "i33163":{"rank":"4"}, - "i33168":{"rank":"1"}, - "i33169":{"rank":"2"}, - "i33170":{"rank":"3"}, - "i33167":{"rank":"5"}, - "i33172":{"rank":"1"}, - "i33173":{"rank":"2"}, - "i33174":{"rank":"3"}, - "i33171":{"rank":"6"}, - "i33176":{"rank":"1"}, - "i33177":{"rank":"2"}, - "i33178":{"rank":"3"}, - "i33175":{"rank":"7"}, - "i33180":{"rank":"1"}, - "i33181":{"rank":"2"}, - "i33182":{"rank":"3"}, - "i33179":{"rank":"8"}, - "i33184":{"rank":"1"}, - "i33185":{"rank":"2"}, - "i33186":{"rank":"3"}, - "i33183":{"rank":"9"}, - "i33188":{"rank":"1"}, - "i33189":{"rank":"2"}, - "i33190":{"rank":"3"}, - "i33187":{"rank":"10"}, - "i33192":{"rank":"1"}, - "i33193":{"rank":"2"}, - "i33194":{"rank":"3"}, - "i33191":{"rank":"11"}, - "i33196":{"rank":"1"}, - "i33197":{"rank":"2"}, - "i33198":{"rank":"3"}, - "i33195":{"rank":"12"}, - "i33200":{"rank":"1"}, - "i33201":{"rank":"2"}, - "i33202":{"rank":"3"}, - "i33199":{"rank":"13"}, - "i33204":{"rank":"1"}, - "i33205":{"rank":"2"}, - "i33206":{"rank":"3"}, - "i33207":{"rank":"4"}, - "i33203":{"rank":"14"}, - "i33147":{"rank":"1"}, - "i33210":{"rank":"1"}, - "i33209":{"rank":"1"}, - "i33212":{"rank":"1"}, - "i33214":{"rank":"1"}, - "i33215":{"rank":"2"}, - "i33216":{"rank":"3"}, - "i33213":{"rank":"2"}, - "i33211":{"rank":"2"}, - "i33218":{"rank":"1"}, - "i33217":{"rank":"3"}, - "i33220":{"rank":"1"}, - "i33219":{"rank":"4"}, - "i33221":{"rank":"5"}, - "i33223":{"rank":"1"}, - "i33224":{"rank":"2"}, - "i33222":{"rank":"6"}, - "i33226":{"rank":"1"}, - "i33225":{"rank":"7"}, - "i33228":{"rank":"1"}, - "i33227":{"rank":"8"}, - "i33230":{"rank":"1"}, - "i33231":{"rank":"2"}, - "i33229":{"rank":"9"}, - "i33233":{"rank":"1"}, - "i33232":{"rank":"10"}, - "i33235":{"rank":"1"}, - "i33234":{"rank":"11"}, - "i33237":{"rank":"1"}, - "i33236":{"rank":"12"}, - "i33239":{"rank":"1"}, - "i33240":{"rank":"2"}, - "i33241":{"rank":"3"}, - "i33238":{"rank":"13"}, - "i33208":{"rank":"2"}, - "i33146":{"rank":"3"}, - "i33243":{"rank":"1"}, - "i33242":{"rank":"4"}, - "i33246":{"rank":"1"}, - "i33247":{"rank":"2"}, - "i33248":{"rank":"3"}, - "i33249":{"rank":"4"}, - "i33245":{"rank":"1"}, - "i33251":{"rank":"1"}, - "i33252":{"rank":"2"}, - "i33253":{"rank":"3"}, - "i33250":{"rank":"2"}, - "i33256":{"rank":"1"}, - "i33257":{"rank":"2"}, - "i33255":{"rank":"1"}, - "i33259":{"rank":"1"}, - "i33260":{"rank":"2"}, - "i33258":{"rank":"2"}, - "i33262":{"rank":"1"}, - "i33263":{"rank":"2"}, - "i33261":{"rank":"3"}, - "i33265":{"rank":"1"}, - "i33266":{"rank":"2"}, - "i33264":{"rank":"4"}, - "i33267":{"rank":"5"}, - "i33254":{"rank":"3"}, - "i33244":{"rank":"5"}, - "i33269":{"rank":"1"}, - "i33270":{"rank":"2"}, - "i33272":{"rank":"1"}, - "i33273":{"rank":"2"}, - "i33274":{"rank":"3"}, - "i33275":{"rank":"4"}, - "i33276":{"rank":"5"}, - "i33277":{"rank":"6"}, - "i33278":{"rank":"7"}, - "i33271":{"rank":"3"}, - "i33279":{"rank":"4"}, - "i33268":{"rank":"6"}, - "i33108":{"rank":"3"}, - "i33280":{"rank":"4"}, - "i33283":{"rank":"1"}, - "i33284":{"rank":"2"}, - "i33282":{"rank":"1"}, - "i33285":{"rank":"2"}, - "i33287":{"rank":"1"}, - "i33288":{"rank":"2"}, - "i33289":{"rank":"3"}, - "i33290":{"rank":"4"}, - "i33286":{"rank":"3"}, - "i33292":{"rank":"1"}, - "i33293":{"rank":"2"}, - "i33291":{"rank":"4"}, - "i33295":{"rank":"1"}, - "i33296":{"rank":"2"}, - "i33294":{"rank":"5"}, - "i33281":{"rank":"5"}, - "i33299":{"rank":"1"}, - "i33298":{"rank":"1"}, - "i33301":{"rank":"1"}, - "i33302":{"rank":"2"}, - "i33300":{"rank":"2"}, - "i33304":{"rank":"1"}, - "i33305":{"rank":"2"}, - "i33306":{"rank":"3"}, - "i33303":{"rank":"3"}, - "i33307":{"rank":"4"}, - "i33308":{"rank":"5"}, - "i33297":{"rank":"6"}, - "i33310":{"rank":"1"}, - "i33311":{"rank":"2"}, - "i33312":{"rank":"3"}, - "i33313":{"rank":"4"}, - "i33309":{"rank":"7"}, - "i33315":{"rank":"1"}, - "i33316":{"rank":"2"}, - "i33317":{"rank":"3"}, - "i33318":{"rank":"4"}, - "i33319":{"rank":"5"}, - "i33320":{"rank":"6"}, - "i33314":{"rank":"8"}, - "i33321":{"rank":"9"}, - "i33322":{"rank":"10"}, - "i33323":{"rank":"11"}, - "p687":{"rank":"12"}, - "i17511":{"rank":"1"}, - "i28408":{"rank":"2"}, - "p519":{"rank":"13"}, - "p581":{"rank":"14"}, - "i24225":{"rank":"1"}, - "i24233":{"rank":"2"}, - "i24234":{"rank":"3"}, - "p593":{"rank":"15"}, - "i30468":{"rank":"1"}, - "i30469":{"rank":"2"}, - "i30470":{"rank":"3"}, - "i30471":{"rank":"4"}, - "i30472":{"rank":"5"}, - "i30473":{"rank":"6"}, - "i30474":{"rank":"7"}, - "i30475":{"rank":"8"}, - "i30476":{"rank":"9"}, - "i30477":{"rank":"10"}, - "i30478":{"rank":"11"}, - "i30479":{"rank":"12"}, - "i30480":{"rank":"13"}, - "i30481":{"rank":"14"}, - "i30482":{"rank":"15"}, - "i30483":{"rank":"16"}, - "i30484":{"rank":"17"}, - "i30485":{"rank":"18"}, - "i30486":{"rank":"19"}, - "i30487":{"rank":"20"}, - "i30488":{"rank":"21"}, - "i30489":{"rank":"22"}, - "i30490":{"rank":"23"}, - "i30491":{"rank":"24"}, - "i30492":{"rank":"25"}, - "i30493":{"rank":"26"}, - "i30494":{"rank":"27"}, - "i30467":{"rank":"1"}, - "i30466":{"rank":"1"}, - "i30495":{"rank":"2"}, - "i30499":{"rank":"1"}, - "i30500":{"rank":"2"}, - "i30501":{"rank":"3"}, - "i30498":{"rank":"1"}, - "i30503":{"rank":"1"}, - "i30504":{"rank":"2"}, - "i30505":{"rank":"3"}, - "i30502":{"rank":"2"}, - "i30509":{"rank":"1"}, - "i30506":{"rank":"3"}, - "i30511":{"rank":"1"}, - "i30512":{"rank":"2"}, - "i30513":{"rank":"3"}, - "i30510":{"rank":"4"}, - "i30497":{"rank":"1"}, - "i30516":{"rank":"1"}, - "i30517":{"rank":"2"}, - "i30518":{"rank":"3"}, - "i30515":{"rank":"1"}, - "i30520":{"rank":"1"}, - "i30521":{"rank":"2"}, - "i30522":{"rank":"3"}, - "i30519":{"rank":"2"}, - "i30524":{"rank":"1"}, - "i30525":{"rank":"2"}, - "i30526":{"rank":"3"}, - "i30527":{"rank":"4"}, - "i30523":{"rank":"3"}, - "i30530":{"rank":"1"}, - "i30531":{"rank":"2"}, - "i30532":{"rank":"3"}, - "i30529":{"rank":"1"}, - "i30533":{"rank":"2"}, - "i30528":{"rank":"4"}, - "i30514":{"rank":"2"}, - "i30538":{"rank":"1"}, - "i30539":{"rank":"2"}, - "i30537":{"rank":"1"}, - "i30541":{"rank":"1"}, - "i30542":{"rank":"2"}, - "i30540":{"rank":"2"}, - "i30536":{"rank":"1"}, - "i30544":{"rank":"1"}, - "i30545":{"rank":"2"}, - "i30546":{"rank":"3"}, - "i30543":{"rank":"2"}, - "i30548":{"rank":"1"}, - "i30549":{"rank":"2"}, - "i30550":{"rank":"3"}, - "i30547":{"rank":"3"}, - "i30552":{"rank":"1"}, - "i30553":{"rank":"2"}, - "i30554":{"rank":"3"}, - "i30551":{"rank":"4"}, - "i30556":{"rank":"1"}, - "i30557":{"rank":"2"}, - "i30558":{"rank":"3"}, - "i30555":{"rank":"5"}, - "i30560":{"rank":"1"}, - "i30561":{"rank":"2"}, - "i30562":{"rank":"3"}, - "i30559":{"rank":"6"}, - "i30564":{"rank":"1"}, - "i30565":{"rank":"2"}, - "i30566":{"rank":"3"}, - "i30563":{"rank":"7"}, - "i30569":{"rank":"1"}, - "i30570":{"rank":"2"}, - "i30567":{"rank":"8"}, - "i30572":{"rank":"1"}, - "i30573":{"rank":"2"}, - "i30574":{"rank":"3"}, - "i30571":{"rank":"9"}, - "i30576":{"rank":"1"}, - "i30577":{"rank":"2"}, - "i30578":{"rank":"3"}, - "i30575":{"rank":"10"}, - "i30580":{"rank":"1"}, - "i30581":{"rank":"2"}, - "i30582":{"rank":"3"}, - "i30579":{"rank":"11"}, - "i30584":{"rank":"1"}, - "i30585":{"rank":"2"}, - "i30586":{"rank":"3"}, - "i30583":{"rank":"12"}, - "i30588":{"rank":"1"}, - "i30589":{"rank":"2"}, - "i30590":{"rank":"3"}, - "i30587":{"rank":"13"}, - "i30592":{"rank":"1"}, - "i30593":{"rank":"2"}, - "i30594":{"rank":"3"}, - "i30595":{"rank":"4"}, - "i30591":{"rank":"14"}, - "i30535":{"rank":"1"}, - "i30598":{"rank":"1"}, - "i30597":{"rank":"1"}, - "i30600":{"rank":"1"}, - "i30602":{"rank":"1"}, - "i30603":{"rank":"2"}, - "i30604":{"rank":"3"}, - "i30601":{"rank":"2"}, - "i30599":{"rank":"2"}, - "i30606":{"rank":"1"}, - "i30605":{"rank":"3"}, - "i30608":{"rank":"1"}, - "i30607":{"rank":"4"}, - "i30609":{"rank":"5"}, - "i30611":{"rank":"1"}, - "i30612":{"rank":"2"}, - "i30610":{"rank":"6"}, - "i30614":{"rank":"1"}, - "i30613":{"rank":"7"}, - "i30616":{"rank":"1"}, - "i30615":{"rank":"8"}, - "i30618":{"rank":"1"}, - "i30619":{"rank":"2"}, - "i30617":{"rank":"9"}, - "i30621":{"rank":"1"}, - "i30620":{"rank":"10"}, - "i30623":{"rank":"1"}, - "i30622":{"rank":"11"}, - "i30625":{"rank":"1"}, - "i30624":{"rank":"12"}, - "i30627":{"rank":"1"}, - "i30628":{"rank":"2"}, - "i30629":{"rank":"3"}, - "i30626":{"rank":"13"}, - "i30596":{"rank":"2"}, - "i30534":{"rank":"3"}, - "i30631":{"rank":"1"}, - "i30630":{"rank":"4"}, - "i30634":{"rank":"1"}, - "i30635":{"rank":"2"}, - "i30636":{"rank":"3"}, - "i30637":{"rank":"4"}, - "i30633":{"rank":"1"}, - "i30639":{"rank":"1"}, - "i30640":{"rank":"2"}, - "i30641":{"rank":"3"}, - "i30638":{"rank":"2"}, - "i30644":{"rank":"1"}, - "i30645":{"rank":"2"}, - "i30643":{"rank":"1"}, - "i30647":{"rank":"1"}, - "i30648":{"rank":"2"}, - "i30646":{"rank":"2"}, - "i30650":{"rank":"1"}, - "i30651":{"rank":"2"}, - "i30649":{"rank":"3"}, - "i30653":{"rank":"1"}, - "i30654":{"rank":"2"}, - "i30652":{"rank":"4"}, - "i30655":{"rank":"5"}, - "i30642":{"rank":"3"}, - "i30632":{"rank":"5"}, - "i30657":{"rank":"1"}, - "i30658":{"rank":"2"}, - "i30660":{"rank":"1"}, - "i30661":{"rank":"2"}, - "i30662":{"rank":"3"}, - "i30663":{"rank":"4"}, - "i30665":{"rank":"5"}, - "i30659":{"rank":"3"}, - "i30667":{"rank":"4"}, - "i30656":{"rank":"6"}, - "i30496":{"rank":"3"}, - "i30668":{"rank":"4"}, - "i30671":{"rank":"1"}, - "i30672":{"rank":"2"}, - "i30670":{"rank":"1"}, - "i30673":{"rank":"2"}, - "i30675":{"rank":"1"}, - "i30676":{"rank":"2"}, - "i30677":{"rank":"3"}, - "i30678":{"rank":"4"}, - "i30674":{"rank":"3"}, - "i30680":{"rank":"1"}, - "i30681":{"rank":"2"}, - "i30679":{"rank":"4"}, - "i30683":{"rank":"1"}, - "i30684":{"rank":"2"}, - "i30682":{"rank":"5"}, - "i30669":{"rank":"5"}, - "i30687":{"rank":"1"}, - "i30686":{"rank":"1"}, - "i30689":{"rank":"1"}, - "i30690":{"rank":"2"}, - "i30688":{"rank":"2"}, - "i30692":{"rank":"1"}, - "i30693":{"rank":"2"}, - "i30694":{"rank":"3"}, - "i30691":{"rank":"3"}, - "i30695":{"rank":"4"}, - "i30696":{"rank":"5"}, - "i30685":{"rank":"6"}, - "i30698":{"rank":"1"}, - "i30699":{"rank":"2"}, - "i30700":{"rank":"3"}, - "i30701":{"rank":"4"}, - "i30697":{"rank":"7"}, - "i30703":{"rank":"1"}, - "i30704":{"rank":"2"}, - "i30705":{"rank":"3"}, - "i30706":{"rank":"4"}, - "i30707":{"rank":"5"}, - "i30708":{"rank":"6"}, - "i30702":{"rank":"8"}, - "i30709":{"rank":"9"}, - "i30710":{"rank":"10"}, - "i30711":{"rank":"11"}, - "p648":{"rank":"16"}, - "i28618":{"rank":"1"}, - "i28619":{"rank":"2"}, - "i28620":{"rank":"3"}, - "i28621":{"rank":"4"}, - "i28622":{"rank":"5"}, - "i28623":{"rank":"6"}, - "i28624":{"rank":"7"}, - "i28625":{"rank":"8"}, - "i28626":{"rank":"9"}, - "i28627":{"rank":"10"}, - "i28628":{"rank":"11"}, - "i28629":{"rank":"12"}, - "i28630":{"rank":"13"}, - "i28631":{"rank":"14"}, - "i28632":{"rank":"15"}, - "i28633":{"rank":"16"}, - "i28634":{"rank":"17"}, - "i28635":{"rank":"18"}, - "i28636":{"rank":"19"}, - "i28637":{"rank":"20"}, - "i28638":{"rank":"21"}, - "i28639":{"rank":"22"}, - "i28640":{"rank":"23"}, - "i28641":{"rank":"24"}, - "i28642":{"rank":"25"}, - "i28643":{"rank":"26"}, - "i28644":{"rank":"27"}, - "i28617":{"rank":"1"}, - "i28616":{"rank":"1"}, - "i28645":{"rank":"2"}, - "i28649":{"rank":"1"}, - "i28650":{"rank":"2"}, - "i28651":{"rank":"3"}, - "i28648":{"rank":"1"}, - "i28653":{"rank":"1"}, - "i28654":{"rank":"2"}, - "i28655":{"rank":"3"}, - "i28652":{"rank":"2"}, - "i28657":{"rank":"1"}, - "i28658":{"rank":"2"}, - "i28659":{"rank":"3"}, - "i28656":{"rank":"3"}, - "i28661":{"rank":"1"}, - "i28662":{"rank":"2"}, - "i28663":{"rank":"3"}, - "i28660":{"rank":"4"}, - "i28647":{"rank":"1"}, - "i28666":{"rank":"1"}, - "i28667":{"rank":"2"}, - "i28668":{"rank":"3"}, - "i28665":{"rank":"1"}, - "i28670":{"rank":"1"}, - "i28671":{"rank":"2"}, - "i28672":{"rank":"3"}, - "i28669":{"rank":"2"}, - "i28674":{"rank":"1"}, - "i28675":{"rank":"2"}, - "i28676":{"rank":"3"}, - "i28677":{"rank":"4"}, - "i28673":{"rank":"3"}, - "i28680":{"rank":"1"}, - "i28681":{"rank":"2"}, - "i28682":{"rank":"3"}, - "i28679":{"rank":"1"}, - "i28683":{"rank":"2"}, - "i28678":{"rank":"4"}, - "i28664":{"rank":"2"}, - "i28688":{"rank":"1"}, - "i28689":{"rank":"2"}, - "i28687":{"rank":"1"}, - "i28691":{"rank":"1"}, - "i28692":{"rank":"2"}, - "i28690":{"rank":"2"}, - "i28686":{"rank":"1"}, - "i28694":{"rank":"1"}, - "i28695":{"rank":"2"}, - "i28696":{"rank":"3"}, - "i28693":{"rank":"2"}, - "i28698":{"rank":"1"}, - "i28699":{"rank":"2"}, - "i28700":{"rank":"3"}, - "i28697":{"rank":"3"}, - "i28702":{"rank":"1"}, - "i28703":{"rank":"2"}, - "i28704":{"rank":"3"}, - "i28701":{"rank":"4"}, - "i28706":{"rank":"1"}, - "i28707":{"rank":"2"}, - "i28708":{"rank":"3"}, - "i28705":{"rank":"5"}, - "i28710":{"rank":"1"}, - "i28711":{"rank":"2"}, - "i28712":{"rank":"3"}, - "i28709":{"rank":"6"}, - "i28714":{"rank":"1"}, - "i28715":{"rank":"2"}, - "i28716":{"rank":"3"}, - "i28713":{"rank":"7"}, - "i28719":{"rank":"1"}, - "i28720":{"rank":"2"}, - "i28717":{"rank":"8"}, - "i28722":{"rank":"1"}, - "i28723":{"rank":"2"}, - "i28724":{"rank":"3"}, - "i28721":{"rank":"9"}, - "i28726":{"rank":"1"}, - "i28727":{"rank":"2"}, - "i28728":{"rank":"3"}, - "i28725":{"rank":"10"}, - "i28730":{"rank":"1"}, - "i28731":{"rank":"2"}, - "i28732":{"rank":"3"}, - "i28729":{"rank":"11"}, - "i28734":{"rank":"1"}, - "i28735":{"rank":"2"}, - "i28736":{"rank":"3"}, - "i28733":{"rank":"12"}, - "i28738":{"rank":"1"}, - "i28739":{"rank":"2"}, - "i28740":{"rank":"3"}, - "i28737":{"rank":"13"}, - "i28742":{"rank":"1"}, - "i28743":{"rank":"2"}, - "i28744":{"rank":"3"}, - "i28745":{"rank":"4"}, - "i28741":{"rank":"14"}, - "i28685":{"rank":"1"}, - "i28748":{"rank":"1"}, - "i28747":{"rank":"1"}, - "i28750":{"rank":"1"}, - "i28752":{"rank":"1"}, - "i28753":{"rank":"2"}, - "i28754":{"rank":"3"}, - "i28751":{"rank":"2"}, - "i28749":{"rank":"2"}, - "i28756":{"rank":"1"}, - "i28755":{"rank":"3"}, - "i28758":{"rank":"1"}, - "i28757":{"rank":"4"}, - "i28759":{"rank":"5"}, - "i28761":{"rank":"1"}, - "i28762":{"rank":"2"}, - "i28760":{"rank":"6"}, - "i28764":{"rank":"1"}, - "i28763":{"rank":"7"}, - "i28766":{"rank":"1"}, - "i28765":{"rank":"8"}, - "i28768":{"rank":"1"}, - "i28769":{"rank":"2"}, - "i28767":{"rank":"9"}, - "i28771":{"rank":"1"}, - "i28770":{"rank":"10"}, - "i28773":{"rank":"1"}, - "i28772":{"rank":"11"}, - "i28775":{"rank":"1"}, - "i28774":{"rank":"12"}, - "i28777":{"rank":"1"}, - "i28778":{"rank":"2"}, - "i28779":{"rank":"3"}, - "i28776":{"rank":"13"}, - "i28746":{"rank":"2"}, - "i28684":{"rank":"3"}, - "i28781":{"rank":"1"}, - "i28780":{"rank":"4"}, - "i28784":{"rank":"1"}, - "i28785":{"rank":"2"}, - "i28786":{"rank":"3"}, - "i28787":{"rank":"4"}, - "i28783":{"rank":"1"}, - "i28789":{"rank":"1"}, - "i28790":{"rank":"2"}, - "i28791":{"rank":"3"}, - "i28788":{"rank":"2"}, - "i28794":{"rank":"1"}, - "i28795":{"rank":"2"}, - "i28793":{"rank":"1"}, - "i28797":{"rank":"1"}, - "i28798":{"rank":"2"}, - "i28796":{"rank":"2"}, - "i28800":{"rank":"1"}, - "i28801":{"rank":"2"}, - "i28799":{"rank":"3"}, - "i28803":{"rank":"1"}, - "i28804":{"rank":"2"}, - "i28802":{"rank":"4"}, - "i28805":{"rank":"5"}, - "i28792":{"rank":"3"}, - "i28782":{"rank":"5"}, - "i28807":{"rank":"1"}, - "i28808":{"rank":"2"}, - "i28810":{"rank":"1"}, - "i28812":{"rank":"2"}, - "i28813":{"rank":"3"}, - "i28815":{"rank":"4"}, - "i28809":{"rank":"3"}, - "i28817":{"rank":"4"}, - "i28806":{"rank":"6"}, - "i28646":{"rank":"3"}, - "i28818":{"rank":"4"}, - "i28821":{"rank":"1"}, - "i28822":{"rank":"2"}, - "i28820":{"rank":"1"}, - "i28823":{"rank":"2"}, - "i28825":{"rank":"1"}, - "i28826":{"rank":"2"}, - "i28827":{"rank":"3"}, - "i28828":{"rank":"4"}, - "i28824":{"rank":"3"}, - "i28830":{"rank":"1"}, - "i28831":{"rank":"2"}, - "i28829":{"rank":"4"}, - "i28833":{"rank":"1"}, - "i28834":{"rank":"2"}, - "i28832":{"rank":"5"}, - "i28819":{"rank":"5"}, - "i28837":{"rank":"1"}, - "i28836":{"rank":"1"}, - "i28839":{"rank":"1"}, - "i28840":{"rank":"2"}, - "i28838":{"rank":"2"}, - "i28842":{"rank":"1"}, - "i28843":{"rank":"2"}, - "i28844":{"rank":"3"}, - "i28841":{"rank":"3"}, - "i28845":{"rank":"4"}, - "i28846":{"rank":"5"}, - "i28835":{"rank":"6"}, - "i28848":{"rank":"1"}, - "i28849":{"rank":"2"}, - "i28850":{"rank":"3"}, - "i28851":{"rank":"4"}, - "i28847":{"rank":"7"}, - "i28853":{"rank":"1"}, - "i28854":{"rank":"2"}, - "i28855":{"rank":"3"}, - "i28856":{"rank":"4"}, - "i28857":{"rank":"5"}, - "i28858":{"rank":"6"}, - "i28852":{"rank":"8"}, - "i28859":{"rank":"9"}, - "i28860":{"rank":"10"}, - "i28861":{"rank":"11"}, - "p639":{"rank":"17"}, - "p129":{"rank":"-1"}, - "p610":{"rank":"1"}, - "i26848":{"rank":"1"}, - "i13767":{"rank":"1"}, - "i14909":{"rank":"2"}, - "i19995":{"rank":"1"}, - "i16921":{"rank":"3"}, - "i17047":{"rank":"4"}, - "i12332":{"rank":"1"}, - "i18444":{"rank":"2"}, - "i17048":{"rank":"5"}, - "i17052":{"rank":"6"}, - "p333":{"rank":"1"}, - "p325":{"rank":"2"}, - "i13491":{"rank":"1"}, - "i13534":{"rank":"2"}, - "i10233":{"rank":"1"}, - "i13489":{"rank":"1"}, - "i13535":{"rank":"2"}, - "i10234":{"rank":"2"}, - "i22063":{"rank":"1"}, - "i13487":{"rank":"1"}, - "i13537":{"rank":"2"}, - "i10235":{"rank":"3"}, - "i13485":{"rank":"1"}, - "i13538":{"rank":"2"}, - "i10236":{"rank":"4"}, - "i10232":{"rank":"1"}, - "i13473":{"rank":"1"}, - "i13539":{"rank":"2"}, - "i10248":{"rank":"1"}, - "i13475":{"rank":"1"}, - "i13540":{"rank":"2"}, - "i10249":{"rank":"2"}, - "i13477":{"rank":"1"}, - "i13541":{"rank":"2"}, - "i10250":{"rank":"3"}, - "i13479":{"rank":"1"}, - "i13542":{"rank":"2"}, - "i10251":{"rank":"4"}, - "i22064":{"rank":"1"}, - "i13481":{"rank":"1"}, - "i13543":{"rank":"2"}, - "i10252":{"rank":"5"}, - "i22065":{"rank":"1"}, - "i13483":{"rank":"1"}, - "i13544":{"rank":"2"}, - "i10253":{"rank":"6"}, - "i20749":{"rank":"1"}, - "i20751":{"rank":"2"}, - "i21083":{"rank":"3"}, - "i20767":{"rank":"7"}, - "i10237":{"rank":"2"}, - "i10231":{"rank":"1"}, - "i12674":{"rank":"1"}, - "i13515":{"rank":"2"}, - "i14388":{"rank":"3"}, - "i10247":{"rank":"1"}, - "i13549":{"rank":"1"}, - "i13550":{"rank":"2"}, - "i14389":{"rank":"3"}, - "i10398":{"rank":"2"}, - "i10238":{"rank":"2"}, - "i14456":{"rank":"3"}, - "i20810":{"rank":"1"}, - "i20811":{"rank":"2"}, - "i20812":{"rank":"3"}, - "i20813":{"rank":"4"}, - "i20816":{"rank":"5"}, - "i20817":{"rank":"6"}, - "i20808":{"rank":"4"}, - "i20824":{"rank":"1"}, - "i20825":{"rank":"2"}, - "i20826":{"rank":"3"}, - "i20827":{"rank":"4"}, - "i20828":{"rank":"5"}, - "i20829":{"rank":"6"}, - "i20818":{"rank":"5"}, - "i20830":{"rank":"6"}, - "i30029":{"rank":"7"}, - "i30033":{"rank":"8"}, - "p417":{"rank":"3"}, - "i29168":{"rank":"1"}, - "i29170":{"rank":"2"}, - "i17074":{"rank":"1"}, - "i17069":{"rank":"1"}, - "i17748":{"rank":"2"}, - "i17065":{"rank":"1"}, - "i28294":{"rank":"2"}, - "p511":{"rank":"4"}, - "i19700":{"rank":"1"}, - "i19701":{"rank":"2"}, - "i19709":{"rank":"1"}, - "i19710":{"rank":"2"}, - "i19711":{"rank":"3"}, - "i19712":{"rank":"4"}, - "i19713":{"rank":"5"}, - "i19714":{"rank":"6"}, - "i19708":{"rank":"1"}, - "i19704":{"rank":"3"}, - "i19716":{"rank":"1"}, - "i19715":{"rank":"1"}, - "i19705":{"rank":"4"}, - "i19918":{"rank":"5"}, - "i33498":{"rank":"6"}, - "i33513":{"rank":"7"}, - "i33514":{"rank":"8"}, - "i33515":{"rank":"9"}, - "i33517":{"rank":"1"}, - "i33518":{"rank":"2"}, - "i33520":{"rank":"3"}, - "i33516":{"rank":"10"}, - "p543":{"rank":"5"}, - "i24963":{"rank":"1"}, - "i23324":{"rank":"1"}, - "i25927":{"rank":"2"}, - "i30913":{"rank":"3"}, - "i16842":{"rank":"1"}, - "i19900":{"rank":"2"}, - "i25128":{"rank":"1"}, - "i30912":{"rank":"2"}, - "i23706":{"rank":"3"}, - "i24970":{"rank":"2"}, - "p507":{"rank":"6"}, - "i24460":{"rank":"1"}, - "i29080":{"rank":"1"}, - "i24461":{"rank":"2"}, - "p594":{"rank":"7"}, - "i29034":{"rank":"1"}, - "i29083":{"rank":"1"}, - "i29084":{"rank":"2"}, - "i29085":{"rank":"3"}, - "i29086":{"rank":"4"}, - "i29087":{"rank":"5"}, - "i29337":{"rank":"6"}, - "i29035":{"rank":"2"}, - "i31521":{"rank":"1"}, - "i31360":{"rank":"1"}, - "i31359":{"rank":"3"}, - "p643":{"rank":"8"}, - "p539":{"rank":"9"}, - "i28550":{"rank":"1"}, - "i28566":{"rank":"1"}, - "i28560":{"rank":"1"}, - "i28569":{"rank":"1"}, - "i28570":{"rank":"2"}, - "i28571":{"rank":"3"}, - "i31879":{"rank":"4"}, - "i28561":{"rank":"2"}, - "i28573":{"rank":"1"}, - "i28574":{"rank":"2"}, - "i28562":{"rank":"3"}, - "i28572":{"rank":"1"}, - "i28563":{"rank":"4"}, - "i28609":{"rank":"5"}, - "i28551":{"rank":"2"}, - "i31597":{"rank":"1"}, - "i31598":{"rank":"2"}, - "i31596":{"rank":"1"}, - "i31602":{"rank":"1"}, - "i31599":{"rank":"2"}, - "i31603":{"rank":"1"}, - "i31605":{"rank":"1"}, - "i31604":{"rank":"2"}, - "i31600":{"rank":"3"}, - "i31595":{"rank":"3"}, - "i31606":{"rank":"4"}, - "p637":{"rank":"10"}, - "i31037":{"rank":"1"}, - "i18050":{"rank":"1"}, - "i20648":{"rank":"1"}, - "i28536":{"rank":"1"}, - "i28537":{"rank":"2"}, - "i31316":{"rank":"3"}, - "i31378":{"rank":"4"}, - "i28534":{"rank":"2"}, - "i18051":{"rank":"2"}, - "i26156":{"rank":"3"}, - "p537":{"rank":"11"}, - "i23336":{"rank":"1"}, - "i29243":{"rank":"2"}, - "i29244":{"rank":"3"}, - "i29245":{"rank":"4"}, - "i29241":{"rank":"1"}, - "i31168":{"rank":"1"}, - "i28041":{"rank":"1"}, - "i25108":{"rank":"1"}, - "i25107":{"rank":"1"}, - "i25114":{"rank":"1"}, - "i25113":{"rank":"2"}, - "i23337":{"rank":"1"}, - "i29246":{"rank":"2"}, - "i29242":{"rank":"2"}, - "i32089":{"rank":"1"}, - "i32090":{"rank":"2"}, - "i31047":{"rank":"1"}, - "i31048":{"rank":"2"}, - "i31049":{"rank":"3"}, - "i29249":{"rank":"3"}, - "i29252":{"rank":"1"}, - "i29253":{"rank":"2"}, - "i29254":{"rank":"3"}, - "i29250":{"rank":"4"}, - "i29255":{"rank":"1"}, - "i29251":{"rank":"5"}, - "p541":{"rank":"12"}, - "p447":{"rank":"2"}, - "i32142":{"rank":"1"}, - "i32143":{"rank":"2"}, - "i32145":{"rank":"3"}, - "i32407":{"rank":"4"}, - "i32408":{"rank":"5"}, - "i32131":{"rank":"1"}, - "i32133":{"rank":"2"}, - "i32134":{"rank":"3"}, - "i32132":{"rank":"1"}, - "i32138":{"rank":"1"}, - "i32139":{"rank":"2"}, - "i32137":{"rank":"2"}, - "i32141":{"rank":"1"}, - "i32140":{"rank":"3"}, - "p680":{"rank":"1"}, - "i30382":{"rank":"1"}, - "i30384":{"rank":"2"}, - "i30385":{"rank":"3"}, - "i20396":{"rank":"1"}, - "i18111":{"rank":"1"}, - "i29964":{"rank":"1"}, - "i18115":{"rank":"2"}, - "i20388":{"rank":"1"}, - "i18118":{"rank":"3"}, - "i31534":{"rank":"1"}, - "i20390":{"rank":"1"}, - "i18119":{"rank":"4"}, - "i19866":{"rank":"5"}, - "i32413":{"rank":"1"}, - "i30270":{"rank":"6"}, - "i31174":{"rank":"1"}, - "i31177":{"rank":"2"}, - "i31173":{"rank":"1"}, - "i31172":{"rank":"7"}, - "i17971":{"rank":"1"}, - "p535":{"rank":"2"}, - "p681":{"rank":"3"}, - "i31238":{"rank":"1"}, - "i31239":{"rank":"2"}, - "i31240":{"rank":"3"}, - "i31237":{"rank":"1"}, - "i31232":{"rank":"1"}, - "i31234":{"rank":"2"}, - "i31158":{"rank":"1"}, - "i31159":{"rank":"2"}, - "i31236":{"rank":"3"}, - "i31241":{"rank":"2"}, - "i31945":{"rank":"1"}, - "i31242":{"rank":"3"}, - "i31243":{"rank":"4"}, - "i31872":{"rank":"1"}, - "i31873":{"rank":"2"}, - "i31874":{"rank":"3"}, - "i31871":{"rank":"1"}, - "i31519":{"rank":"1"}, - "i31875":{"rank":"2"}, - "i31515":{"rank":"5"}, - "i31195":{"rank":"1"}, - "i31209":{"rank":"1"}, - "i31210":{"rank":"2"}, - "i31196":{"rank":"2"}, - "i31211":{"rank":"1"}, - "i31197":{"rank":"3"}, - "p672":{"rank":"4"}, - "i31384":{"rank":"1"}, - "i32063":{"rank":"2"}, - "i31295":{"rank":"1"}, - "i31305":{"rank":"1"}, - "i31304":{"rank":"2"}, - "i31308":{"rank":"1"}, - "i31309":{"rank":"2"}, - "i31307":{"rank":"3"}, - "p676":{"rank":"5"}, - "i32388":{"rank":"1"}, - "i32389":{"rank":"2"}, - "i32390":{"rank":"3"}, - "i26346":{"rank":"1"}, - "p606":{"rank":"6"}, - "i33438":{"rank":"1"}, - "i33439":{"rank":"2"}, - "i33440":{"rank":"3"}, - "i33441":{"rank":"4"}, - "i33442":{"rank":"5"}, - "i33443":{"rank":"6"}, - "i33444":{"rank":"7"}, - "i33445":{"rank":"8"}, - "i33437":{"rank":"1"}, - "i33447":{"rank":"1"}, - "i33448":{"rank":"2"}, - "i33446":{"rank":"2"}, - "i33450":{"rank":"1"}, - "i33451":{"rank":"2"}, - "i33449":{"rank":"3"}, - "p719":{"rank":"7"}, - "i30279":{"rank":"1"}, - "i32363":{"rank":"2"}, - "i30280":{"rank":"1"}, - "i32362":{"rank":"1"}, - "i32365":{"rank":"2"}, - "i32366":{"rank":"3"}, - "i32434":{"rank":"4"}, - "i32435":{"rank":"5"}, - "i32436":{"rank":"6"}, - "i32364":{"rank":"2"}, - "i30278":{"rank":"1"}, - "i32368":{"rank":"2"}, - "i32369":{"rank":"3"}, - "i32367":{"rank":"3"}, - "i30274":{"rank":"1"}, - "i23760":{"rank":"1"}, - "i30036":{"rank":"1"}, - "i30037":{"rank":"2"}, - "i30034":{"rank":"2"}, - "i30038":{"rank":"1"}, - "i30039":{"rank":"2"}, - "i30040":{"rank":"3"}, - "i30041":{"rank":"4"}, - "i30042":{"rank":"5"}, - "i30035":{"rank":"3"}, - "p591":{"rank":"8"}, - "i32158":{"rank":"1"}, - "i32335":{"rank":"2"}, - "i32336":{"rank":"3"}, - "i32337":{"rank":"4"}, - "i32338":{"rank":"5"}, - "p701":{"rank":"1"}, - "i31709":{"rank":"1"}, - "i31711":{"rank":"2"}, - "i31695":{"rank":"1"}, - "i31707":{"rank":"1"}, - "i31708":{"rank":"2"}, - "i31696":{"rank":"2"}, - "i31697":{"rank":"1"}, - "i31688":{"rank":"1"}, - "i31700":{"rank":"1"}, - "i31702":{"rank":"2"}, - "i31703":{"rank":"3"}, - "i32121":{"rank":"4"}, - "i32157":{"rank":"5"}, - "i31699":{"rank":"1"}, - "i31704":{"rank":"1"}, - "i31705":{"rank":"2"}, - "i31706":{"rank":"3"}, - "i31701":{"rank":"2"}, - "i31689":{"rank":"2"}, - "i31736":{"rank":"1"}, - "i31737":{"rank":"2"}, - "i31738":{"rank":"3"}, - "i31739":{"rank":"4"}, - "i31740":{"rank":"5"}, - "i31718":{"rank":"1"}, - "i31741":{"rank":"1"}, - "i31742":{"rank":"2"}, - "i31719":{"rank":"2"}, - "i31690":{"rank":"3"}, - "i31713":{"rank":"1"}, - "i31714":{"rank":"2"}, - "i31715":{"rank":"3"}, - "i31716":{"rank":"4"}, - "i31717":{"rank":"5"}, - "i31712":{"rank":"1"}, - "i31691":{"rank":"4"}, - "i31721":{"rank":"1"}, - "i31722":{"rank":"2"}, - "i31723":{"rank":"3"}, - "i31724":{"rank":"4"}, - "i31725":{"rank":"5"}, - "i31726":{"rank":"6"}, - "i31720":{"rank":"1"}, - "i31692":{"rank":"5"}, - "i31728":{"rank":"1"}, - "i31729":{"rank":"2"}, - "i31730":{"rank":"3"}, - "i31727":{"rank":"1"}, - "i31733":{"rank":"1"}, - "i31734":{"rank":"2"}, - "i31735":{"rank":"3"}, - "i31731":{"rank":"2"}, - "i31693":{"rank":"6"}, - "i31744":{"rank":"1"}, - "i31745":{"rank":"2"}, - "i31746":{"rank":"3"}, - "i31747":{"rank":"4"}, - "i31743":{"rank":"1"}, - "i31694":{"rank":"7"}, - "i31698":{"rank":"2"}, - "p677":{"rank":"9"}, - "i30786":{"rank":"1"}, - "i30787":{"rank":"2"}, - "i30776":{"rank":"1"}, - "i30791":{"rank":"1"}, - "i30777":{"rank":"2"}, - "i30770":{"rank":"1"}, - "i30792":{"rank":"1"}, - "i30848":{"rank":"2"}, - "i30779":{"rank":"3"}, - "i30771":{"rank":"1"}, - "i30805":{"rank":"2"}, - "i30845":{"rank":"3"}, - "i30780":{"rank":"4"}, - "i30806":{"rank":"1"}, - "i30849":{"rank":"2"}, - "i30781":{"rank":"5"}, - "i30807":{"rank":"1"}, - "i30846":{"rank":"2"}, - "i30782":{"rank":"6"}, - "i30808":{"rank":"1"}, - "i30850":{"rank":"2"}, - "i30783":{"rank":"7"}, - "i30809":{"rank":"1"}, - "i30847":{"rank":"2"}, - "i30784":{"rank":"8"}, - "i30810":{"rank":"1"}, - "i30851":{"rank":"2"}, - "i30785":{"rank":"9"}, - "p661":{"rank":"10"}, - "p550":{"rank":"3"}, - "i31915":{"rank":"1"}, - "i31916":{"rank":"2"}, - "i31918":{"rank":"1"}, - "i31924":{"rank":"2"}, - "i31925":{"rank":"3"}, - "i31917":{"rank":"3"}, - "i31920":{"rank":"1"}, - "i31921":{"rank":"2"}, - "i31929":{"rank":"3"}, - "i31930":{"rank":"4"}, - "i31919":{"rank":"4"}, - "i31923":{"rank":"5"}, - "i31934":{"rank":"1"}, - "i31935":{"rank":"2"}, - "i31936":{"rank":"3"}, - "i31937":{"rank":"4"}, - "i31938":{"rank":"5"}, - "i31927":{"rank":"1"}, - "i31926":{"rank":"6"}, - "i31931":{"rank":"7"}, - "i31993":{"rank":"1"}, - "i31932":{"rank":"8"}, - "i31940":{"rank":"1"}, - "i31941":{"rank":"2"}, - "i31942":{"rank":"3"}, - "i31939":{"rank":"9"}, - "i31943":{"rank":"10"}, - "i32013":{"rank":"11"}, - "p683":{"rank":"1"}, - "i31996":{"rank":"1"}, - "i31999":{"rank":"1"}, - "i32000":{"rank":"2"}, - "i32001":{"rank":"3"}, - "i31998":{"rank":"2"}, - "i32002":{"rank":"3"}, - "i32081":{"rank":"1"}, - "i32004":{"rank":"4"}, - "i32082":{"rank":"1"}, - "i32005":{"rank":"5"}, - "i32009":{"rank":"6"}, - "i32012":{"rank":"7"}, - "i32014":{"rank":"8"}, - "p686":{"rank":"2"}, - "i31776":{"rank":"1"}, - "i31777":{"rank":"2"}, - "i31778":{"rank":"3"}, - "i31779":{"rank":"4"}, - "i31780":{"rank":"5"}, - "i31781":{"rank":"6"}, - "i31782":{"rank":"7"}, - "i31783":{"rank":"8"}, - "i31787":{"rank":"1"}, - "i31765":{"rank":"9"}, - "p679":{"rank":"3"}, - "p678":{"rank":"4"}, - "i32496":{"rank":"1"}, - "i32497":{"rank":"2"}, - "i32498":{"rank":"3"}, - "i32585":{"rank":"4"}, - "i32495":{"rank":"1"}, - "p705":{"rank":"1"}, - "i28203":{"rank":"1"}, - "i28204":{"rank":"2"}, - "i32500":{"rank":"1"}, - "i32501":{"rank":"2"}, - "i32502":{"rank":"3"}, - "i32499":{"rank":"3"}, - "i28193":{"rank":"1"}, - "i28205":{"rank":"1"}, - "i28206":{"rank":"2"}, - "i28194":{"rank":"2"}, - "i28207":{"rank":"1"}, - "i28195":{"rank":"3"}, - "p540":{"rank":"2"}, - "i29326":{"rank":"1"}, - "i31224":{"rank":"2"}, - "i27140":{"rank":"1"}, - "i25115":{"rank":"1"}, - "i27816":{"rank":"1"}, - "i28970":{"rank":"2"}, - "i27815":{"rank":"2"}, - "i25200":{"rank":"1"}, - "i31550":{"rank":"1"}, - "i31549":{"rank":"1"}, - "i25201":{"rank":"2"}, - "i31217":{"rank":"1"}, - "i31218":{"rank":"2"}, - "i32020":{"rank":"3"}, - "i32061":{"rank":"4"}, - "i31215":{"rank":"3"}, - "p596":{"rank":"3"}, - "p714":{"rank":"5"}, - "p641":{"rank":"6"}, - "i27947":{"rank":"1"}, - "i30327":{"rank":"1"}, - "i28293":{"rank":"2"}, - "i31188":{"rank":"1"}, - "i31189":{"rank":"2"}, - "i31191":{"rank":"3"}, - "i31187":{"rank":"3"}, - "i27411":{"rank":"1"}, - "p631":{"rank":"1"}, - "i32028":{"rank":"1"}, - "i32030":{"rank":"2"}, - "i32031":{"rank":"3"}, - "i32032":{"rank":"4"}, - "i32033":{"rank":"5"}, - "i32060":{"rank":"6"}, - "i32029":{"rank":"1"}, - "i32035":{"rank":"1"}, - "i32036":{"rank":"2"}, - "i32037":{"rank":"3"}, - "i32038":{"rank":"4"}, - "i32034":{"rank":"2"}, - "i32041":{"rank":"1"}, - "i32040":{"rank":"1"}, - "i32039":{"rank":"3"}, - "i32043":{"rank":"1"}, - "i32044":{"rank":"2"}, - "i32045":{"rank":"3"}, - "i32058":{"rank":"4"}, - "i32042":{"rank":"4"}, - "i32047":{"rank":"1"}, - "i32048":{"rank":"2"}, - "i32046":{"rank":"5"}, - "i32049":{"rank":"6"}, - "i32051":{"rank":"7"}, - "i32052":{"rank":"8"}, - "i32054":{"rank":"1"}, - "i32055":{"rank":"2"}, - "i32053":{"rank":"9"}, - "i32056":{"rank":"10"}, - "i32330":{"rank":"1"}, - "i32508":{"rank":"2"}, - "i32329":{"rank":"11"}, - "i32411":{"rank":"1"}, - "i32410":{"rank":"12"}, - "i32027":{"rank":"1"}, - "i32400":{"rank":"1"}, - "i32401":{"rank":"2"}, - "i32402":{"rank":"3"}, - "i32403":{"rank":"4"}, - "i32397":{"rank":"2"}, - "p690":{"rank":"2"}, - "i11522":{"rank":"1"}, - "i11520":{"rank":"1"}, - "i11576":{"rank":"2"}, - "i11600":{"rank":"3"}, - "i12263":{"rank":"4"}, - "i18134":{"rank":"5"}, - "i11599":{"rank":"2"}, - "i14056":{"rank":"3"}, - "i28190":{"rank":"1"}, - "i8033":{"rank":"1"}, - "p376":{"rank":"3"}, - "i23838":{"rank":"1"}, - "i23839":{"rank":"2"}, - "i23840":{"rank":"3"}, - "i23837":{"rank":"1"}, - "i29166":{"rank":"2"}, - "i23755":{"rank":"1"}, - "p589":{"rank":"4"}, - "p122":{"rank":"7"}, - "i9120":{"rank":"8"}, - "i26702":{"rank":"1"}, - "i22742":{"rank":"9"}}; - var encoded = easyTests.ysyInstance.storage.extra._encodeLayout(layout); - var decoded = easyTests.ysyInstance.storage.extra._decodeLayout(encoded); - var keys = Object.getOwnPropertyNames(layout); - for(var i=0;i<keys.length;i++){ - var key = keys[i]; - if (!_.isEqual(layout[key], decoded[key])) { - console.log(key); - console.log(layout[key]); - console.log(decoded[key]); - throw "different"; - } - } - }, - encodeSmallLayout: function () { - var layout = {"p25":{},"i2280": {"rank": 1}, "i3027": {"rank": 5, "position": [808.5, 111.25, 1]}}; - var encoded = easyTests.ysyInstance.storage.extra._encodeLayout(layout); - var decoded = easyTests.ysyInstance.storage.extra._decodeLayout(encoded); - if (!_.isEqual(layout, decoded)) { - console.log(JSON.stringify(layout)); - console.log(JSON.stringify(decoded)); - throw "different"; - } - } -}); \ No newline at end of file diff --git a/plugins/easy_wbs/assets/javascripts/wbs_main.js b/plugins/easy_wbs/assets/javascripts/wbs_main.js index a531949..545e533 100644 --- a/plugins/easy_wbs/assets/javascripts/wbs_main.js +++ b/plugins/easy_wbs/assets/javascripts/wbs_main.js @@ -15,9 +15,7 @@ this.patch(); this.init(); // this.settings.noSave = true; - if (window.easyTests) { - easyTests.ysyInstance = this; - } + easyTests.ysyInstance = this; } classes.extendClass(WbsMain, classes.MindMup); diff --git a/plugins/easy_wbs/assets/javascripts/wbs_modals.js b/plugins/easy_wbs/assets/javascripts/wbs_modals.js index 9be08d4..74a694c 100644 --- a/plugins/easy_wbs/assets/javascripts/wbs_modals.js +++ b/plugins/easy_wbs/assets/javascripts/wbs_modals.js @@ -41,7 +41,7 @@ sendPack.updateNodeData(); var startsWith = this.ysy.util.startsWith; var preFill = _.omit(sendPack.node.attr.data, function (value, key) { - return _.isFunction(value) || startsWith(key, "_") || key === "journals"; + return _.isFunction(value) || startsWith(key, "_"); }); if(preFill.custom_fields){ var customValues={}; diff --git a/plugins/easy_wbs/assets/stylesheets/generated/easy_wbs.css b/plugins/easy_wbs/assets/stylesheets/generated/easy_wbs.css deleted file mode 100644 index 2217480..0000000 --- a/plugins/easy_wbs/assets/stylesheets/generated/easy_wbs.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";#logo-img,.logo{background-image:url(//d1g6a398qq2djm.cloudfront.net/img/logo_32.png);height:40px}#floating-toolbar,.ideaInput{z-index:999;position:absolute}#gplus-share,#logo-img,.logo{background-position:left center;background-repeat:no-repeat}#gplus-share,#toolbarSocial,.menulink{margin-right:20px}#modalDownload .modal-body,.center,.ideaInput{text-align:center}#splittable,.ios-wkwebview{-webkit-tap-highlight-color:transparent}body,html{height:100%}#container,#floating-toolbar,#topbar,.dropdown-menu,.floating{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.logo{width:0;padding-left:40px;padding-top:5px;padding-bottom:5px}#logo-img{width:40px;padding:0;margin-left:50px}@media (max-width:979px){.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;margin-bottom:0}}@media (max-width:767px){.navbar{padding-left:20px;padding-right:20px}body{padding:0}}.span-toolbar{width:120px;cursor:move}.dropdown-menu li a,.share{cursor:pointer}@media (max-width:1024px){.hidden-tablet-landscape{display:none!important}}@media (max-width:768px){.hidden-tablet-portrait{display:none!important}}@media (max-width:320px){.hidden-phone-portrait{display:none!important}}@media (max-height:320px){.form-horizontal .control-group{margin-bottom:5px}.hidden-phone-landscape{display:none}}@media (max-width:400px){.map-changed .changed-phone-hidden{display:none!important}}@media (max-width:1024px){.map-changed .changed-tablet-hidden{display:none!important}}@media screen{#topbar .nav>li>a{font-weight:700}}#floating-toolbar{top:100px;right:10px;border:1px solid #08c;border-radius:9px}.android .visible-touch,.ios .visible-touch{display:block!important}.android .hidden-touch,.ios .hidden-touch,.map-changed .hidden-map-changed,.map-unchanged .visible-map-changed{display:none!important}div.topbar-color-picker{width:127px}.topbar-color-picker .colorPicker-swatch{width:15px;height:15px}.android .colorPicker-swatch,.ios .colorPicker-swatch{width:30px;height:30px}.android .colorPicker-palette,.ios .colorPicker-palette{width:217px;top:55px;left:5px}#toolbarEdit .btn-group{margin-left:0}#toolbarEdit button{margin-top:5px;width:40px;height:32px}#toolbarEdit button.two{width:80px}#toolbarEdit button.three{width:120px}.ideaInput{background-color:#5FBF5F;color:#FFF;font-family:Helvetica,"Arial Unicode MS",sans-serif;font-weight:700;font-size:13px;line-height:13px;padding:0}.ideaInput:focus{outline:0}.menulink{margin-top:10px}#gplus-share{border:0;height:16px;background-image:url(//ssl.gstatic.com/images/icons/gplus-16.png);margin-left:6px;margin-top:2px;padding-right:0}.btn-inline,a.repo{margin-left:5px}#modalDownload .modal-body p{text-align:left}#modalDownload img{max-height:300px;max-width:450px}.share{width:32px;height:32px;display:inline-block;margin:5px}.btn-inline{border:0;background:0 0;margin-right:5px}.colorPicker_hexWrap{display:none}div.colorPicker-picker{display:inline;float:left;background:0 0}.repo,.repo-a{background-image:url(//d1g6a398qq2djm.cloudfront.net/img/logo_16.png)}#modalImport input[type=file]{opacity:0;position:absolute;width:0;height:0}.repo{background-repeat:no-repeat;background-position:left center}.repo-b{background-image:url(//d1g6a398qq2djm.cloudfront.net/img/logo_gold_16.png)}.repo-p{background-image:url(//d1g6a398qq2djm.cloudfront.net/img/logo_gold_private_16.png)}.repo-g{background-image:url(//d1g6a398qq2djm.cloudfront.net/img/google_drive.png)}.repo-menuitem{background-position:14px center;padding-left:35px!important}span.repo-a,span.repo-b,span.repo-g,span.repo-p{padding-left:16px}.visible-collapsed-toolbar{display:none}.collapsed-toolbar .visible-collapsed-toolbar{display:block!important}.collapsed-toolbar span.visible-collapsed-toolbar{display:inline!important}.collapsed-toolbar .hidden-collapsed-toolbar{display:none!important}.visible-row-split{display:none}.row-split .visible-row-split{display:block!important}.row-split span.visible-row-split{display:inline!important}.row-split .hidden-row-split{display:none!important}.visible-column-split{display:none}.column-split .visible-column-split{display:block!important}.column-split span.visible-column-split{display:inline!important}.column-split .hidden-column-split{display:none!important}#listBookmarks a .btn-inline{margin-left:15px}.gecko #listBookmarks a .btn-inline{position:relative;top:-17px;left:15px}.hidden-topbar #topbar .navbar-inner{visibility:hidden}.column-split.hidden-topbar #topbar .navbar-inner,.hidden-topbar #topbar:hover .navbar-inner{visibility:visible}.topbar-autohide{display:inline}.hidden-topbar .topbar-autohide,span.topbar-fixed{display:none}.hidden-topbar span.topbar-fixed{display:inline}body.ipad{margin:1px}body.ios-wkwebview{margin:0;padding:0;width:100%}.image-render-visible{display:none}.image-render .image-render-visible{display:block}.image-render .image-render-hidden,.offline-disabled-hidden{display:none}body.offline-enabled .offline-disabled-hidden{display:block}#attachEditArea{overflow:scroll;outline:0;border:1px solid #ccc;padding:4px;margin:10px;box-sizing:content-box;-webkit-box-shadow:rgba(0,0,0,.0745098) 0 1px 1px 0 inset;box-shadow:rgba(0,0,0,.0745098) 0 1px 1px 0 inset;border-radius:3px;background-color:#fff;display:block;clear:both}.alert-error input,input.noglow{outline:0;box-shadow:none!important;cursor:pointer}#modalAttachmentEditor .editor-topbar{padding-right:10px;padding-left:10px;display:none}#modalAttachmentEditor.mm-editable .editor-topbar{display:block}#modalAttachmentEditor .viewer-topbar{margin-top:5px;margin-bottom:5px;padding-right:10px;padding-left:10px;height:30px}#modalAttachmentEditor.mm-editable .viewer-topbar{display:none}#modalAttachmentEditor button.close{margin-left:5px}#modalAttachmentEditor .editor-topbar>.btn-toolbar{display:inline-block;background-color:#eee;width:100%}#modalAttachmentEditor{position:absolute;height:80%;width:80%;left:10%;top:10%;padding:5px;z-index:9999;background-color:#eee;-moz-border-radius:6px;border-radius:6px;margin:0}@media (max-width:1200px){#modalAttachmentEditor{width:90%;left:5%;top:5%}}#modalAttachmentEditor .dropdown-menu .btn-toolbar{display:block}.show-active,.visible-map-source-a,.visible-map-source-b,.visible-map-source-g,.visible-map-source-n,.visible-map-source-p{display:none}#modalAttachmentEditor .dropdown-menu .btn-toolbar .btn-group{margin-top:5px;margin-left:5px;margin-right:5px}.android #modalAttachmentEditor .btn-toolbar,.ios #modalAttachmentEditor .btn-toolbar{margin-top:5px;margin-bottom:5px;padding-bottom:10px}.android #modalAttachmentEditor,.ios #modalAttachmentEditor{top:0;height:60%}@media (max-height:768px){.android #modalAttachmentEditor,.ios #modalAttachmentEditor{height:37%}}.non-group .btn{margin-left:5px}#linkEditWidget{display:none;z-index:999;position:absolute;padding:10px}.alert a{text-decoration:underline;cursor:pointer}.alert a.btn{text-decoration:none}input.noglow{border:none!important}.map-source-a .visible-map-source-a,.map-source-b .visible-map-source-b,.map-source-g .visible-map-source-g,.map-source-n .visible-map-source-n,.map-source-p .visible-map-source-p{display:inherit}@media (max-width:1200px){.hidden-narrow-screen{display:none}}@media (max-width:1600px){#optionalPane .hidden-narrow-screen{display:none}}.btn-xlarge{padding:18px 28px;font-size:40px;line-height:normal;-webkit-border-radius:8px;-moz-border-radius:8px;border-radius:8px;width:250px;height:180px;margin-left:10px;margin-bottom:10px}@media (max-height:320px){.btn-xlarge{padding:9px 14px;font-size:20px;line-height:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;width:230px;height:70px;margin-left:5px;margin-bottom:5px}}@media (max-width:320px){.btn-xlarge{padding:9px 14px;font-size:20px;line-height:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;width:230px;height:70px;margin-left:5px;margin-bottom:5px}#mainMenu>li>a{padding-left:0;padding-right:15px}#logo-img{margin:0;padding-left:5px;padding-right:0}}@media (min-height:640px){#modalKeyActions .modal-body{min-height:400px}}#modalKeyActions .item a span{padding-right:10px}#modalKeyActions a{cursor:pointer;color:#000}#modalKeyActions div.item>a{text-decoration:underline}.social{display:inline-block!important;padding-left:2px!important;padding-right:2px!important;margin-right:2px!important;cursor:pointer}#modalGoldLicense textarea{width:95%;height:200px}#modalIconEdit .file-drop-zone{width:150px;height:150px;border:1px dashed #000;color:gray;font-size:7px;text-align:center}#modalIconEdit .file-drop-zone img{max-width:150px;max-height:150px}#modalIconEdit{z-index:9999}.btn .export{max-width:24px}.btn .landscape{transform:rotate(90deg);-moz-transform:rotate(90deg);-webkit-transform:rotate(90deg)}#modalPDFExport form{margin-top:20px}.alert-error input{background:0 0;margin:0 0 2px;color:#b94a48;border:none!important;width:100%}body .modal.huge{width:90%;height:80%;left:5%;margin-left:auto;margin-right:auto}#measuresSheet table{background-color:#fff;border:1px solid #d4d4d4}#measuresSheet td{text-align:right;width:20%}#measuresSheet th+th{text-align:right}#measuresSheet table thead tr th{border-bottom:3px gray solid!important}#measuresSheet table tfoot tr th{border-top:3px gray solid!important}#measuresSheet table tbody th{width:30%}#measuresSheet form{margin:0!important}#measuresSheet td:focus{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;outline:#5b9dd9 auto}.measures-editor.error{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px red!important;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px red!important;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px red!important;outline:red auto thin!important}#measuresSheet .modal-footer a{margin-right:20px;margin-left:20px;line-height:30px}#optionalPane .input-append.navbar-form button{margin-top:5px!important}#measuresSheet{margin:5px;border-left:1px;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.table-container{margin-right:5px}.column-split .table-container{margin-bottom:45px}.mm-active .show-active{display:initial}.black{color:#000!important}.activated-scene{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #7ab5d3;outline:#5b9dd9 auto}#storyboard{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.storyboard-container{padding-left:10px;padding-right:10px}.storyboard-scene{display:inline-block;max-height:120px;max-width:160px;min-height:120px;min-width:160px;overflow:hidden;background-color:#fff;border:1px solid silver;border-radius:4px;color:#000;margin:10px;text-align:center;cursor:move}.ios-icon-label,.ios-modal-title{color:#22AAE0;line-height:1.2;font-family:Helvetica,"Arial Unicode MS",sans-serif;text-align:center}.storyboard-scene-title{display:table-cell;vertical-align:middle;white-space:pre-wrap;font-weight:700}.drag-shadow{opacity:.5;box-shadow:none;outline:0;cursor:move;z-index:10;position:relative}.collaborator-list-container td,.mapjs-node{cursor:pointer}.potential-drop-left{margin-left:0;margin-right:20px}.potential-drop-right{margin-left:20px;margin-right:0}#splittable{width:100%;height:calc(100% - 41px);margin-top:41px}.hidden-topbar:not(.column-split)>#splittable{height:100%;margin-top:0}.ios-wkwebview>#splittable{width:100%;height:calc(100%);margin-top:0}.splittable-optional .navbar{width:100%;margin-bottom:0}.splittable-optional .content{overflow:scroll;height:calc(100% - 41px)}.ios-wkwebview .splittable-optional .content{overflow:scroll;height:100%}.splittable-optional{display:none;outline:#d4d4d4 solid 1px}.column-split .splittable-primary{float:left;height:100%;overflow:scroll;width:50%}.column-split .splittable-optional{float:right;height:100%;width:50%;display:block}.splittable-primary{outline:#d4d4d4 solid 1px;height:100%;width:100%}.row-split .splittable-primary{height:50%;overflow:scroll;width:100%}.row-split .splittable-optional{height:50%;width:100%;display:block}#optionalPane{background-color:#f5f5f5;-webkit-box-shadow:0 1px 10px rgba(0,0,0,.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,.1);box-shadow:0 1px 10px rgba(0,0,0,.1)}.splittable-optional .content-container{height:100%}.pull-middle{display:table;height:100%}.pull-middle>div{display:table-cell;vertical-align:middle;padding-left:20%;padding-right:20%}.ios-menu-toggle-container{position:absolute;height:90px;width:75px;top:calc(100% - 90px);left:0;overflow:hidden;z-index:999}.ios-corner-icon-container{position:absolute;height:100px;width:75px;top:5;left:-11px;border:1px solid #22AAE0;background-color:#FFF;border-radius:5px;box-shadow:2px 2px 1px rgba(104,104,104,.8)}.ios-modal,.ios-toolbar{background-color:rgba(255,255,255,.85)}.ios-menu-toggle{position:absolute;top:5;left:12;height:73px;width:65px}.ios-toolbar{position:absolute;box-shadow:0 -1px 0 #22AAE0;top:calc(100% - 80px);left:65px;width:calc(100% - 65px);height:80px;z-index:998;overflow:scroll}.ios-icon-label{position:absolute;font-size:8pt;font-weight:400;top:calc(100% - 22px);width:50px;left:5px;height:22px}.ios-toolbar-item{display:inline-block;position:relative;top:0;height:75px;width:60px}.ios-toolbar-icon{position:absolute;left:10px;top:8px;height:40px;width:40px}.ios-modal{z-index:1090;position:absolute;overflow:hidden;left:0;top:0;height:100%;width:100%}.ios-modal-close{position:absolute;top:20;left:calc(100% - 45px);height:40px;width:40px}.ios-modal-title{position:absolute;left:45px;top:30px;height:40px;width:calc(100% - 90px);font-size:12pt;font-weight:700}.ios-modal-content{z-index:1095;position:absolute;top:80px;height:calc(100% - 75px);overflow-x:hidden;overflow-y:auto;background-color:#FFF;border:1px solid #22AAE0;border-radius:5px;box-shadow:2px 2px 1px rgba(104,104,104,.8)}.ios-color-selector{display:inline-block;box-shadow:1px 1px 3px rgba(104,104,104,.7)}.ios-color-palette{display:block;text-align:center}@media (min-width:601px){.ios-modal-content{left:calc(50% - 300px);width:600px}.ios-color-palette{margin:20px 50px}.ios-color-selector{margin:20px;height:80px;width:80px;border-radius:40px}}@media (max-width:600px){.ios-modal-content{left:calc(50% - 150px);width:300px}.ios-color-palette{margin:10px 20px}.ios-color-selector{margin:10px;height:40px;width:40px;border-radius:20px}}.collaborator-list-container{max-height:200px;overflow-y:scroll;padding-top:10px}.mm-icon-gdrive,.mm-icon-gmail{padding:15px 9px;background-position:center center;background-repeat:no-repeat}.collaborator-list-container table{margin-bottom:0}.mm-collaborator{position:absolute;width:32px;height:32px;border:3px solid transparent;border-radius:20px;transform:translate(5px,5px);min-width:30px}.collab-name{overflow-x:hidden;max-width:50px;white-space:nowrap}.collab-name a{line-height:30px}.collab-photo{width:30px}.collab-photo img{width:30px;height:30px;float:left;margin-right:10px;border:2px solid transparent;border-radius:20px}#floating-collaborators{position:absolute;top:100px;left:10px;z-index:999;border:1px solid #08c;border-radius:9px}.visible-collaboration-toolbar{display:none}.map-source-c.collaboration-toolbar .visible-collaboration-toolbar{display:initial!important}.collaboration-toolbar .hidden-collaboration-toolbar{display:none!important}.mm-has-collaborators .hidden-has-collaborators,.visible-has-collaborators{display:none}.mm-has-collaborators .visible-has-collaborators{display:block}.visible-collaboration-mute-speech{display:none}.collaboration-mute-speech .visible-collaboration-mute-speech{display:initial!important}.collaboration-mute-speech .hidden-collaboration-mute-speech{display:none!important}.btn-share{margin-left:10px;margin-right:10px}.mm-icon-gmail{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgNDU1NTUgMzMwOTciPg0KIDxnPg0KICA8cGF0aCBmaWxsPSIjRTc1QTREIiBkPSJNNDE2MDggMjUwYzIwMzAsMCAzNjkxLDE2NjEgMzY5MSwzNjkxbDAgMTA3MCAtNTEyOCAzNjYzIC0xNzQ1OCAxMjAyNiAtMTc0NTggLTEyMTQ4IDAgMjQyOTUgLTEzMTQgMGMtMjAzMCwwIC0zNjkxLC0xNjYxIC0zNjkxLC0zNjkxbDAgLTI1MjE0YzAsLTIwMzAgMTY2MSwtMzY5MSAzNjkxLC0zNjkxbDE4NzcyIDEzOTE4IDE4ODk0IC0xMzkxOHoiLz4NCiAgPHBvbHlnb24gZmlsbD0iI0U3RTRENyIgcG9pbnRzPSI1MjU1LDg1NTIgNTI1NSwzMjg0NyA0MDE3MiwzMjg0NyA0MDE3Miw4Njc0IDIyNzEzLDIwNzAwICIvPg0KICA8cG9seWdvbiBmaWxsPSIjQjhCN0FFIiBwb2ludHM9IjUyNTUsMzI4NDcgMjI3MTMsMjA3MDAgMjI2MzQsMjA2NDQgNTI1NSwzMjU4MCAiLz4NCiAgPHBvbHlnb24gZmlsbD0iI0I3QjZBRCIgcG9pbnRzPSI0MDE3Miw4Njc0IDQwMTgyLDMyODQ3IDIyNzEzLDIwNzAwICIvPg0KICA8cGF0aCBmaWxsPSIjQjIzOTJGIiBkPSJNNDUyOTkgNTAxMWw2IDI0MTQ4Yy04OCwyNjg0IC0xMjUxLDM2NDIgLTUxMjMsMzY4N2wtMTAgLTI0MTczIDUxMjggLTM2NjN6Ii8+DQogIDxwb2x5Z29uIGZpbGw9IiNGN0Y1RUQiIHBvaW50cz0iMzk0MSwyNTAgMjI3MTMsMTQxNjggNDE2MDgsMjUwICIvPg0KIDwvZz4NCjwvc3ZnPg0K)}.mm-icon-gdrive{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiB3aWR0aD0iNTA5LjA4bW0iIGhlaWdodD0iNDQwLjk3bW0iIHZlcnNpb249IjEuMSIgc3R5bGU9InNoYXBlLXJlbmRlcmluZzpnZW9tZXRyaWNQcmVjaXNpb247IHRleHQtcmVuZGVyaW5nOmdlb21ldHJpY1ByZWNpc2lvbjsgaW1hZ2UtcmVuZGVyaW5nOm9wdGltaXplUXVhbGl0eTsgZmlsbC1ydWxlOmV2ZW5vZGQ7IGNsaXAtcnVsZTpldmVub2RkIg0Kdmlld0JveD0iMCAwIDEzMzE1NiAxMTUzNDEiDQogeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPg0KIDxkZWZzPg0KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KICAgPCFbQ0RBVEFbDQogICAgLmZpbDIge2ZpbGw6IzExQTg2MX0NCiAgICAuZmlsMCB7ZmlsbDojMzc3N0UzfQ0KICAgIC5maWwxIHtmaWxsOiNGRkNGNjN9DQogICBdXT4NCiAgPC9zdHlsZT4NCiA8L2RlZnM+DQogPGcgaWQ9IkxpdmVsbG9feDAwMjBfMSI+DQogIDxtZXRhZGF0YSBpZD0iQ29yZWxDb3JwSURfMENvcmVsLUxheWVyIi8+DQogIDxwb2x5Z29uIGNsYXNzPSJmaWwwIiBwb2ludHM9IjIyMTk0LDExNTM0MSA0NDM4NSw3Njg5NCAxMzMxNTYsNzY4OTQgMTEwOTYzLDExNTM0MSAiLz4NCiAgPHBvbHlnb24gY2xhc3M9ImZpbDEiIHBvaW50cz0iODg3NzIsNzY4OTQgMTMzMTU2LDc2ODk0IDg4NzcyLDAgNDQzODUsMCAiLz4NCiAgPHBvbHlnb24gY2xhc3M9ImZpbDIiIHBvaW50cz0iMCw3Njg5NCAyMjE5NCwxMTUzNDEgNjY1NzgsMzg0NDcgNDQzODUsMCAiLz4NCiA8L2c+DQo8L3N2Zz4=);background-size:100% 100%}.mapjs-attachment,.mapjs-hyperlink{background-repeat:no-repeat no-repeat;position:absolute}#collaboratorSpeechBubble{position:absolute;z-index:2;top:100px;left:20px}#collaboratorSpeechBubble img{width:60px;height:60px;border-radius:60px;border:2px solid transparent;box-shadow:0 5px 10px rgba(0,0,0,.2);cursor:pointer}#collaboratorSpeechBubble img:hover{box-shadow:0 5px 10px rgba(0,0,0,.5)}.speech-bubble-inner{max-width:300px;max-height:100px;min-width:150px;overflow:hidden}.speech-bubble-title{max-width:300px;white-space:nowrap;overflow:hidden}.icon-volume-off{margin-left:3px;margin-right:5px}.hidden-topbar #mainMenu .dropdown-menu{margin-top:0}.mapjs-node.activated,.mapjs-node.droppable{margin:-2px;outline:0}.mapjs-node{z-index:3;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;font-family:Helvetica,"Arial Unicode MS",sans-serif;font-weight:700;font-size:12px}.mapjs-add-link{cursor:crosshair}.mapjs-add-link .mapjs-node{cursor:alias}.mapjs-node span{white-space:pre-wrap;text-align:center;line-height:150%;display:block;max-width:146px;min-height:1.5em;min-width:1em;outline:0;cursor:pointer}.mapjs-node span[contenteditable=true]{user-select:text;-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text;cursor:auto}#container,.mindmup-container,.mindmup-noselect{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.mapjs-node:focus{outline:0}.mapjs-node.selected{outline:0;z-index:4}.mapjs-node.dragging{opacity:.4;z-index:5}.mapjs-node-light{color:#4F4F4F}.mapjs-node-dark{color:#EEE}.mapjs-node-white{color:#000}.mapjs-label{left:-.75em;position:absolute;bottom:-1em;opacity:.8;background-color:#f13333;padding:1px 2px;border:1px solid #fff;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;display:inline-block;font-size:10px;font-weight:700;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25);white-space:nowrap;vertical-align:baseline}.mapjs-hyperlink{right:-.75em;bottom:-.75em;background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiDQoNCgkgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQoNCjxwYXRoIHN0eWxlPSJmaWxsOmJsYWNrIiBkPSJNMTU2LjIyNiwxOTkuNjc5YzcuNTQxLTcuNTQsMTUuOTAyLTEzLjc1NywyNC43OTQtMTguNjU5YzQ5LjU1Ni0yNy4zMTgsMTEzLjExNy0xMi43ODgsMTQ0Ljk3LDM1LjUxOA0KDQoJbC0zOC41NDcsMzguNTQ3Yy0xMS4wNTktMjUuMjI3LTM4LjUtMzkuNTY1LTY1LjgxMy0zMy40NTZjLTEwLjI4MiwyLjMtMjAuMDU0LDcuNDI3LTI4LjAzOSwxNS40MTNsLTczLjg5OCw3My44OTYNCg0KCWMtMjIuNDMzLDIyLjQzMy0yMi40MzIsNTguOTM2LDAuMDAyLDgxLjM2OWMyMi40MzMsMjIuNDMzLDU4LjkzNSwyMi40MzMsODEuMzY4LDBsMjIuNzgtMjIuNzc5DQoNCgljMjAuNzEsOC4yMTcsNDIuOTM4LDExLjUwOCw2NC44NjIsOS44NjNsLTUwLjI3OCw1MC4yNzhjLTQzLjEwNSw0My4xMDUtMTEyLjk5MSw0My4xMDUtMTU2LjA5NiwwDQoNCgljLTQzLjEwNS00My4xMDQtNDMuMTA2LTExMi45OTEtMC4wMDEtMTU2LjA5NkwxNTYuMjI2LDE5OS42Nzl6IE0yNzMuNTc0LDgyLjMzbC01MC4yNzgsNTAuMjc4DQoNCgljMjEuOTI4LTEuNjQzLDQ0LjE1MiwxLjY0OCw2NC44NjMsOS44NjVsMjIuNzc5LTIyLjc4YzIyLjQzNC0yMi40MzQsNTguOTM2LTIyLjQzNCw4MS4zNywwYzIyLjQzNCwyMi40MzQsMjIuNDM0LDU4LjkzNiwwLDgxLjM3DQoNCglsLTczLjg5Nyw3My44OTVjLTIyLjUwMSwyMi41MDEtNTkuMDYxLDIyLjMxMS04MS4zNjgsMGMtNS4yMDItNS4yMDEtOS42OTQtMTEuNjc4LTEyLjQ4NC0xOC4wNGwtMzguNTQ2LDM4LjU0Ng0KDQoJYzQuMDQ5LDYuMTQyLDguMjYxLDExLjQ1MywxMy42NjYsMTYuODU4YzEzLjk0OSwxMy45NSwzMS42OTgsMjQuMzM5LDUyLjExNywyOS4yNTFjMjYuNDY2LDYuMzcsNTQuODIzLDIuODM5LDc5LjE4NS0xMC41OTINCg0KCWM4Ljg5Mi00LjkwMywxNy4yNTQtMTEuMTE5LDI0Ljc5NC0xOC42NTlsNzMuODk2LTczLjg5NWM0My4xMDUtNDMuMTA1LDQzLjEwNS0xMTIuOTkxLDAuMDAxLTE1Ni4wOTcNCg0KCUMzODYuNTY2LDM5LjIyNSwzMTYuNjgsMzkuMjI1LDI3My41NzQsODIuMzN6Ii8+DQo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjAsLTIwKSIgc3R5bGU9ImZpbGw6IzAwOUFDRCI+DQo8cGF0aCAgZD0iTTE1Ni4yMjYsMTk5LjY3OWM3LjU0MS03LjU0LDE1LjkwMi0xMy43NTcsMjQuNzk0LTE4LjY1OWM0OS41NTYtMjcuMzE4LDExMy4xMTctMTIuNzg4LDE0NC45NywzNS41MTgNCg0KCWwtMzguNTQ3LDM4LjU0N2MtMTEuMDU5LTI1LjIyNy0zOC41LTM5LjU2NS02NS44MTMtMzMuNDU2Yy0xMC4yODIsMi4zLTIwLjA1NCw3LjQyNy0yOC4wMzksMTUuNDEzbC03My44OTgsNzMuODk2DQoNCgljLTIyLjQzMywyMi40MzMtMjIuNDMyLDU4LjkzNiwwLjAwMiw4MS4zNjljMjIuNDMzLDIyLjQzMyw1OC45MzUsMjIuNDMzLDgxLjM2OCwwbDIyLjc4LTIyLjc3OQ0KDQoJYzIwLjcxLDguMjE3LDQyLjkzOCwxMS41MDgsNjQuODYyLDkuODYzbC01MC4yNzgsNTAuMjc4Yy00My4xMDUsNDMuMTA1LTExMi45OTEsNDMuMTA1LTE1Ni4wOTYsMA0KDQoJYy00My4xMDUtNDMuMTA0LTQzLjEwNi0xMTIuOTkxLTAuMDAxLTE1Ni4wOTZMMTU2LjIyNiwxOTkuNjc5eiBNMjczLjU3NCw4Mi4zM2wtNTAuMjc4LDUwLjI3OA0KDQoJYzIxLjkyOC0xLjY0Myw0NC4xNTIsMS42NDgsNjQuODYzLDkuODY1bDIyLjc3OS0yMi43OGMyMi40MzQtMjIuNDM0LDU4LjkzNi0yMi40MzQsODEuMzcsMGMyMi40MzQsMjIuNDM0LDIyLjQzNCw1OC45MzYsMCw4MS4zNw0KDQoJbC03My44OTcsNzMuODk1Yy0yMi41MDEsMjIuNTAxLTU5LjA2MSwyMi4zMTEtODEuMzY4LDBjLTUuMjAyLTUuMjAxLTkuNjk0LTExLjY3OC0xMi40ODQtMTguMDRsLTM4LjU0NiwzOC41NDYNCg0KCWM0LjA0OSw2LjE0Miw4LjI2MSwxMS40NTMsMTMuNjY2LDE2Ljg1OGMxMy45NDksMTMuOTUsMzEuNjk4LDI0LjMzOSw1Mi4xMTcsMjkuMjUxYzI2LjQ2Niw2LjM3LDU0LjgyMywyLjgzOSw3OS4xODUtMTAuNTkyDQoNCgljOC44OTItNC45MDMsMTcuMjU0LTExLjExOSwyNC43OTQtMTguNjU5bDczLjg5Ni03My44OTVjNDMuMTA1LTQzLjEwNSw0My4xMDUtMTEyLjk5MSwwLjAwMS0xNTYuMDk3DQoNCglDMzg2LjU2NiwzOS4yMjUsMzE2LjY4LDM5LjIyNSwyNzMuNTc0LDgyLjMzeiIvPg0KPC9nPg0KDQo8L3N2Zz4NCg0K);width:2em;height:2em;background-size:2em}.mapjs-hyperlink:hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiDQoNCgkgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQoNCjxwYXRoIHN0eWxlPSJmaWxsOmJsYWNrIiBkPSJNMTU2LjIyNiwxOTkuNjc5YzcuNTQxLTcuNTQsMTUuOTAyLTEzLjc1NywyNC43OTQtMTguNjU5YzQ5LjU1Ni0yNy4zMTgsMTEzLjExNy0xMi43ODgsMTQ0Ljk3LDM1LjUxOA0KDQoJbC0zOC41NDcsMzguNTQ3Yy0xMS4wNTktMjUuMjI3LTM4LjUtMzkuNTY1LTY1LjgxMy0zMy40NTZjLTEwLjI4MiwyLjMtMjAuMDU0LDcuNDI3LTI4LjAzOSwxNS40MTNsLTczLjg5OCw3My44OTYNCg0KCWMtMjIuNDMzLDIyLjQzMy0yMi40MzIsNTguOTM2LDAuMDAyLDgxLjM2OWMyMi40MzMsMjIuNDMzLDU4LjkzNSwyMi40MzMsODEuMzY4LDBsMjIuNzgtMjIuNzc5DQoNCgljMjAuNzEsOC4yMTcsNDIuOTM4LDExLjUwOCw2NC44NjIsOS44NjNsLTUwLjI3OCw1MC4yNzhjLTQzLjEwNSw0My4xMDUtMTEyLjk5MSw0My4xMDUtMTU2LjA5NiwwDQoNCgljLTQzLjEwNS00My4xMDQtNDMuMTA2LTExMi45OTEtMC4wMDEtMTU2LjA5NkwxNTYuMjI2LDE5OS42Nzl6IE0yNzMuNTc0LDgyLjMzbC01MC4yNzgsNTAuMjc4DQoNCgljMjEuOTI4LTEuNjQzLDQ0LjE1MiwxLjY0OCw2NC44NjMsOS44NjVsMjIuNzc5LTIyLjc4YzIyLjQzNC0yMi40MzQsNTguOTM2LTIyLjQzNCw4MS4zNywwYzIyLjQzNCwyMi40MzQsMjIuNDM0LDU4LjkzNiwwLDgxLjM3DQoNCglsLTczLjg5Nyw3My44OTVjLTIyLjUwMSwyMi41MDEtNTkuMDYxLDIyLjMxMS04MS4zNjgsMGMtNS4yMDItNS4yMDEtOS42OTQtMTEuNjc4LTEyLjQ4NC0xOC4wNGwtMzguNTQ2LDM4LjU0Ng0KDQoJYzQuMDQ5LDYuMTQyLDguMjYxLDExLjQ1MywxMy42NjYsMTYuODU4YzEzLjk0OSwxMy45NSwzMS42OTgsMjQuMzM5LDUyLjExNywyOS4yNTFjMjYuNDY2LDYuMzcsNTQuODIzLDIuODM5LDc5LjE4NS0xMC41OTINCg0KCWM4Ljg5Mi00LjkwMywxNy4yNTQtMTEuMTE5LDI0Ljc5NC0xOC42NTlsNzMuODk2LTczLjg5NWM0My4xMDUtNDMuMTA1LDQzLjEwNS0xMTIuOTkxLDAuMDAxLTE1Ni4wOTcNCg0KCUMzODYuNTY2LDM5LjIyNSwzMTYuNjgsMzkuMjI1LDI3My41NzQsODIuMzN6Ii8+DQo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjAsLTIwKSIgc3R5bGU9ImZpbGw6IzAwQkZGRiI+DQo8cGF0aCAgZD0iTTE1Ni4yMjYsMTk5LjY3OWM3LjU0MS03LjU0LDE1LjkwMi0xMy43NTcsMjQuNzk0LTE4LjY1OWM0OS41NTYtMjcuMzE4LDExMy4xMTctMTIuNzg4LDE0NC45NywzNS41MTgNCg0KCWwtMzguNTQ3LDM4LjU0N2MtMTEuMDU5LTI1LjIyNy0zOC41LTM5LjU2NS02NS44MTMtMzMuNDU2Yy0xMC4yODIsMi4zLTIwLjA1NCw3LjQyNy0yOC4wMzksMTUuNDEzbC03My44OTgsNzMuODk2DQoNCgljLTIyLjQzMywyMi40MzMtMjIuNDMyLDU4LjkzNiwwLjAwMiw4MS4zNjljMjIuNDMzLDIyLjQzMyw1OC45MzUsMjIuNDMzLDgxLjM2OCwwbDIyLjc4LTIyLjc3OQ0KDQoJYzIwLjcxLDguMjE3LDQyLjkzOCwxMS41MDgsNjQuODYyLDkuODYzbC01MC4yNzgsNTAuMjc4Yy00My4xMDUsNDMuMTA1LTExMi45OTEsNDMuMTA1LTE1Ni4wOTYsMA0KDQoJYy00My4xMDUtNDMuMTA0LTQzLjEwNi0xMTIuOTkxLTAuMDAxLTE1Ni4wOTZMMTU2LjIyNiwxOTkuNjc5eiBNMjczLjU3NCw4Mi4zM2wtNTAuMjc4LDUwLjI3OA0KDQoJYzIxLjkyOC0xLjY0Myw0NC4xNTIsMS42NDgsNjQuODYzLDkuODY1bDIyLjc3OS0yMi43OGMyMi40MzQtMjIuNDM0LDU4LjkzNi0yMi40MzQsODEuMzcsMGMyMi40MzQsMjIuNDM0LDIyLjQzNCw1OC45MzYsMCw4MS4zNw0KDQoJbC03My44OTcsNzMuODk1Yy0yMi41MDEsMjIuNTAxLTU5LjA2MSwyMi4zMTEtODEuMzY4LDBjLTUuMjAyLTUuMjAxLTkuNjk0LTExLjY3OC0xMi40ODQtMTguMDRsLTM4LjU0NiwzOC41NDYNCg0KCWM0LjA0OSw2LjE0Miw4LjI2MSwxMS40NTMsMTMuNjY2LDE2Ljg1OGMxMy45NDksMTMuOTUsMzEuNjk4LDI0LjMzOSw1Mi4xMTcsMjkuMjUxYzI2LjQ2Niw2LjM3LDU0LjgyMywyLjgzOSw3OS4xODUtMTAuNTkyDQoNCgljOC44OTItNC45MDMsMTcuMjU0LTExLjExOSwyNC43OTQtMTguNjU5bDczLjg5Ni03My44OTVjNDMuMTA1LTQzLjEwNSw0My4xMDUtMTEyLjk5MSwwLjAwMS0xNTYuMDk3DQoNCglDMzg2LjU2NiwzOS4yMjUsMzE2LjY4LDM5LjIyNSwyNzMuNTc0LDgyLjMzeiIvPg0KPC9nPg0KDQo8L3N2Zz4NCg==)}.mapjs-attachment{right:-5px;top:-15px;background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjMycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJ0cmFuc2Zvcm06cm90YXRlKDEwKSI+DQogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiwtMikgcm90YXRlKDEwKSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2Utd2lkdGg6MiI+DQogICA8cGF0aCBkPSJNNCwxNiBMNCwxMSBDNCwxIDE1LDEgMTUsMTEgTDE1LDI2IEMxNSwzMSA4LDMxIDgsMjUgTDgsMTAiDQogICAgIHN0eWxlPSJzdHJva2U6YmxhY2siLz4NCiAgIDxwYXRoIGQ9Ik0zLDE2IEwzLDEwIEMzLDAgMTQsMCAxNCwxMCBMMTQsMjUgQzE0LDMwIDcsMzAgNywyNSBMNywxMCINCiAgICAgc3R5bGU9InN0cm9rZTojMDA5QUNEIi8+DQogIDwvZz4NCjwvc3ZnPg0K);width:16px;height:32px;background-size:16px 32px}.mapjs-attachment:hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjMycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJ0cmFuc2Zvcm06cm90YXRlKDEwKSI+DQogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMiwtMikgcm90YXRlKDEwKSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2Utd2lkdGg6MiI+DQogICA8cGF0aCBkPSJNNCwxNiBMNCwxMSBDNCwxIDE1LDEgMTUsMTEgTDE1LDI2IEMxNSwzMSA4LDMxIDgsMjUgTDgsMTAiDQogICAgIHN0eWxlPSJzdHJva2U6YmxhY2siLz4NCiAgIDxwYXRoIGQ9Ik0zLDE2IEwzLDEwIEMzLDAgMTQsMCAxNCwxMCBMMTQsMjUgQzE0LDMwIDcsMzAgNywyNSBMNywxMCINCiAgICAgc3R5bGU9InN0cm9rZTojMDBCRkZGIi8+DQogIDwvZz4NCjwvc3ZnPg0K)}.mapjs-draw-container{position:absolute;margin:0;padding:0}.mapjs-draw-container[data-mapjs-role=connector]{z-index:1}.mapjs-draw-container[data-mapjs-role=link]{z-index:2}.mapjs-connector{stroke-width:1px;fill:none}.mapjs-link{stroke-width:1.5px;fill:none}.mapjs-link-hit{stroke:transparent;stroke-width:15;cursor:crosshair}#container{background-color:#FFF;margin:0;padding:0}.mapjs-reorder-bounds{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9IjExcHgiIGhlaWdodD0iMjAuNjM0cHgiPg0KPHBhdGggZD0iTTAuMTk5LDEwLjc5bDkuNjQ2LDkuNjQ2YzAuMTMsMC4xMzMsMC4zMDIsMC4xOTcsMC40NzEsMC4xOTdjMC4xNzMsMCwwLjM0NC0wLjA2NCwwLjQ3My0wLjE5N2wwLjAxMy0wLjAxMQ0KCUMxMC45MjksMjAuMzA1LDExLDIwLjEzMiwxMSwxOS45NTJ2LTQuNjhjMC0wLjE4Mi0wLjA3LTAuMzQ3LTAuMTk4LTAuNDc1TDYuMzIzLDEwLjMybDQuNDc5LTQuNDgzQzEwLjkyOSw1LjcxNCwxMSw1LjU0MywxMSw1LjM2NA0KCVYwLjY4MmMwLTAuMTc2LTAuMDctMC4zNDUtMC4xOTgtMC40NzRsLTAuMDEzLTAuMDEyQzEwLjY2NywwLjA3MSwxMC40OTcsMCwxMC4zMTYsMEMxMC4xNDMsMCw5Ljk3LDAuMDcxLDkuODQ1LDAuMTk2TDAuMTk5LDkuODQ1DQoJQy0wLjA2NiwxMC4xMDQtMC4wNjYsMTAuNTMyLDAuMTk5LDEwLjc5eiIvPg0KPC9zdmc+);background-height:100%;background-width:100%;height:20px;width:11px;z-index:999;background-repeat:no-repeat}.mapjs-reorder-bounds[mapjs-edge=left]{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgd2lkdGg9IjExcHgiIGhlaWdodD0iMjAuNjM0cHgiPg0KICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTEsIDApIHNjYWxlKC0xLCAxKSI+DQo8cGF0aCBkPSJNMC4xOTksMTAuNzlsOS42NDYsOS42NDZjMC4xMywwLjEzMywwLjMwMiwwLjE5NywwLjQ3MSwwLjE5N2MwLjE3MywwLDAuMzQ0LTAuMDY0LDAuNDczLTAuMTk3bDAuMDEzLTAuMDExDQoJQzEwLjkyOSwyMC4zMDUsMTEsMjAuMTMyLDExLDE5Ljk1MnYtNC42OGMwLTAuMTgyLTAuMDctMC4zNDctMC4xOTgtMC40NzVMNi4zMjMsMTAuMzJsNC40NzktNC40ODNDMTAuOTI5LDUuNzE0LDExLDUuNTQzLDExLDUuMzY0DQoJVjAuNjgyYzAtMC4xNzYtMC4wNy0wLjM0NS0wLjE5OC0wLjQ3NGwtMC4wMTMtMC4wMTJDMTAuNjY3LDAuMDcxLDEwLjQ5NywwLDEwLjMxNiwwQzEwLjE0MywwLDkuOTcsMC4wNzEsOS44NDUsMC4xOTZMMC4xOTksOS44NDUNCglDLTAuMDY2LDEwLjEwNC0wLjA2NiwxMC41MzIsMC4xOTksMTAuNzl6Ii8+DQogIDwvZz4NCjwvc3ZnPg==)}.mindmup_context_menu_item{cursor:pointer}.mindmup-legend-item-cont .gravatar,.mindmup-node-icon-avatar{height:24px;width:24px}.mindmup-legend-item-cont .avatar-container,.mindmup-node-icon-avatar .avatar-container{margin:0}.mindmup-legend-item-cont .gravatar:hover,.mindmup-node-icon-avatar .gravatar:hover{transform:scale(2);position:relative;z-index:5}#context-menu li:not(.folder)>ul{display:none}.controller-easy_wbs .easy-content-page{transform:none!important}.link-edit-widget{display:none;z-index:999;position:absolute;padding:10px;background-color:#fff;border:1px solid #dd3e3a;cursor:default}mindmup-sidebar{display:block}.date-picker__date-wrap .input-append{width:auto;float:right}.date-picker__date-wrap{margin-bottom:5px;overflow:hidden}easy-lookup .easy-lookup-values-wrapper{height:auto}#sidebar{z-index:3}.mindmup-sidebar__closed_button:before{font-family:"Material Icons",sans-serif}.mindmup-sidebar-comments .journal{margin:6px 0 0}.mindmup_hover_menu{-webkit-border-radius:.24rem;-moz-border-radius:.24rem;border-radius:.24rem}.mapjs-collapsor,.mindmup-legend-color-box{-webkit-border-radius:5000px!important;-moz-border-radius:5000px!important;border-radius:5000px!important}.easy .mindmup__menu-group--tooltiped li>.menu-children,.easy .mindmup__menu-group--tooltiped ul,.mapjs-node{-webkit-border-radius:.12rem;-moz-border-radius:.12rem;border-radius:.12rem}.mindmup-node-icon-milestone-shell{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.mindmup-node-icon-progress{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.mindmup__legend-body,.mindmup_hover_menu{-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.mindmup__menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mindmup__legend-header,.mindmup__menu,.mindmup__node-control{display:-webkit-box;display:-moz-box;display:box;display:-webkit-flex;display:-moz-flex;display:-ms-flexbox;display:flex}.mindmup__menu,.mindmup__node-control{-webkit-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between}.mindmup__legend-header>label,.mindmup__menu-group--sizing{-webkit-flex-grow:1;-ms-flex-grow:1;flex-grow:1}.easy .mindmup__menu-group--tooltiped li{font-family:Ubuntu,"Open Sans",sans-serif;font-size:15px;font-weight:400;color:#324164}@media only screen and (max-width:1400px){.easy .mindmup__menu-group--tooltiped li{font-size:12px}}@media only screen and (max-width:1400px) and (min-resolution:100dpi){.easy .mindmup__menu-group--tooltiped li{font-size:15px}}@-webkit-keyframes icon-blink{from{opacity:.25;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}to{opacity:1;-webkit-transform:scale(1.25);-moz-transform:scale(1.25);-ms-transform:scale(1.25);-o-transform:scale(1.25);transform:scale(1.25)}}@-moz-keyframes icon-blink{from{opacity:.25;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}to{opacity:1;-webkit-transform:scale(1.25);-moz-transform:scale(1.25);-ms-transform:scale(1.25);-o-transform:scale(1.25);transform:scale(1.25)}}@-ms-keyframes icon-blink{from{opacity:.25;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}to{opacity:1;-webkit-transform:scale(1.25);-moz-transform:scale(1.25);-ms-transform:scale(1.25);-o-transform:scale(1.25);transform:scale(1.25)}}@keyframes icon-blink{from{opacity:.25;-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}to{opacity:1;-webkit-transform:scale(1.25);-moz-transform:scale(1.25);-ms-transform:scale(1.25);-o-transform:scale(1.25);transform:scale(1.25)}}@-webkit-keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@-moz-keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@-ms-keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@-webkit-keyframes spin-inverse{from{transform:rotate(0)}to{transform:rotate(-360deg)}}@-moz-keyframes spin-inverse{from{transform:rotate(0)}to{transform:rotate(-360deg)}}@-ms-keyframes spin-inverse{from{transform:rotate(0)}to{transform:rotate(-360deg)}}@keyframes spin-inverse{from{transform:rotate(0)}to{transform:rotate(-360deg)}}@-webkit-keyframes translate-x{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-moz-keyframes translate-x{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-ms-keyframes translate-x{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@keyframes translate-x{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes translate-y{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-moz-keyframes translate-y{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-ms-keyframes translate-y{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@keyframes translate-y{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes translate-menu{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-moz-keyframes translate-menu{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-ms-keyframes translate-menu{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@keyframes translate-menu{100%{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes load7{0%,100%,80%{box-shadow:0 2.5em 0 -1.3em}40%{box-shadow:0 2.5em 0 0}}@-moz-keyframes load7{0%,100%,80%{box-shadow:0 2.5em 0 -1.3em}40%{box-shadow:0 2.5em 0 0}}@-ms-keyframes load7{0%,100%,80%{box-shadow:0 2.5em 0 -1.3em}40%{box-shadow:0 2.5em 0 0}}@keyframes load7{0%,100%,80%{box-shadow:0 2.5em 0 -1.3em}40%{box-shadow:0 2.5em 0 0}}@-webkit-keyframes ripple{0%{-webkit-transform:scale(0);-moz-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);opacity:.2}100%{-webkit-transform:scale(3);-moz-transform:scale(3);-ms-transform:scale(3);-o-transform:scale(3);transform:scale(3);opacity:0}}@-moz-keyframes ripple{0%{-webkit-transform:scale(0);-moz-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);opacity:.2}100%{-webkit-transform:scale(3);-moz-transform:scale(3);-ms-transform:scale(3);-o-transform:scale(3);transform:scale(3);opacity:0}}@-ms-keyframes ripple{0%{-webkit-transform:scale(0);-moz-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);opacity:.2}100%{-webkit-transform:scale(3);-moz-transform:scale(3);-ms-transform:scale(3);-o-transform:scale(3);transform:scale(3);opacity:0}}@keyframes ripple{0%{-webkit-transform:scale(0);-moz-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);transform:scale(0);opacity:.2}100%{-webkit-transform:scale(3);-moz-transform:scale(3);-ms-transform:scale(3);-o-transform:scale(3);transform:scale(3);opacity:0}}.easy .mindmup__menu-group--tooltiped li a.submenu:after,.easy-mindmup__icon:before,.mapjs-collapsor,.mindmup__legend-toggler.active a:before{speak:none;font-weight:400;font-style:normal;display:inline-block;width:auto;height:auto;background-position:0 0;background-repeat:repeat;background-image:none;vertical-align:baseline;text-decoration:none!important;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:'liga';font-family:'Material Icons',EasyIcons}.easy-mindmup__icon:before,.mapjs-collapsor,.material-icons{font-family:"Material Icons",sans-serif}.easy-mindmup__icon,.mindmup__legend-toggler.active a{position:relative;background-repeat:no-repeat;background-image:none!important}.mindmup-container{background-image:linear-gradient(rgba(217,217,217,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(217,217,217,.2) 1px,transparent 1px);background-size:.8rem .8rem,.8rem .8rem;background-position:-1px -1px,-1px -1px}.easy .mindmup__menu-group--tooltiped li>.menu-children,.easy .mindmup__menu-group--tooltiped ul,.mindmup-sidebar__input__name .baseline,.mindmup__legend-container{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1),0 1px 1px rgba(0,0,0,.1);-moz-box-shadow:0 1px 2px rgba(0,0,0,.1),0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 2px rgba(0,0,0,.1),0 1px 1px rgba(0,0,0,.1)}.easy .mindmup__menu-group--tooltiped ul,.mapjs-node.activated,.mapjs-node.droppable,.mapjs-node.selected{-webkit-box-shadow:0 2px 4px rgba(0,0,0,.1),0 2px 2px rgba(0,0,0,.1);-moz-box-shadow:0 2px 4px rgba(0,0,0,.1),0 2px 2px rgba(0,0,0,.1);box-shadow:0 2px 4px rgba(0,0,0,.1),0 2px 2px rgba(0,0,0,.1)}@-webkit-keyframes sidebar-buttons-slide-left{from{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}to{-webkit-transform:translate(-12.2666666667rem,0);-moz-transform:translate(-12.2666666667rem,0);-ms-transform:translate(-12.2666666667rem,0);-o-transform:translate(-12.2666666667rem,0);transform:translate(-12.2666666667rem,0)}}@-moz-keyframes sidebar-buttons-slide-left{from{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}to{-webkit-transform:translate(-12.2666666667rem,0);-moz-transform:translate(-12.2666666667rem,0);-ms-transform:translate(-12.2666666667rem,0);-o-transform:translate(-12.2666666667rem,0);transform:translate(-12.2666666667rem,0)}}@-ms-keyframes sidebar-buttons-slide-left{from{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}to{-webkit-transform:translate(-12.2666666667rem,0);-moz-transform:translate(-12.2666666667rem,0);-ms-transform:translate(-12.2666666667rem,0);-o-transform:translate(-12.2666666667rem,0);transform:translate(-12.2666666667rem,0)}}@keyframes sidebar-buttons-slide-left{from{-webkit-transform:translate(0,0);-moz-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}to{-webkit-transform:translate(-12.2666666667rem,0);-moz-transform:translate(-12.2666666667rem,0);-ms-transform:translate(-12.2666666667rem,0);-o-transform:translate(-12.2666666667rem,0);transform:translate(-12.2666666667rem,0)}}.easy-mindmup__icon.button:before{position:absolute;left:0;width:2.4rem;text-align:center;font-size:1.25em;line-height:1;color:inherit;top:50%;margin-top:-.5em}.easy .mindmup__menu-group--tooltiped li a.submenu:after{margin:0;padding:0;display:block;background:0 0;border:none;min-width:1.6rem;position:absolute;top:0;right:0;bottom:0}.easy .mindmup__menu-group--tooltiped li a.submenu:after span{display:none}.easy .mindmup__menu-group--tooltiped li>.menu-children,.easy .mindmup__menu-group--tooltiped ul{position:absolute;background-color:#fff;color:#324164;padding:.5em;font-size:.89em;line-height:1;margin-top:-.25em;white-space:pre}.easy .mindmup__menu-group--tooltiped li>.menu-children a,.easy .mindmup__menu-group--tooltiped ul a{color:#324164}.easy .mindmup__menu-group--tooltiped .input-append li>.menu-children,.easy .mindmup__menu-group--tooltiped .input-append ul,.input-append .easy .mindmup__menu-group--tooltiped li>.menu-children,.input-append .easy .mindmup__menu-group--tooltiped ul{font-weight:400;margin-top:1px}.easy .mindmup__menu-group--tooltiped li{padding:0 0 0 2.4rem;border:1px solid transparent;border-left:none;border-right:none;position:relative;line-height:1.25}.easy .mindmup__menu-group--tooltiped li:hover{border-color:#f0f8ff!important;background:#f0f8ff!important;z-index:1}.easy .mindmup__menu-group--tooltiped li a{-ms-word-break:break-all;word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto;padding:.4rem .8rem .4rem 3.2rem;margin-left:-2.4rem;display:block;text-decoration:none}.easy .mindmup__menu-group--tooltiped li a.active{color:#fe7d99;background:0 0;border:none}.easy .mindmup__menu-group--tooltiped li a.active:before{color:#fe7d99}.easy .mindmup__menu-group--tooltiped li a:before{position:absolute;left:0;width:2.4rem;text-align:center;padding-top:1px;color:#324164}.easy .mindmup__menu-group--tooltiped li a.submenu{padding-right:0!important;background:0 0!important;position:relative}.easy .mindmup__menu-group--tooltiped li a.submenu:after{content:"\005d"!important;left:auto;font-size:.6666666667rem;text-align:left;line-height:1.9}.easy .mindmup__menu-group--tooltiped li a~span{font-size:.89em}.easy .mindmup__menu-group--tooltiped{position:relative}.easy .mindmup__menu-group--tooltiped ul{z-index:1500;margin:0;list-style:none;font-size:1.125em;min-width:200px;padding:.4rem 0;white-space:normal!important}.easy .mindmup__menu-group--tooltiped ul:after{content:'';position:absolute;top:0;bottom:0;right:auto;left:2.4rem;border-left:1px solid #e5e5e5;z-index:0}.easy .mindmup__menu-group--tooltiped li>.menu-children{display:none;white-space:normal;top:6px;left:99%;width:150px}.easy .mindmup__menu-group--tooltiped li>.menu-children>li+li{border-top:1px dashed #e5e5e5}.easy .mindmup__menu-group--tooltiped li>.menu-children>li a{text-decoration:none;display:block;padding:.5em}.easy .mindmup__menu-group--tooltiped li:hover>.menu-children,.mindmup__menu-item{display:inline-block}.easy .mindmup__menu-group--tooltiped li>.menu-children>li a:hover{text-decoration:underline}.easy .mindmup__menu-group--tooltiped li>.menu-children>li a:before{color:#01c8a9;text-decoration:none;margin-right:.4rem}.mindmup-cont{margin:0 -1.2rem .4rem;width:auto;overflow:hidden}@media only screen and (max-width:32rem){.mindmup-cont{margin-top:-1.6rem}}.mindmup-container{position:relative;cursor:all-scroll;border-top:none;box-sizing:border-box;background-color:#f9f9f9;margin:0;padding:0;outline:0;overflow-y:hidden!important}.mindmup__menu{user-select:none;position:relative;z-index:1;background-color:#fff;border-bottom:1px solid #e5e5e5;padding:1.6rem}.mindmup__menu_addons{position:absolute;right:0;top:5.6rem;z-index:5}.mindmup__menu-item{text-align:left}.mindmup__menu-item a.active,.mindmup__menu-item a.active:before{color:#d94838!important}@media only screen and (max-width:960px){.mindmup__menu-item>a{padding-right:0}.mindmup__menu-item>a>span{display:none}}@media only screen and (max-width:1400px){.mindmup__menu-item>a.easy-mindmup__icon--display,.mindmup__menu-item>a.easy-mindmup__icon--settings{padding-right:0}.mindmup__menu-item>a.easy-mindmup__icon--display>span,.mindmup__menu-item>a.easy-mindmup__icon--settings>span{display:none}}.mindmup__menu .right-menu{float:right}.mindmup__menu-group ul{margin:0}.mindmup__menu-group--tooltiped>ul{display:none}.mindmup__menu-group--tooltiped:hover>ul{display:block}.mindmup__menu-group--sizing{text-align:center;font-size:1.5em;position:absolute;top:1.6rem;left:-7.6rem;line-height:1.6rem}@media only screen and (max-width:32rem){.mindmup__menu-group--sizing{display:none}}.mindmup__menu-group--sizing a{color:rgba(50,65,100,.5);text-decoration:none}.mindmup__menu-group--sizing li{list-style:none;display:inline-block}@media only screen and (min-width:33rem){.mindmup__menu-group-display{margin-left:18.1333333333rem}}.mindmup__legend-container--hidden+.mindmup__menu-group-display{margin-left:0}.mindmup_hover_menu{display:block;position:absolute;z-index:99;background-color:#fff;min-width:160px;padding:5px 0;margin:2px 0 0;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.mindmup-reload-modal-errors li{color:red}.mindmup-last-modal-diffs li{color:#fa9b3c}.mindmup__button--disabled{opacity:.4}.mindmup_modal__flash_close{position:absolute;right:.8rem;font-size:1.6rem;line-height:1.6rem}.easy-mindmup__icon--add:before,.easy-mindmup__icon--add_sibling:before{content:"\e3ba"}.easy-mindmup__icon--insert_between:before{content:"\e3bb"}.easy-mindmup__icon--remove:before{content:"\e92b"}.easy-mindmup__icon--rename:before{content:"\e22b"}.easy-mindmup__icon--edit_data:before{content:"\e880"}.easy-mindmup__icon--edit:before{content:"\e3c9"}.easy-mindmup__icon--follow_url:before{content:"\e0e2"}.easy-mindmup__icon--zoom_in:before{content:"\e8ff"}.easy-mindmup__icon--zoom_out:before{content:"\e900"}.easy-mindmup__icon--refresh_view:before{content:"\e881"}.easy-mindmup__icon--filter:before{content:"\e152"}.easy-mindmup__icon--cancel:before,.easy-mindmup__icon--cancel_filter:before{content:"\e5c9"}.easy-mindmup__icon--close:before{content:"\e5cd"}.easy-mindmup__icon--display:before{content:"\e417"}.easy-mindmup__icon--links:before{content:"\e157"}.easy-mindmup__icon--icons:before{content:"\e24e"}.easy-mindmup__icon--collapse:before{content:"\e909"}.easy-mindmup__icon--expand:before{content:"\e146"}.easy-mindmup__icon--one_side:before{content:"\e86d"}.easy-mindmup__icon--settings:before{content:"\e8b8"}.easy-mindmup__icon--undo:before{content:"\e166"}.easy-mindmup__icon--redo:before{content:"\e15a"}.easy-mindmup__icon--print:before{content:"\e16b"}.easy-mindmup__icon--cut:before{content:"\e14e"}.easy-mindmup__icon--copy:before{content:"\e14d"}.easy-mindmup__icon--paste:before{content:"\e14f"}.easy-mindmup__icon--save:before{content:"\e161"}.easy-mindmup__icon--legend:before{content:"\e5d2"}.easy-mindmup__icon--legend_hide:before{content:"\e06d"}.easy-mindmup__icon.button{padding-left:2.4rem}.material-icons{font-style:normal}.redmine .button.easy-mindmup__icon:before,.redmine.mindmup__context_menu .easy-mindmup__icon:before{position:absolute;left:0;text-align:center;font-size:1.2em;line-height:1;color:inherit;top:50%;margin-top:-.5em}.redmine .mindmup-cont{margin:0 -10px -10px}.redmine .mindmup-menu{padding:5px 1px 0}.redmine .mindmup-menu .menu-item{padding:8px 10px}.redmine .mindmup__menu{background-color:#FFF;padding:14px}.redmine .mindmup__menu-group--tooltiped>a{padding-top:10px;padding-bottom:10px}.redmine .mindmup__menu-group--tooltiped>ul{position:absolute;background-color:#fff;border:1px solid #dfccaf;color:#42321a;padding:5px 5px 5px 0;margin-top:10px;margin-left:-10px;min-width:150px}.redmine .mindmup__menu-group--tooltiped>ul li{display:block;padding:3px}.redmine .mindmup__menu-group--tooltiped>ul li a{display:block;padding-left:20px}.mindmup__legend-container--hidden,.mindmup__legend-toggler .tip,.redmine .mindmup__menu-group--tooltiped:after{display:none}.redmine .mindmup__menu-group--tooltiped>ul li a:before{position:absolute;left:0;width:20px}.redmine .mindmup__menu-group--tooltiped .icon{background-image:none}.redmine .mindmup__menu-group--sizing a{padding:0}.redmine .mindmup__menu-item{padding-left:15px;padding-right:15px}.redmine .mindmup__menu-save a{padding-left:30px!important}.redmine .mindmup__menu-save a:before{text-align:center;width:36px;position:absolute}.redmine .mindmup__menu_addons{top:25px}.redmine .mindmup__menu_addons ul{padding:0}.redmine .mindmup-legend{margin-top:1px}.redmine .mindmup-legend__filter_cancel_icon{position:absolute}.redmine .mindmup__legend-container{top:0}.redmine .mindmup__legend-toggler{height:25px}.redmine .mindmup__legend-toggler a{line-height:1.5em}.redmine .mindmup__legend-header{padding:8px}.redmine a.button-positive{background-color:#4ebf67;border-radius:2px;border:1.3px solid #3aa051;color:#fff;padding:8px 16px}.redmine .menu-item.active{background-color:#9DB9D5}.redmine .menu-item.active a.button{color:#fff}.redmine .gravatar{max-width:100%;height:auto;border-radius:5000px;overflow:hidden;box-sizing:border-box}.redmine .button-2.active{background-color:transparent!important}.redmine .mindmup_modal__flash_close{color:#800;right:15px;line-height:18px}.mindmup-progress-modal{position:fixed;z-index:10000;width:40%;height:150px;top:150px;left:30%;right:30%;padding-top:30px;background-color:#fff;border:1px solid #d9d9d9;box-shadow:6px 6px 42px 7px rgba(217,217,217,.65)}.mindmup-progress-modal h3{text-align:center}.mindmup-progress-cont{height:8px;width:80%;margin-left:10%;margin-right:10%;border:1px solid #d9d9d9}.mindmup-progress-bar{width:50%;height:100%;background-color:green}.mindmup__legend-container{box-shadow:0 1px 4px rgba(0,0,0,.14),0 2px 2px rgba(0,0,0,.05);background-color:#fff;position:absolute;left:.8rem;top:.8rem;width:17.3333333333rem}@media only screen and (max-width:32rem){.mindmup__legend-container{top:110%}}.mindmup__legend-header{background:#f9f9f9;padding:.8rem}.mindmup__legend-toggler a{color:rgba(50,65,100,.5);font-size:1.5em;line-height:2.4rem}.mindmup__legend-toggler.active a:before{content:"\e5ce"!important}.mindmup__legend-body{overflow-y:auto;padding:.8rem 1.6rem;overflow-x:hidden}.mindmup-legend-color-box{background-color:#E0E0E0;border-color:#E0E0E0;width:1.6rem;height:1.6rem;display:inline-block;border-width:1px;border-style:solid;vertical-align:middle;margin-right:5px}.mindmup-legend-used{margin-top:10px;display:inline-block;color:rgba(66,50,26,.5)}.mindmup-legend-used-toggle{float:right;margin:5px 0}.mindmup-legend-used-all--used,.mindmup-legend-used-used--all{display:none}.mindmup-legend-item-cont{cursor:pointer;margin-top:.4rem}.mindmup-legend-item-cont .avatar-container{float:none}.mindmup-legend .hotkey_link{margin:.8rem -.8rem 0;padding-top:.8rem;border-top:1px solid #e5e5e5}.easy .mindmup-legend .hotkey_link a{color:rgba(50,65,100,.5)}.mindmup-legend__filter_cont{position:absolute;right:.8rem}.mindmup-legend__filter_icon{color:rgba(50,65,100,.4);font-size:2em;padding:5px}.mindmup-legend__filter_cancel_icon{position:absolute;display:none;color:#f33;font-size:2em;padding:0 5px 5px}.mindmup-legend__filter_cont:hover .mindmup-legend__filter_cancel_icon{display:inline}.mindmup-legend-drag-overlay{position:fixed;top:0;left:0;z-index:10000;cursor:move}.mindmup-legend-drag-avatar{position:absolute;z-index:5;height:20px;width:20px;margin-left:-12px;margin-top:-12px;border-width:1px;border-style:solid}.mapjs-connector{stroke:#ccc}.mapjs-draw-container{pointer-events:none;overflow:visible}.mapjs-link-hit{pointer-events:all;fill:none}.mapjs-exclamation{right:-.9em;position:absolute;top:-.9em;background-image:url(data:image/png;base64,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);width:2em;height:2em;background-size:2em;background-repeat:no-repeat no-repeat}.mapjs-link{stroke:#4ebf67!important}.mapjs-link[stroke-dasharray*='8']{stroke:#628DB6!important}.mapjs-arrow{fill:#4ebf67!important}.mapjs-node{border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background-color:#e5e5e5;color:#324164;margin:0;padding:0}.mapjs-node.activated{background-color:#ccc;border:1px solid transparent}.mapjs-node.activated,.mapjs-node.droppable,.mapjs-node.selected{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,.3);-moz-box-shadow:0 3px 3px 0 rgba(0,0,0,.3);box-shadow:0 3px 3px 0 rgba(0,0,0,.3);margin-left:-2px}.mapjs-node.droppable{border:1px solid #fa9b3c}.mapjs-node span{margin:.6rem 1.2rem;border:1px solid transparent}.mapjs-collapsor{position:absolute;left:auto;top:50%;right:-.9333333333rem;margin-top:-.8rem;background-color:#fff;color:red;line-height:1;font-size:1.6rem;font-weight:400}.mapjs-collapsor:before{content:"î…ś"}.collapsed .mapjs-collapsor{color:#4ebf67}.collapsed .mapjs-collapsor:before{content:"î…‡"}.mindmup-node-left .mapjs-collapsor{right:auto;left:-.9333333333rem}.mapjs-node:hover .mapjs-collapsor{font-size:2rem;margin-top:-.9333333333rem;right:-1.1333333333rem}.mindmup-node-left:hover .mapjs-collapsor{left:-1.1333333333rem;right:auto}.mindmup__node--filtered_out{opacity:.2}.mindmup-node-icon{height:1.3333333333rem;display:inline-block;margin-right:.1333333333rem;vertical-align:middle}.mindmup-node-icons{position:absolute;left:-.6666666667rem;top:-1.2rem;white-space:nowrap}.mindmup-node-icons-all{display:inline-block}.mindmup-node-icon-progress{width:.2666666667rem;background-color:#d9d9d9;margin:0 .8rem}.mindmup-node-icon-progress-bar{background-color:#4ebf67;position:relative}.mindmup-node-icon-milestone-shell{vertical-align:middle;display:inline-block}.mindmup-node-icon-milestone-diamond{border-width:1px;border-style:solid;width:8px;height:8px}.mindmup-node-icon-status{font-weight:400;opacity:.5;max-width:2rem;overflow:hidden;font-size:.89em}.mindmup-node-icon-avatar .gravatar{vertical-align:sub}.mindmup-node-icon-is_edited{margin-right:1px;margin-left:-10px;vertical-align:middle;border-radius:5000px;width:1.3333333333rem;height:1.3333333333rem;color:#fff;background-color:#e50026;line-height:1.2;font-size:1.2rem;text-align:center;display:inline-block}.mindmup-node--renaming .mindmup__node-control,.mindmup__print-area .mindmup-node-add-button{display:none}.mindmup-node-icon-is_edited:before{content:"!";font-family:inherit}.mindmup-node-filtered{opacity:.2}.mindmup-node-avatar{position:absolute;top:50%;margin-top:-.7333333333rem}.mindmup-node-left .mindmup-node-avatar{left:auto;right:-2.0666666667rem}.mindmup__node-control{color:#628DB6;background-color:transparent;position:absolute;bottom:100%;height:1rem;width:100%;line-height:1rem;margin-bottom:-.3333333333rem;-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,.1);-moz-box-shadow:0 3px 3px 0 rgba(0,0,0,.1);box-shadow:0 3px 3px 0 rgba(0,0,0,.1)}.mindmup__node-control .easy-mindmup__icon{width:50%;text-align:center;background-color:#f2f2f2}.mindmup__node-control .easy-mindmup__icon--edit{border-right:solid 1px #e5e5e5}.mindmup__node-control .easy-mindmup__icon--add{border-left:solid 1px #e5e5e5}.mindmup__node-control .easy-mindmup__icon:hover{background-color:#e5e5e5}.mindmup-scheme-project{background-color:#628DB6;color:#fff}.mindmup-scheme-project.activated{background-color:#49739c}.scheme-by-priority .scheme-priority-1{background-color:#fad1d1}.scheme-by-priority .scheme-priority-1.activated,.scheme-by-priority .scheme-priority-2{background-color:#f5a3a3}.scheme-by-priority .scheme-priority-2.activated{background-color:#f07575}.scheme-by-priority .scheme-priority-3{background-color:#d1fada}.scheme-by-priority .scheme-priority-3.activated,.scheme-by-priority .scheme-priority-4{background-color:#a3f5b5}.scheme-by-priority .scheme-priority-4.activated{background-color:#75f090}.scheme-by-priority .scheme-priority-5{background-color:#d1e6fa}.scheme-by-priority .scheme-priority-5.activated,.scheme-by-priority .scheme-priority-6{background-color:#a3cdf5}.scheme-by-priority .scheme-priority-6.activated{background-color:#75b4f0}.scheme-by-priority .scheme-priority-7{background-color:#fae6d1}.scheme-by-priority .scheme-priority-7.activated,.scheme-by-priority .scheme-priority-8{background-color:#f5cca3}.scheme-by-priority .scheme-priority-8.activated{background-color:#f0b375}.scheme-by-priority .scheme-priority-9{background-color:#add7f3}.scheme-by-priority .scheme-priority-9.activated{background-color:#81c1ec}.scheme-by-priority .scheme-priority-10{background-color:#e5e5e5}.scheme-by-priority .scheme-priority-10.activated{background-color:#ccc}.scheme-by-priority .scheme-priority-11{background-color:#fcc}.scheme-by-priority .scheme-priority-11.activated{background-color:#f99}.scheme-by-priority .scheme-priority-12{background-color:#ffb4b4}.scheme-by-priority .scheme-priority-12.activated{background-color:#ff8181}.scheme-by-status .scheme-status-1{background-color:#f9f6a8}.scheme-by-status .scheme-status-1.activated{background-color:#f6f178}.scheme-by-status .scheme-status-2{background-color:#fbe3bd}.scheme-by-status .scheme-status-2.activated{background-color:#f8cf8d}.scheme-by-status .scheme-status-3{background-color:#f3cccf}.scheme-by-status .scheme-status-3.activated{background-color:#e9a3a8}.scheme-by-status .scheme-status-4{background-color:#eabdec}.scheme-by-status .scheme-status-4.activated{background-color:#dd95e1}.scheme-by-status .scheme-status-5{background-color:#d0b8ec}.scheme-by-status .scheme-status-5.activated{background-color:#b590e1}.scheme-by-status .scheme-status-6{background-color:#c9c9f9}.scheme-by-status .scheme-status-6.activated{background-color:#9b9bf4}.scheme-by-status .scheme-status-7{background-color:#c9dcff}.scheme-by-status .scheme-status-7.activated{background-color:#96bbff}.scheme-by-status .scheme-status-8{background-color:#d2f4f0}.scheme-by-status .scheme-status-8.activated{background-color:#a9eae2}.scheme-by-status .scheme-status-9{background-color:#d2ecc9}.scheme-by-status .scheme-status-9.activated{background-color:#b3dfa3}.scheme-by-status .scheme-status-10{background-color:#e1ee9e}.scheme-by-status .scheme-status-10.activated{background-color:#d4e673}.scheme-by-status .scheme-status-11{background-color:#dadace}.scheme-by-status .scheme-status-11.activated{background-color:#c4c4b1}.scheme-by-status .scheme-status-12{background-color:#dbd1c7}.scheme-by-status .scheme-status-12.activated{background-color:#c7b8a8}.scheme-by-tracker .scheme-tracker-1{background-color:#f9f6a8}.scheme-by-tracker .scheme-tracker-1.activated{background-color:#f6f178}.scheme-by-tracker .scheme-tracker-2{background-color:#fbe3bd}.scheme-by-tracker .scheme-tracker-2.activated{background-color:#f8cf8d}.scheme-by-tracker .scheme-tracker-3{background-color:#f3cccf}.scheme-by-tracker .scheme-tracker-3.activated{background-color:#e9a3a8}.scheme-by-tracker .scheme-tracker-4{background-color:#eabdec}.scheme-by-tracker .scheme-tracker-4.activated{background-color:#dd95e1}.scheme-by-tracker .scheme-tracker-5{background-color:#d0b8ec}.scheme-by-tracker .scheme-tracker-5.activated{background-color:#b590e1}.scheme-by-tracker .scheme-tracker-6{background-color:#c9c9f9}.scheme-by-tracker .scheme-tracker-6.activated{background-color:#9b9bf4}.scheme-by-tracker .scheme-tracker-7{background-color:#c9dcff}.scheme-by-tracker .scheme-tracker-7.activated{background-color:#96bbff}.scheme-by-tracker .scheme-tracker-8{background-color:#d2f4f0}.scheme-by-tracker .scheme-tracker-8.activated{background-color:#a9eae2}.scheme-by-tracker .scheme-tracker-9{background-color:#d2ecc9}.scheme-by-tracker .scheme-tracker-9.activated{background-color:#b3dfa3}.scheme-by-tracker .scheme-tracker-10{background-color:#e1ee9e}.scheme-by-tracker .scheme-tracker-10.activated{background-color:#d4e673}.scheme-by-tracker .scheme-tracker-11{background-color:#dadace}.scheme-by-tracker .scheme-tracker-11.activated{background-color:#c4c4b1}.scheme-by-tracker .scheme-tracker-12{background-color:#dbd1c7}.scheme-by-tracker .scheme-tracker-12.activated{background-color:#c7b8a8}.scheme-by-assignee .scheme-assignee-1{background-color:#f9f6a8}.scheme-by-assignee .scheme-assignee-1.activated{background-color:#f6f178}.scheme-by-assignee .scheme-assignee-2{background-color:#fbe3bd}.scheme-by-assignee .scheme-assignee-2.activated{background-color:#f8cf8d}.scheme-by-assignee .scheme-assignee-3{background-color:#f3cccf}.scheme-by-assignee .scheme-assignee-3.activated{background-color:#e9a3a8}.scheme-by-assignee .scheme-assignee-4{background-color:#eabdec}.scheme-by-assignee .scheme-assignee-4.activated{background-color:#dd95e1}.scheme-by-assignee .scheme-assignee-5{background-color:#d0b8ec}.scheme-by-assignee .scheme-assignee-5.activated{background-color:#b590e1}.scheme-by-assignee .scheme-assignee-6{background-color:#c9c9f9}.scheme-by-assignee .scheme-assignee-6.activated{background-color:#9b9bf4}.scheme-by-assignee .scheme-assignee-7{background-color:#c9dcff}.scheme-by-assignee .scheme-assignee-7.activated{background-color:#96bbff}.scheme-by-assignee .scheme-assignee-8{background-color:#d2f4f0}.scheme-by-assignee .scheme-assignee-8.activated{background-color:#a9eae2}.scheme-by-assignee .scheme-assignee-9{background-color:#d2ecc9}.scheme-by-assignee .scheme-assignee-9.activated{background-color:#b3dfa3}.scheme-by-assignee .scheme-assignee-10{background-color:#e1ee9e}.scheme-by-assignee .scheme-assignee-10.activated{background-color:#d4e673}.scheme-by-assignee .scheme-assignee-11{background-color:#dadace}.scheme-by-assignee .scheme-assignee-11.activated{background-color:#c4c4b1}.scheme-by-assignee .scheme-assignee-12{background-color:#dbd1c7}.scheme-by-assignee .scheme-assignee-12.activated{background-color:#c7b8a8}.scheme-by-milestone .scheme-milestone-1{background-color:#f9f6a8}.scheme-by-milestone .scheme-milestone-1.activated{background-color:#f6f178}.scheme-by-milestone .scheme-milestone-2{background-color:#fbe3bd}.scheme-by-milestone .scheme-milestone-2.activated{background-color:#f8cf8d}.scheme-by-milestone .scheme-milestone-3{background-color:#f3cccf}.scheme-by-milestone .scheme-milestone-3.activated{background-color:#e9a3a8}.scheme-by-milestone .scheme-milestone-4{background-color:#eabdec}.scheme-by-milestone .scheme-milestone-4.activated{background-color:#dd95e1}.scheme-by-milestone .scheme-milestone-5{background-color:#d0b8ec}.scheme-by-milestone .scheme-milestone-5.activated{background-color:#b590e1}.scheme-by-milestone .scheme-milestone-6{background-color:#c9c9f9}.scheme-by-milestone .scheme-milestone-6.activated{background-color:#9b9bf4}.scheme-by-milestone .scheme-milestone-7{background-color:#c9dcff}.scheme-by-milestone .scheme-milestone-7.activated{background-color:#96bbff}.scheme-by-milestone .scheme-milestone-8{background-color:#d2f4f0}.scheme-by-milestone .scheme-milestone-8.activated{background-color:#a9eae2}.scheme-by-milestone .scheme-milestone-9{background-color:#d2ecc9}.scheme-by-milestone .scheme-milestone-9.activated{background-color:#b3dfa3}.scheme-by-milestone .scheme-milestone-10{background-color:#e1ee9e}.scheme-by-milestone .scheme-milestone-10.activated{background-color:#d4e673}.scheme-by-milestone .scheme-milestone-11{background-color:#dadace}.scheme-by-milestone .scheme-milestone-11.activated{background-color:#c4c4b1}.scheme-by-milestone .scheme-milestone-12{background-color:#dbd1c7}.scheme-by-milestone .scheme-milestone-12.activated{background-color:#c7b8a8}.scheme-by-progress .scheme-progress-1{background-color:#e5e5e5}.scheme-by-progress .scheme-progress-1.activated{background-color:#ccc}.scheme-by-progress .scheme-progress-2{background-color:#edf9f0}.scheme-by-progress .scheme-progress-2.activated{background-color:#c7ecd0}.scheme-by-progress .scheme-progress-3{background-color:#d3efd9}.scheme-by-progress .scheme-progress-3.activated{background-color:#aee1b9}.scheme-by-progress .scheme-progress-4{background-color:#afe2bb}.scheme-by-progress .scheme-progress-4.activated{background-color:#8ad49b}.scheme-by-progress .scheme-progress-5{background-color:#83d295}.scheme-by-progress .scheme-progress-5.activated{background-color:#5ec475}.scheme-by-progress .scheme-progress-6{background-color:#4ebf67}.scheme-by-progress .scheme-progress-6.activated{background-color:#3aa051}@media print{.gravatar{max-width:100%;height:auto;-webkit-border-radius:5000px;-moz-border-radius:5000px;border-radius:5000px}}.mindmup__print-area--compact{position:relative}.mindmup__print-area--stripped{border-left:2px solid #e6e6e6}.mindmup__print-area .mapjs-node{box-sizing:border-box}.mindmup__print-strip{position:relative;border:1px solid #e6e6e6;overflow:hidden;white-space:nowrap;break-inside:avoid;margin-left:-1px;display:inline-block;background-color:#fff;border-left:0}.mindmup-sidebar .mindmup-sidebar__coworkers--no_id,.mindmup-sidebar .mindmup-sidebar__empty-title{background-color:#ffa;border:1px solid #bfb23f;padding:5px;text-align:center}.mindmup-sidebar__empty-title{display:block;margin-top:10px;box-sizing:border-box}.mindmup-sidebar__resize{position:fixed;width:10px;text-align:center;cursor:col-resize;display:block;z-index:101;background-color:silver;box-sizing:border-box;padding-top:8px;font-size:20px;border-radius:5px}.mindmup-sidebar__root{display:block}.mindmup-sidebar__toggler{position:absolute;top:1px;right:1px;padding:0;z-index:1}.mindmup-sidebar__toggler.active{background:#d94838;color:#fff}.mindmup-sidebar__toggler.easy-mindmup__icon{padding-left:15px}.mindmup-sidebar__toggler.easy-mindmup__icon:before{width:auto}.mindmup-sidebar__container{border-left:1px solid #dfccaf;background:#f9f9f9;padding-left:8px;overflow-y:scroll}.mindmup-sidebar__long-text{position:relative;max-height:100px;overflow:hidden;min-height:30px;border:1px solid #dfccaf}.mindmup-sidebar__long-text__curtain{height:100%;position:absolute;width:100%;top:0;left:1px;border-bottom:1px solid #dfccaf;background:-moz-linear-gradient(top,rgba(255,255,255,.01) 0,rgba(255,255,255,.01) 65%,#fff 100%);background:-webkit-linear-gradient(top,rgba(255,255,255,.01) 0,rgba(255,255,255,.01) 65%,#fff 100%);background:linear-gradient(to bottom,rgba(255,255,255,.01) 0,rgba(255,255,255,.01) 65%,#fff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#03ffffff', endColorstr='#ffffff', GradientType=0)}.mindmup-sidebar__long-text__content{padding:3px;min-height:25px;width:100%;box-sizing:border-box;max-height:100px}.mindmup-sidebar__long-text__textarea{height:200px}.mindmup-sidebar__long-text--hidden{display:none}.mindmup-sidebar__attribute{display:block;overflow:hidden;margin-top:3px;position:relative}.mindmup-sidebar__attribute .invalid{border:1px solid red;color:red}.mindmup-sidebar__attribute-form-field{max-width:200px!important;padding:1px 1px 1px 5px!important;line-height:normal;float:right;overflow-x:hidden!important}.mindmup-sidebar__attribute-value{max-width:197px!important;width:100%;padding:1px 1px 1px 5px!important;line-height:normal;float:right;margin-top:6px}.mindmup-sidebar__attribute-label-disabled{color:rgba(0,0,0,.38)}.mindmup-sidebar__attribute-label{display:inline-block;width:100px;margin-top:5px}.mindmup-sidebar__attribute .spaceholder{display:none}.mindmup-sidebar__attribute .top-section{margin-bottom:0}.mindmup-sidebar__attribute-full-screen-icon{display:block;float:right;cursor:pointer;margin-top:6px;position:absolute;top:0;right:0}.mindmup-sidebar__attribute .icon-edit{position:absolute;top:0;right:0;cursor:pointer}.mindmup-sidebar__closed_button{width:200px}.mindmup-sidebar__add-comment-button{margin:3px 0 12px}.mindmup-sidebar__add-comment-button a{width:100%;box-sizing:border-box}.mindmup-sidebar__journal-avatar-container{margin-top:6px}.mindmup-sidebar__journal-header{margin-top:-6px}.mindmup-sidebar__journal-notes{margin-top:0;background:#fff;border:1px solid #dfccaf;margin-bottom:0!important;box-sizing:border-box;padding:3px}.mindmup-sidebar__journal-notes p:last-child{margin-bottom:0}.mindmup-sidebar__journal-full-screen-icon{display:block;float:right;cursor:pointer;margin-top:25px;position:absolute;top:0;right:20px}.mindmup-sidebar__journal-timestamp{font-weight:700;font-size:smaller}.mindmup-sidebar__tab{margin-left:1px;margin-right:1px}.mindmup-sidebar__tab header{min-height:36px!important;padding:0 12px 8px!important}.mindmup-sidebar__tab header.open{min-height:44px}.mindmup-sidebar__tab header:not(:hover){color:inherit!important;background-color:unset!important}.mindmup-sidebar__input__name{color:#000;font-family:"Open Sans",sans-serif;width:100%!important}.mindmup-sidebar__input__name .input{max-width:none;text-align:center;font-weight:700;opacity:1;color:#000!important}.mindmup-sidebar__input__name .bottom-section{display:none!important}.mindmup-sidebar__input__name .baseline{background-color:#fff}.mindmup-sidebar .gravatar{max-height:32px;max-width:32px}.mindmup-sidebar .content-wrapper{margin:0 12px 8px!important}.mindmup-sidebar .material-tab{padding:0!important}@font-face{font-family:"Material Icons";src:local("Material Icons"),local("MaterialIcons-Regular"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.eot?#iefix) format("embedded-opentype"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.woff) format("woff"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.woff2) format("woff2"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.ttf) format("truetype"),url(../../../easy_mindmup/fonts/EasyMaterialIcons-Regular.svg) format("svg");font-weight:400;font-style:normal}.mindmup-sidebar__attachments-thumbnails>img{display:none}.mindmup-sidebar__attachments-thumbnails{margin:0} \ No newline at end of file diff --git a/plugins/easy_wbs/config/locales/hu.yml b/plugins/easy_wbs/config/locales/hu.yml index d0309d9..c5a661d 100644 --- a/plugins/easy_wbs/config/locales/hu.yml +++ b/plugins/easy_wbs/config/locales/hu.yml @@ -4,11 +4,10 @@ hu: name: easy_wbs_easy_issue_query: Easy WBS easy_wbs: - button_actions: EgyĂ©b műveletek - button_add_comment: Ăšj hozzászĂłlás + button_actions: Más tevĂ©kenysĂ©gek button_add_child: Alfeladat hozzáadása button_add_parent: SzĂĽlĹ‘ feladat hozzáadása - button_add_sibling: Feladat hozzáadása + button_add_sibling: TestvĂ©rfeladat hozzáadása button_all_icons: Ikonok button_collapse: Ă–sszezár button_collapse_all: Ă–sszes összezárása @@ -18,22 +17,21 @@ hu: button_expand: Kinyit button_expand_all: Ă–sszes kinyitása button_expand_collapse: Kinyit / Ă–sszezár - button_legend: Jelmagyarázat + button_legend: Magyarázat button_one_side: Egy oldal button_paste: BeillesztĂ©s button_project_menu: Easy WBS button_redo: VisszaállĂt - button_remove_comment: HozzászĂłlás eltávolĂtása button_remove_node: Ăg eltávolĂtása - button_show_links: Kapcsolatok megjelenĂtĂ©se + button_show_links: Linkek megjelenĂtĂ©se button_undo: Visszavonás edit_issue: Feladat szerkesztĂ©se - error_create: lĂ©trehozás sikertelen + error_create: nem lehet lĂ©trehozni error_delete: nem lehet törölni error_update: FrissĂtĂ©s nem sikerĂĽlt errors: - not_subtaskable: 'Ez a feladat: "%{task_name}" nem lehet alfeladat a tĂpus beállĂtása - miatt' + not_subtaskable: Feladat "%{task_name}" nem lehet alfeladat a beállĂtás tĂpusa + miatt free: button_upgrade: Szerezze be a teljes verziĂłt button_upgrade_href: https://www.easyproject.hu/szoftver @@ -41,31 +39,31 @@ hu: feature_context_menu: Ăg tulajdonságának megváltoztatása feature_filtering: Ăgak szűrĂ©se tulajdonság alapján header_not_available: Csak teljes verziĂłban Ă©rhetĹ‘ el - text_not_available: csak az Easy WBS teljes verziĂłjában elĂ©rhetĹ‘ + text_not_available: az Easy WBS teljes verziĂłja elĂ©rhetĹ‘ hotkeys: info_mac_metakey: Mac OSX esetĂ©n a Ctrl Ă©s Cmd billentyűkombináciĂł használhatĂł olyan rövidĂtĂ©sekre melyeket Ctrl-er jelölĂĽnk- nĂ©hány böngĂ©szĹ‘ nem engedĂ©lyezi ezeket a kombináciĂłkat. EzĂ©rt pĂ©ldául, ha a Cmd + Space nem működik a böngĂ©szĹ‘jĂ©ben, akkor prĂłbálja meg a Ctrl+ Space kombináciĂłt keyboard: - - title: Elemek mĂłdosĂtása + - title: ág manipulálás hotkeys: - hotkey: Enter - info: Feladat hozzáadása + info: TestvĂ©r hozzáadása - hotkey: Shift+Enter - info: Feladat hozzáadása fölĂ© vagy sortörĂ©ssel (elem átnevezĂ©sekor) + info: TestvĂ©r hozzáadása fölĂ© vagy sortörĂ©ssel (ág átnevezĂ©sekor) - hotkey: Tab vagy BeillesztĂ©s info: alfeladat hozzáadása - hotkey: Shift+Tab - info: SzĂĽlĹ‘feladat hozzáadása + info: SzĂĽlĹ‘ beillesztĂ©se - hotkey: Space - info: Elem átnevezĂ©se + info: Ăg átnevezĂ©se - hotkey: Shift+Space - info: Elem adatainak szerkesztĂ©se + info: Ăg adat szerkesztĂ©se - hotkey: Backspace vagy TörlĂ©s - info: Elem eltávolĂtása + info: Ăg eltávolĂtása - hotkey: Ctrl+Fel/Le - info: Elem elmozdĂtása fel/le + info: Ăg elmozdĂtása fel/le - title: SzerkesztĂ©s hotkeys: - hotkey: Ctrl+S @@ -77,42 +75,40 @@ hu: - hotkey: Ctrl+V vagy P info: BeillesztĂ©s - hotkey: U vagy Ctrl+Z - info: VisszaállĂtás + info: VisszaállĂt - hotkey: R vagy Ctrl+Y vagy Ctrl+Shift+Z info: Redo - title: Kiválasztás hotkeys: - - hotkey: Kurzor billentyűk - info: A jelenleg kiválaszott elemhez kĂ©pest válassza ki a fel/le/bal/jobb - irányban lĂ©vĹ‘t - - hotkey: Shift + Kurzor billentyűk - info: Elem hozzáadása fel/le/bal/jobb irányban ( hasznos a feladat többszörös - kiválasztására) + - hotkey: Nyilak + info: A jelenleg kiválaszott ágbĂłl válassza ki a fel/le/bal/jobb + - hotkey: Shift + Nyilak + info: Ăg hozzáadása fel/le/bel/jobb ( hasznos a testvĂ©r többszörös kiválasztása) - hotkey: "{" - info: Jelenlegi elem Ă©s az alatta lĂ©vĹ‘ fastruktĂşra elemeinek kiválasztása + info: Többszörös kiválasztása a jelenlegi ágnak Ă©s a maradĂ© alfának alatta - hotkey: "[" - info: Jelenlegi elem alatt lĂ©vĹ‘ fastruktĂşra elemeinek kiválasztása(maga - az elem nem kerĂĽl kijelölĂ©sre) + info: Többszörös kiválasztása az alfának amelyik a jelenlegi ág alatt van + (nem az ágat magát) - hotkey: "=" - info: A jelenlegi elemmel egy szinten lĂ©vĹ‘ elemek egyĂĽttes kiválasztása - (amelyek azonos szĂĽlĹ‘vel rendelkeznek) + info: A jelenlegi ág testvĂ©reinek többszörös kiválasztása ( amelyek ugyanazza + a szĂĽlĹ‘vel rendelkeznek) - hotkey: "." - info: Többszörös kiválasztás törlĂ©se Ă©s válassza ki a jelenlegi elemet ismĂ©t + info: Többszörös kiválasztás törĂ©lese Ă©s válassza ki a jelenlegi ágat ismĂ©t - hotkey: 1 - 9 - info: 'Egy kiválasztott szinten az összes elem kiválasztása (pl. 1: az összes - elsĹ‘ szintű elem kiválasztása)' + info: Egy kiválaszott szinten az összes ág kiválasztása (pl. 1 az összes + eslĹ‘ szintes ágak kiválasztása) - title: NavigáciĂł Ă©s kĂ©pernyĹ‘ hotkeys: - hotkey: "/ vagy F" - info: Elem kibontása vagy összezárása (kibontja vagy bezárja az alfeladatokat + info: Ăg kinyitása vagy összezárása (kinyitja vagy bezárja az alfeladatot ) - hotkey: Ctrl + vagy Z info: NagyĂtás - hotkey: Ctrl - vagy Shift Z - info: KicsinyĂtĂ©s + info: Kizoomolás - hotkey: Esc, 0, Ctrl+0 - info: TĂ©rkĂ©p nĂ©zet alaphelyzetbe állĂtása - GyökĂ©relem kiválasztása Ă©s a - kĂ©pernyĹ‘ közepĂ©re hozása + info: TĂ©rkĂ©p nĂ©zet ĂşjraindĂtása - GyökĂ©rág kiválasztása Ă©s a kĂ©pernyĹ‘ közepĂ©re + hozása mouse: - action: TĂ©rkĂ©p áthelyezĂ©se gesture: kattintás Ă©s behĂşzás a központi ágba, kattintás Ă©s behĂşzás a háttĂ©r @@ -140,37 +136,32 @@ hu: - action: Nyitott feladatok vagy projekt kĂĽlönbözĹ‘ ablakban gesture: Alt+click title_key_shortcuts: Billentyű műveletek - title_mouse_shortcuts: EgĂ©r műveletek - title_shortcuts: Gyorsparancsok + title_mouse_shortcuts: EgĂ©r műveltek + title_shortcuts: LerövidĂtĂ©sek info_all_saved: Sikeresen mentve info_any_failed: KĂ©rĂ©sek egy rĂ©sze sikertelen info_no_permission: Nem rendelkezik a szĂĽksĂ©ges jogosultsággal a művelet vĂ©grehajtásához - label_additional_data: További adatok - label_all: Mind - label_basic_data: Alap adatok - label_color_by: SzĂnkĂłdolás szempontja - label_go_to: Ugrás feladathoz + label_color_by: SzĂnezve + label_go_to: Megy! label_or: vagy last_state_modal: label_differencies: KĂĽlönbsĂ©gek a szerveren message_changed: kĂĽlönbözĹ‘ attribĂştumokkal rendelkezik (böngĂ©szĹ‘ => server ({{változások}}) message_missing: hiányzik a szĂĽlĹ‘ feladatbĂłl "{{from}}" message_moved: alfeladata "{{to}}", nem "{{from}}" - message_present: 'alfeladatakĂ©nt jelenik meg az alábbi feladatnak: "{{to}}"' + message_present: alfeladatkĂ©nt kelenik meg a "{{to}}"-nak text_reload_appeal: SzeretnĂ© Ăşjratölteni az állapotot a szerverrĹ‘l? title: A WBS utolsĂł kliens állapota kĂĽlönbözik a szerver állapottĂłl - name_an_entity_first: ElĹ‘ször adjon egy nevet reload_modal: label_errors: Hibák text_reload_appeal: SzeretnĂ© figyelmen kĂvĂĽl hagyni a nem mentett elemeket Ă©s - Ăşjratölteni a szerverrĹ‘l? + Ăşjratölteni a serverrĹ‘l? title: Nem sikerĂĽlt megfelelĹ‘en menteni a WBS-t stored_modal: text_load_appeal: SzeretnĂ© betölteni? A friss állapot helyett a szerverrĹ‘l? title: Nem mentett állapotot talált - text_issue_not_saved: Feladat mĂ©g nincs mentve. ElĹ‘ször mentsen. - warning_delete_node: Biztosan törölni akarja a(z) {{name}} elemet Ă©s annak alsĂłbb - szintű elemeit? + warning_delete_node: Biztosan törölni akarja a {{name}} csomĂłpontot Ă©s minden + származĂ©kát? warning_not_saved: nem megfelelĹ‘ mentĂ©s heading_easy_wbs_issues: Easy WBS label_filter_group_easy_wbs_easy_issue_query: Feladatok @@ -178,4 +169,3 @@ hu: easy_issue_query: Feladatok project_default_page: easy_wbs: Easy WBS - project_module_easy_wbs: Easy WBS diff --git a/plugins/easy_wbs/config/locales/ja.yml b/plugins/easy_wbs/config/locales/ja.yml index 82947d5..55af76a 100644 --- a/plugins/easy_wbs/config/locales/ja.yml +++ b/plugins/easy_wbs/config/locales/ja.yml @@ -4,35 +4,31 @@ ja: name: easy_wbs_easy_issue_query: Easy WBS easy_wbs: - button_actions: ăťă®ä»–ă®ć“Ťä˝ś - button_add_comment: ă‚łăˇăłăă‚’čż˝ĺŠ ă™ă‚‹ - button_add_child: ĺăŽăĽă‰ă®čż˝ĺŠ - button_add_parent: 親ăŽăĽă‰ă®čż˝ĺŠ - button_add_sibling: 兄弟ăŽăĽă‰ă®čż˝ĺŠ + button_add_child: ĺă‚’čż˝ĺŠ + button_add_parent: č¦Şă‚’čż˝ĺŠ + button_add_sibling: ĺ…„ĺĽźă‚’čż˝ĺŠ button_all_icons: アイコ㳠button_collapse: ćŠă‚Šăźăźăż - button_collapse_all: ă™ăąă¦ćŠă‚Šăźăźăż + button_collapse_all: ă™ăąă¦ĺ±•é–‹ button_cut: ĺ‡ă‚ŠĺŹ–ă‚Š - button_display: 画面ă®čˇ¨ç¤şč¨ĺ®š - button_edit_data: ă‡ăĽă‚żă®ç·¨é›† + button_edit_data: ăŽăĽă‰ă®č©łç´°ă‚’編集 button_expand: 展開 button_expand_all: ă™ăąă¦ĺ±•é–‹ button_expand_collapse: 展開/ćŠă‚Šăźăźăż - button_legend: 凡例 + button_legend: ĺ¤äľ‹ button_one_side: ç‰‡ĺ´ button_paste: 貼りä»ă‘ - button_project_menu: Easy WBS + button_project_menu: WBS button_redo: ă‚„ă‚Šç›´ă— - button_remove_comment: ă‚łăˇăłăを削除ă™ă‚‹ button_remove_node: ăŽăĽă‰ă‚’削除 button_show_links: ăŞăłă‚Żă‚’表示 button_undo: ĺ…ă«ć»ă™ - edit_issue: ă‚żă‚ąă‚Żă®ç·¨é›† + edit_issue: ăケăăを編集 error_create: 作ćă§ăŤăľă›ă‚“ă§ă—ăź error_delete: 削除ă§ăŤăľă›ă‚“ă§ă—ăź error_update: ć›´ć–°ă§ăŤăľă›ă‚“ă§ă—ăź errors: - not_subtaskable: ăă©ăă‚«ăĽă®č¨ĺ®šă®ăźă‚ă€ă‚żă‚ąă‚Ż "%{task_name}" サă–ă‚żă‚ąă‚Żă«ă™ă‚‹ă“ă¨ăŻă§ăŤăľă›ă‚“ + not_subtaskable: "%{task_name} ăŻĺăケăăă«ă§ăŤăľă›ă‚“" free: button_upgrade: ă•ă«ăăĽă‚¸ă§ăłă‚’入手 button_upgrade_href: https://www.easyredmine.com/redmine-wbs-plugin @@ -40,9 +36,9 @@ ja: feature_context_menu: ăŽăĽă‰ă®ă—ăă‘ă†ă‚Łă‚’変更 feature_filtering: ă—ăă‘ă†ă‚ŁćŻŽă«ăŽăĽă‰ă‚’ă•ă‚Łă«ă‚ż header_not_available: ă•ă«ăăĽă‚¸ă§ăłă§ă®ăżĺ…Ąć‰‹ĺŹŻč˝ - text_not_available: 'Easy WBSă®ă•ă«ăăĽă‚¸ă§ăłă§ă®ăżĺ©ç”¨ĺŹŻč˝' + text_not_available: 'WBSă—ă©ă‚°ă‚¤ăłă®ă•ă«ăăĽă‚¸ă§ăłă§ĺ…Ąć‰‹ĺŹŻč˝:' hotkeys: - info_mac_metakey: Mac OS Xă®ĺ ´ĺă€Controlă‚ăĽă¨Commandă‚ăĽăŻă€ä»Ąä¸‹ă§Ctrlă¨čˇ¨ç¤şă—ă¦ă„ă‚‹ă‚·ă§ăĽăă‚«ăăă«ä˝żç”¨ĺŹŻč˝ă§ă™ă€‚ă—ă‹ă—ă–ă©ă‚¦ă‚¶ă«ă‚ăŁă¦ăŻă€ç„ˇĺŠąăŞă‚ăĽă®çµ„ăżĺă‚Źă›ăŚă‚ă‚Šăľă™ă€‚ăťă®ăźă‚ă€äľ‹ăă°Command+SpaceăŚĺŠąă‹ăŞă„ĺ ´ĺăŻă€Control+Spaceを試ă—ă¦ăŹă ă•ă„。 + info_mac_metakey: Mac OS Xă®ĺ ´ĺă€Controlă‚ăĽă¨Commandă‚ăĽăŻă€ä»Ąä¸‹ă§Ctrlă¨čˇ¨ç¤şă—ă¦ă„ă‚‹ă‚·ă§ăĽăă‚«ăăă§ä˝żç”¨ĺŹŻč˝ă§ă™ă€‚ă—ă‹ă—ă–ă©ă‚¦ă‚¶ă«ă‚ăŁă¦ăŻă€ç„ˇĺŠąăŞă‚ăĽă®çµ„ăżĺă‚Źă›ăŚă‚ă‚Šăľă™ă€‚ăťă®ăźă‚ă€äľ‹ăă°Command+SpaceăŚĺŠąă‹ăŞă„ĺ ´ĺăŻă€Control+Spaceを試ă—ă¦ăŹă ă•ă„。 keyboard: - title: ăŽăĽă‰ć“Ťä˝ś hotkeys: @@ -79,9 +75,9 @@ ja: - title: ăŽăĽă‰é¸ćŠž hotkeys: - hotkey: 矢印ă‚㼠- info: 現在é¸ćŠžä¸ă®ăŽăĽă‰ă®ä¸Šä¸‹ĺ·¦ĺŹłă®ăŽăĽă‰ă‚’é¸ćŠžă™ă‚‹ + info: ăŽăĽă‰ă®é¸ćŠžă‚’ă€ä¸Šä¸‹ĺ·¦ĺŹłă«ç§»ĺ‹• - hotkey: Shift+矢印ă‚㼠- info: 現在é¸ćŠžă—ă¦ă„ă‚‹ăŽăĽă‰ă«ä¸Šä¸‹ĺ·¦ĺŹłă®ăŽăĽă‰ă‚’čż˝ĺŠ ďĽĺ…„弟ăŽăĽă‰ă‚’複数é¸ćŠžă™ă‚‹ă¨ăŤă«äľżĺ©ă§ă™ďĽ‰ + info: ăŽăĽă‰ă®é¸ćŠžçŻ„囲をă€ä¸Šä¸‹ĺ·¦ĺŹłă«ć‹ˇĺ¤§ďĽĺ…„弟を複数é¸ćŠžă™ă‚‹ă¨ăŤă«äľżĺ©ă§ă™ďĽ‰ - hotkey: "{" info: é¸ćŠžä¸ăŽăĽă‰ă¨ă€ăťă®ĺă™ăąă¦ă‚’é¸ćŠž - hotkey: "[" @@ -92,34 +88,34 @@ ja: info: 複数é¸ćŠžă‚’解除ă—ă€çŹľĺś¨ă®ăŽăĽă‰ă ă‘ă‚’é¸ćŠž - hotkey: 1 - 9 info: 指定ă•ă‚ŚăźéšŽĺ±¤ă®ăŽăĽă‰ă‚’ă™ăąă¦é¸ćŠžďĽ1ă®ĺ ´ĺă€ç¬¬ä¸€éšŽĺ±¤ă®ăŽăĽă‰ăŚă™ăąă¦é¸ćŠžă•ă‚Śăľă™ďĽ‰ - - title: ăŠă“ゲăĽă‚·ă§ăłă¨ç”»éť˘ + - title: 表示 hotkeys: - hotkey: "/ ăľăźăŻ F" - info: ăŽăĽă‰ă®ĺ±•é–‹ďĽŹćŠă‚ŠăźăźăżďĽĺăŽăĽă‰ă‚’ă•ă‚©ă«ă€ăĽă«ĺ…Ąă‚Śă‚‹/ă‹ă‚‰ĺ‡şă™ďĽ‰ + info: ăŽăĽă‰ă®ĺ±•é–‹ďĽŹćŠă‚Šăźăźăż - hotkey: Ctrl+(+) ăľăźăŻ Z - info: ă‚şăĽă イ㳠+ info: 拡大 - hotkey: Ctrl+(-) ăľăźăŻ Shift+Z - info: ă‚şăĽă アウă + info: 縮小 - hotkey: Esc ăľăźăŻ 0 ăľăźăŻ Ctrl+0 - info: ăžăă—表示ă®ăŞă‚»ăă - ă«ăĽăăŽăĽă‰ă‚’画面ä¸ĺ¤®ă«ç§»ĺ‹•ă™ă‚‹ + info: ăžăă—表示をĺ…ă«ć»ă™ - ă«ăĽăăŽăĽă‰ă‚’画面ä¸ĺ¤®ă«ç•°ĺ‹•ă—ăľă™ mouse: - action: ăžăă—を移動 - gesture: ä¸ĺ¤®ă®ăŽăĽă‰ă‚’ă‚ŻăŞăă‚Żă€čŚć™Żă‚’ă‚ŻăŞăă‚Żă»ă‚˘ăłă‰ă»ă‰ă©ăă‚°ă€ăă©ăă‚Żă‘ăă‰/ă‚żăăă‘ăă‰ă‚’ă‚ąă‚ŻăăĽă«ă™ă‚‹ + gesture: ă«ăĽăăŽăĽă‰ă‚’é¸ćŠžă—ă¦ă‰ă©ăă‚° - action: ăŽăĽă‰é¸ćŠž - gesture: ăŽăĽă‰ă‚’ă‚ŻăŞăă‚Żă€ăľăźăŻă‚żăă—ă™ă‚‹ + gesture: ăŽăĽă‰ă‚’ă‚ŻăŞăă‚Ż - action: ăŽăĽă‰ă®č¤‡ć•°é¸ćŠž gesture: Shift+ă‚ŻăŞăă‚Ż - action: ăŽăĽă‰ă®ä¸¦ăąć›żă - gesture: 兄弟ăŽăĽă‰ă‚’ć°´ĺąłć–ąĺ‘ă«ä¸¦ăąć›żă位置ă«čż‘ă„ă¨ă“ă‚Ťčż„ă‰ă©ăă‚°ă™ă‚‹ă€‚並ăąć›żăä¸ă«ăŻé»’ă„矢印ăŚčˇ¨ç¤şă•ă‚Śă‚‹ + gesture: ăŽăĽă‰ă‚’ă‚‚ăŁă¦ă„ăŤăźă„ă¨ă“ă‚Ťăľă§ă‰ă©ăă‚°ă—ă¦ă€é»’ă„矢印ăŚčˇ¨ç¤şă•ă‚Śăźă‚‰é›˘ă™ - action: ăŽăĽă‰ă®ä˝Ťç˝®ă‚’手動ă§čŞżć•´ - gesture: é»’ă„矢印ă®čˇ¨ç¤şăŚć¶ăă‚‹ă¨ă“ă‚Ťăľă§ă€ăŽăĽă‰ă‚’ă‰ă©ăă‚°ă—ă¦ăŹă ă•ă„。並ăąć›żăă®é»’ă„矢印ăŚčˇ¨ç¤şă•ă‚Śă¦ă„る時ă«ĺĽ·ĺ¶çš„ă«ć‰‹ĺ‹•ă§ä˝Ťç˝®ă‚’決ă‚ă‚‹ă«ăŻă€Shiftを押ă—ăŞăŚă‚‰ă‰ă©ăă‚°ă—ă¦ăŹă ă•ă„。注意:第一階層ă®ăŽăĽă‰ăŻă©ă®ć–ąĺ‘ă«ă‚‚自由ă«é…Ťç˝®ă§ăŤăľă™ăŚă€ăťă‚Śă‚り下位階層ă®ăŽăĽă‰ăŻă€ă«ăĽăă«ĺŻľă—ă¦č¦Şă®ĺ‘ăŤă«ă—ă‹é…Ťç˝®ă§ăŤăľă›ă‚“。 + gesture: ăŽăĽă‰ă‚’ă‚‚ăŁă¦ă„ăŤăźă„ă¨ă“ă‚Ťăľă§ă‰ă©ăă‚°ă—ă¦é›˘ă™ďĽé»’ă„矢印ăŚčˇ¨ç¤şă•ă‚Śă¦ă„ăŞă„ă“ă¨ă‚’確認ă—ă¦ăŹă ă•ă„)。黒ă„矢印ăŚčˇ¨ç¤şă•ă‚Śă‚‹ĺ ´ć‰€ă«ç§»ĺ‹•ă—ăźă„ĺ ´ĺăŻă€Shiftを押ă—ăŞăŚă‚‰ă‰ă©ăă‚°ă—ă¦ăŹă ă•ă„。注意:第一階層ă®ăŽăĽă‰ăŻă©ă®ć–ąĺ‘ă«ă‚‚自由ă«é…Ťç˝®ă§ăŤăľă™ăŚă€ăťă‚Śă‚り下位階層ă®ăŽăĽă‰ăŻă€ă«ăĽă⇒親ă®ĺ‘ăŤă«ă—ă‹é…Ťç˝®ă§ăŤăľă›ă‚“。 - action: ăŽăĽă‰ĺŤă®ĺ¤‰ć›´ - gesture: ă€ă–ă«ă‚ŻăŞăă‚ŻăľăźăŻă€ă–ă«ă‚żăă— - - action: 操作ă«é–˘ă™ă‚‹ă‚łăłă†ă‚ă‚ąăă»ăˇă‹ăĄăĽă‚’表示ă™ă‚‹ - gesture: ăŽăĽă‰ă‚’右クăŞăă‚Żă™ă‚‹ďĽăžă‚¦ă‚ąă§ďĽ‰ă€ăľăźăŻčŚć™Żă‚’ă€ă–ă«ă‚żăă—ă™ă‚‹ă€ăľăźăŻďĽă‚żăăă‘ăŤă«ä¸Šă«ďĽ‰čˇ¨ç¤şă™ă‚‹ + gesture: ă€ă–ă«ă‚ŻăŞăă‚Ż + - action: ă‚łăłă†ă‚ă‚ąăăˇă‹ăĄăĽă‚’表示 + gesture: ăŽăĽă‰ă‚’右クăŞăă‚Ż - action: ăŽăĽă‰ă®č¦Şă‚’変更 - gesture: ă‰ă©ăグ&ă‰ăăă—ďĽç„ˇé™ă«ăĽă—ă¨ăŞă‚‹é–˘äż‚ă«ăŻă§ăŤăľă›ă‚“) - - action: ă‚żă‚ąă‚Ż/ă—ăジェクăă‚’ĺĄă‚¦ă‚Łăłă‰ă‚¦ă§čˇ¨ç¤ş + gesture: ă‰ă©ăグ&ă‰ăăă—ďĽĺľŞç’°ăŞăłă‚Żă¨ăŞă‚‹é–˘äż‚ă«ăŻă§ăŤăľă›ă‚“) + - action: ăケăă/ă—ăジェクăă‚’ĺĄă‚¦ă‚Łăłă‰ă‚¦ă§čˇ¨ç¤ş gesture: Alt+ă‚ŻăŞăă‚Ż title_key_shortcuts: ă‚ăĽăśăĽă‰ć“Ťä˝ś title_mouse_shortcuts: ăžă‚¦ă‚ąă¨ă‚żăă操作 @@ -127,9 +123,6 @@ ja: info_all_saved: ă™ăąă¦ćŁă—ăŹäżťĺă•ă‚Śăľă—ăź info_any_failed: ă„ăŹă¤ă‹ă®č¦ć±‚ă«ĺ¤±ć•—ă—ăľă—ăźďĽš info_no_permission: ăťă®ć“Ťä˝śă‚’ă™ă‚‹ć¨©é™ăŚă‚ă‚Šăľă›ă‚“ - label_additional_data: čż˝ĺŠ ă‡ăĽă‚ż - label_all: ă™ăąă¦ - label_basic_data: 基本ă‡ăĽă‚ż label_color_by: 色ă®ĺˇ—ă‚Šĺ†ă‘ label_go_to: '次ă¸:' label_or: ăľăźăŻ @@ -140,22 +133,19 @@ ja: message_moved: 㯠{from} ă§ăŻăŞăŹ {to} ă®ĺăŽăĽă‰ă§ă™ message_present: 㯠{to} ă®ĺăŽăĽă‰ă§ă™ text_reload_appeal: サăĽăă‹ă‚‰ă‚ąă†ăĽă‚żă‚ąă‚’再čŞčľĽăżă—ăľă™ă‹ďĽź - title: 最新ă®WBSă‚Żă©ă‚¤ă‚˘ăłăă»ă‡ăĽă‚żă»ă‚ąă†ăĽă‚żă‚ąăŚă‚µăĽăăĽă»ă§ăĽăźă»ă‚ąă†ăĽă‚żă‚ąă¨ç•°ăŞă‚Šăľă™ - name_an_entity_first: ăľăšăŻăă‚ă«ă‚¨ăłă†ă‚Łă†ă‚ŁăĽă®ĺŤĺ‰Ťă‚’ă¤ă‘ă‚‹ + title: WBSă®ă‚Żă©ă‚¤ă‚˘ăłăă®ă‚ąă†ăĽă‚żă‚ąăŚă‚µăĽăă®ă‡ăĽă‚żă¨ç•°ăŞă‚Šăľă™ reload_modal: label_errors: エă©ăĽ text_reload_appeal: äżťĺă•ă‚Śă¦ă„ăŞă„ĺ¤‰ć›´ă‚’ç ´ćŁ„ă—ă¦ă€ă‚µăĽăă®ă‡ăĽă‚żă‚’再čŞăżčľĽăżă—ăľă™ă‹ďĽź title: WBSă‚’ćŁă—ăŹäżťĺă§ăŤăľă›ă‚“ă§ă—ăź stored_modal: - text_load_appeal: サăĽăăĽă‹ă‚‰ă®ćś€ć–°ă‡ăĽă‚żă®ä»Łă‚Źă‚Šă«ă“ă‚Śă‚’čŞăżčľĽăżăľă™ă‹ďĽź + text_load_appeal: サăĽăă‹ă‚‰ćś€ć–°ă®ă‡ăĽă‚żă‚’čŞăżčľĽăżăľă™ă‹ďĽź title: äżťĺă•ă‚Śă¦ă„ăŞă„ă‚ąă†ăĽă‚żă‚ąăŚč¦‹ă¤ă‹ă‚Šăľă—ăź - text_issue_not_saved: 懸ćˇäş‹é …ăŚäżťĺă•ă‚Śă¦ă„ăľă›ă‚“。ăľăšäżťĺă—ă¦ăŹă ă•ă„。 warning_delete_node: 本当㫠{{name}} ă¨ăťă®ĺĺ«ăケăăを削除ă—ă¦ă‚‚ă‚ă‚Ťă—ă„ă§ă™ă‹ďĽź warning_not_saved: ăŻćŁă—ăŹäżťĺă•ă‚Śăľă›ă‚“ heading_easy_wbs_issues: Easy WBS label_filter_group_easy_wbs_easy_issue_query: ă‚żă‚ąă‚Ż label_filter_group_easy_wbs: - easy_issue_query: ă‚żă‚ąă‚Ż + easy_issue_query: ăケăă project_default_page: easy_wbs: Easy WBS - project_module_easy_wbs: Easy WBS diff --git a/plugins/easy_wbs/init.rb b/plugins/easy_wbs/init.rb index a7a0195..2f8159a 100644 --- a/plugins/easy_wbs/init.rb +++ b/plugins/easy_wbs/init.rb @@ -2,11 +2,11 @@ Redmine::Plugin.register :easy_wbs do name 'Easy WBS plugin' author 'Easy Software Ltd' description 'new WBS tree hierarchy generator' - version '1.9' + version '1.5' url 'www.easyredmine.com' author_url 'www.easysoftware.cz' - requires_redmine_plugin :easy_mindmup, version_or_higher: '1.4' + requires_redmine_plugin :easy_mindmup, version_or_higher: '1.0' if Redmine::Plugin.installed?(:easy_extensions) depends_on [:easy_mindmup] diff --git a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/controllers/queries_controller_patch.rb b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/controllers/queries_controller_patch.rb index 6c75d86..ca8efa8 100644 --- a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/controllers/queries_controller_patch.rb +++ b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/controllers/queries_controller_patch.rb @@ -2,25 +2,19 @@ module EasyWbs module QueriesControllerPatch def self.included(base) - base.send(:include, InstanceMethods) - - base.class_eval do - if method_defined?(:query_class) || private_method_defined?(:query_class) - alias_method_chain :query_class, :easy_wbs - end - end + base.prepend(InstanceMethods) end module InstanceMethods # Redmine return only direct sublasses but # Wbs query inherit from IssueQuery - def query_class_with_easy_wbs + def query_class case params[:type] when 'EasyWbs::IssueQuery' EasyWbs::IssueQuery else - query_class_without_easy_wbs + super end end diff --git a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/helpers/application_helper_patch.rb b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/helpers/application_helper_patch.rb index e0ab00f..6eb1ada 100644 --- a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/helpers/application_helper_patch.rb +++ b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/helpers/application_helper_patch.rb @@ -2,9 +2,6 @@ module EasyWbs module ApplicationHelperPatch def self.included(base) - base.extend(ClassMethods) - base.send(:include, InstanceMethods) - base.class_eval do def link_to_project_with_easy_wbs(project, options = {}) @@ -14,12 +11,6 @@ module EasyWbs end end - module InstanceMethods - end - - module ClassMethods - end - end end diff --git a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/models/project_patch.rb b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/models/project_patch.rb index 86a82e6..67467aa 100644 --- a/plugins/easy_wbs/lib/easy_wbs/redmine_patch/models/project_patch.rb +++ b/plugins/easy_wbs/lib/easy_wbs/redmine_patch/models/project_patch.rb @@ -3,12 +3,6 @@ module EasyWbs def self.included(base) base.send(:include, InstanceMethods) unless base.respond_to?(:assignable_users_including_all_subprojects) - - base.class_eval do - - - end - end module InstanceMethods diff --git a/plugins/easy_wbs/spec/features/jasmine_spec.rb b/plugins/easy_wbs/spec/features/jasmine_spec.rb index 2ba6df8..6f26ae3 100644 --- a/plugins/easy_wbs/spec/features/jasmine_spec.rb +++ b/plugins/easy_wbs/spec/features/jasmine_spec.rb @@ -1,31 +1,22 @@ -require File.expand_path('../../../../easyproject/easy_plugins/easy_extensions/test/spec/spec_helper', __FILE__) - -RSpec.feature 'Jasmine', logged: :admin, js: true, js_wait: true do - - let(:superproject) { - FactoryGirl.create(:project, add_modules: ['easy_wbs'], number_of_issues: 3) - } - let(:subproject) { - FactoryGirl.create(:project, parent_id: superproject.id, number_of_issues: 3) - } - let(:subissues) { - FactoryGirl.create_list(:issue, 3, parent_issue_id: superproject.issues[0].id, project_id: superproject.id) - } - - around(:each) do |example| - with_settings(rest_api_enabled: 1) { - with_easy_settings(easy_wbs_no_sidebar: 1) { example.run } - } - end - - describe 'WBS' do - it 'should not fail' do - visit project_easy_wbs_index_path(superproject, run_jasmine_tests: true) - wait_for_ajax - expect(page).to have_css('.jasmine-bar') - result = page.evaluate_script('jasmine.ysyInstance.tests.parseResult();') - expect(result).to eq('success') - end - end - -end +# require File.expand_path('../../../../easyproject/easy_plugins/easy_extensions/test/spec/spec_helper', __FILE__) +# +# RSpec.feature 'Jasmine', logged: :admin, js: true, js_wait: true do +# +# let(:superproject) { +# FactoryGirl.create(:project, add_modules: ['easy_wbs'], number_of_issues: 3) +# } +# +# around(:each) do |example| +# with_settings(rest_api_enabled: 1) { example.run } +# end +# +# describe 'WBS' do +# it 'should not fail' do +# visit project_easy_wbs_index_path(superproject, run_jasmine_tests: true) +# wait_for_ajax +# expect(page).to have_css('.jasmine-bar') +# result = page.evaluate_script('ysy.pro.test.parseResult();') +# expect(result).to eq('success') +# end +# end +# end diff --git a/plugins/redmine_agile/.drone.yml b/plugins/redmine_agile/.drone.yml deleted file mode 100644 index 19242fc..0000000 --- a/plugins/redmine_agile/.drone.yml +++ /dev/null @@ -1,78 +0,0 @@ -pipeline: - tests: - image: redmineup/redmine_agile - pull: true - commands: - - service postgresql start && service mysql start && sleep 5 - - export PATH=~/.rbenv/shims:$PATH - - export CODEPATH=`pwd` - - /root/run_for.sh redmine_agile+${LICENSE} ${RUBY_VER} ${DB} ${REDMINE} ${PLUGINS} -matrix: - include: - - RUBY_VER: ruby-2.2.6 - LICENSE: pro - DB: mysql - REDMINE: redmine-3.3 - - RUBY_VER: ruby-2.2.6 - LICENSE: light - DB: mysql - REDMINE: redmine-3.3 - - RUBY_VER: ruby-2.2.6 - LICENSE: pro - DB: pg - REDMINE: redmine-3.3 - - RUBY_VER: ruby-2.2.6 - LICENSE: light - DB: pg - REDMINE: redmine-3.3 - - RUBY_VER: ruby-2.2.6 - LICENSE: pro - DB: mysql - REDMINE: redmine-3.0 - - RUBY_VER: ruby-2.4.1 - DB: mysql - LICENSE: light - REDMINE: redmine-3.4 - - RUBY_VER: ruby-2.4.1 - DB: pg - LICENSE: pro - REDMINE: redmine-3.4 - - RUBY_VER: ruby-2.4.1 - DB: mysql - LICENSE: pro - REDMINE: redmine-3.4 - PLUGINS: redmine_checklists+pro - PLUGIN: redmine_agile - COVERAGE_EXPORT: 1 - - RUBY_VER: ruby-1.9.3 - DB: pg - LICENSE: pro - REDMINE: redmine-2.6 - - RUBY_VER: ruby-1.9.3 - DB: pg - LICENSE: pro - REDMINE: redmine-3.3 - - RUBY_VER: ruby-1.9.3 - DB: mysql - LICENSE: pro - REDMINE: redmine-3.3 - - RUBY_VER: ruby-1.9.3 - DB: pg - LICENSE: light - REDMINE: redmine-3.3 - - RUBY_VER: ruby-2.4.1 - DB: mysql - LICENSE: pro - REDMINE: redmine-trunk - - RUBY_VER: ruby-2.4.1 - DB: mysql - LICENSE: light - REDMINE: redmine-trunk - - RUBY_VER: ruby-2.4.1 - DB: pg - LICENSE: pro - REDMINE: redmine-trunk - - RUBY_VER: ruby-2.2.6 - DB: mysql - LICENSE: pro - REDMINE: redmine-trunk \ No newline at end of file diff --git a/plugins/redmine_agile/README.rdoc b/plugins/redmine_agile/README.rdoc old mode 100755 new mode 100644 diff --git a/plugins/redmine_agile/app/controllers/agile_boards_controller.rb b/plugins/redmine_agile/app/controllers/agile_boards_controller.rb old mode 100755 new mode 100644 index 8d39544..2f04dd2 --- a/plugins/redmine_agile/app/controllers/agile_boards_controller.rb +++ b/plugins/redmine_agile/app/controllers/agile_boards_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -22,8 +22,14 @@ class AgileBoardsController < ApplicationController menu_item :agile - before_action :find_issue, :only => [:update, :issue_tooltip, :inline_comment] - before_action :find_optional_project, :only => [:index, :create_issue] + before_action :find_issue, only: [:update, :issue_tooltip, :inline_comment, :edit_issue, :update_issue, :agile_data] + before_action :find_optional_project, only: [ + :index, + :create_issue, + ] + before_action :authorize, except: [:index, :edit_issue, :update_issue] + + accept_api_auth :agile_data helper :issues helper :journals @@ -46,6 +52,7 @@ class AgileBoardsController < ApplicationController include IssuesHelper helper :timelog include RedmineAgile::AgileHelper + helper :checklists if RedmineAgile.use_checklist? def index retrieve_agile_query @@ -53,7 +60,7 @@ class AgileBoardsController < ApplicationController @issues = @query.issues @issue_board = @query.issue_board @board_columns = @query.board_statuses - @swimlanes = @query.swimlanes + @allowed_statuses = statuses_allowed_for_create respond_to do |format| format.html { render :template => 'agile_boards/index', :layout => !request.xhr? } @@ -76,6 +83,7 @@ class AgileBoardsController < ApplicationController @issue.init_journal(User.current) @issue.safe_attributes = auto_assign_on_move? ? params[:issue].merge(:assigned_to_id => User.current.id) : params[:issue] checking_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params + saved = checking_params['issue'] && checking_params['issue'].inject(true) do |total, attribute| if @issue.attributes.include?(attribute.first) total &&= @issue.attributes[attribute.first].to_i == attribute.last.to_i @@ -83,7 +91,7 @@ class AgileBoardsController < ApplicationController total &&= true end end - call_hook(:controller_agile_boards_update_before_save, { :params => params, :issue => @issue}) + call_hook(:controller_agile_boards_update_before_save, { params: params, issue: @issue}) @update = true if saved && @issue.save call_hook(:controller_agile_boards_update_after_save, { :params => params, :issue => @issue}) @@ -95,16 +103,6 @@ class AgileBoardsController < ApplicationController end if params[:positions] @inline_adding = params[:issue][:notes] || nil - if Redmine::VERSION.to_s > '2.4' - if current_status = @query.board_statuses.detect{ |st| st == @issue.status } - @error_msg = l(:lable_agile_wip_limit_exceeded) if current_status.over_wp_limit? - @wp_class = current_status.wp_class - end - - if @old_status = @query.board_statuses.detect{ |st| st == old_status } - @wp_class_for_old_status = @old_status.wp_class - end - end respond_to do |format| format.html { render(:partial => 'issue_card', :locals => {:issue => @issue}, :status => :ok, :layout => nil) } @@ -114,52 +112,7 @@ class AgileBoardsController < ApplicationController messages = @issue.errors.full_messages messages = [l(:text_agile_move_not_possible)] if messages.empty? format.html { - render :json => messages, :status => :fail, :layout => nil - } - end - end - end - def create_issue - if !User.current.allowed_to?(:add_issues, @project) || params[:subject].blank? - messages = [l(:notice_not_authorized)] - respond_to do |format| - format.html { - render :json => messages, :status => :fail, :layout => nil - } - end - return - end - retrieve_agile_query_from_session - @update = true - @issue = Issue.new(:subject => params[:subject].strip, :project => @project, - :tracker => @project.trackers.first, :author => User.current, :status_id => params[:status_id]) - begin - if @issue.save(:validate => false) - if Redmine::VERSION.to_s > '2.4' - if current_status = @query.board_statuses.detect{ |st| st == @issue.status } - @error_msg = l(:lable_agile_wip_limit_exceeded) if current_status.over_wp_limit? - @wp_class = current_status.wp_class - end - end - @not_in_scope = !@query.issues.include?(@issue) - respond_to do |format| - format.html { render(:partial => 'issue_card', :locals => {:issue => @issue}, :status => :ok, :layout => nil) } - end - else - respond_to do |format| - messages = @issue.errors.full_messages - messages = [l(:text_agile_move_not_possible)] if messages.empty? - format.html { - render :json => messages, :status => :fail, :layout => nil - } - end - end - rescue - respond_to do |format| - messages = @issue.errors.full_messages - messages = [l(:text_agile_create_issue_error)] if messages.empty? - format.html { - render :json => messages, :status => :fail, :layout => nil + render json: messages, status: :unprocessable_entity, layout: nil } end end @@ -173,6 +126,16 @@ class AgileBoardsController < ApplicationController render 'inline_comment', :layout => nil end + def agile_data + @agile_data = @issue.agile_data + return render_404 unless @agile_data + + respond_to do |format| + format.any { head :ok } + format.api { } + end + end + private def auto_assign_on_move? @@ -181,4 +144,15 @@ class AgileBoardsController < ApplicationController @issue.status_id != params[:issue]['status_id'].to_i end + def statuses_allowed_for_create + issue = Issue.new(project: @project) + issue.tracker = issue_tracker(issue) + issue.new_statuses_allowed_to + end + + def issue_tracker(issue) + return issue.allowed_target_trackers.first if issue.respond_to?(:allowed_target_trackers) + return @project.trackers.first if @project + nil + end end diff --git a/plugins/redmine_agile/app/controllers/agile_charts_controller.rb b/plugins/redmine_agile/app/controllers/agile_charts_controller.rb index 563c9ae..5fd0ebe 100644 --- a/plugins/redmine_agile/app/controllers/agile_charts_controller.rb +++ b/plugins/redmine_agile/app/controllers/agile_charts_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -59,17 +59,22 @@ class AgileChartsController < ApplicationController def render_chart if @version @issues = @version.fixed_issues - options = {:date_from => @version.start_date, - :date_to => [@version.due_date, - @issues.maximum(:due_date), - @issues.maximum(:updated_on)].compact.max, - :due_date => @version.due_date || @issues.maximum(:due_date) || @issues.maximum(:updated_on)} + options = { date_from: @version.start_date, + date_to: [@version.due_date, + @issues.maximum(:due_date), + @issues.maximum(:updated_on)].compact.max, + due_date: @version.due_date || @issues.maximum(:due_date) || @issues.maximum(:updated_on), + chart_unit: params[:chart_unit] } @chart = params[:chart] else retrieve_charts_query - @query.date_to ||= Date.today - @issues = Issue.visible.where(@query.statement) - options = {:date_from => @query.date_from, :date_to => @query.date_to} + @issues = Issue.visible + @issues = @issues.joins(:fixed_version) if @query.filters.keys.include?('version_status') + @issues = @issues.where(@query.statement) + options = { date_from: @query.date_from, + date_to: @query.date_to, + interval_size: @query.interval_size, + chart_unit: @query.chart_unit } end render_data(options) end @@ -80,31 +85,15 @@ class AgileChartsController < ApplicationController private def render_data(options = {}) - case @chart - when 'lead_time' - data = RedmineAgile::LeadTimeChart.data(@issues, options) - when 'average_lead_time' - data = RedmineAgile::AverageLeadTimeChart.data(@issues, options) - when 'burnup' - data = RedmineAgile::BurnupChart.data(@issues, options) - when 'trackers_cumulative_flow' - data = RedmineAgile::TrackersCumulativeFlowChart.data(@issues, options) - when 'cumulative_flow' - data = RedmineAgile::CumulativeFlowChart.data(@issues, options) - when 'issues_velocity' - data = RedmineAgile::VelocityChart.data(@issues, options) - when 'work_burnup_hours' - data = RedmineAgile::WorkBurnupChart.data(@issues, options.merge(:estimated_unit => 'hours')) - when 'work_burnup_sp' - data = RedmineAgile::WorkBurnupChart.data(@issues, options.merge(:estimated_unit => 'story_points')) - when 'work_burndown_hours' - data = RedmineAgile::WorkBurndownChart.data(@issues, options.merge(:estimated_unit => 'hours')) - when 'work_burndown_sp' - data = RedmineAgile::WorkBurndownChart.data(@issues, options.merge(:estimated_unit => 'story_points')) - else - data = RedmineAgile::BurndownChart.data(@issues, options) + agile_chart = RedmineAgile::Charts::AGILE_CHARTS[@chart] + data = agile_chart[:class].data(@issues, options) if agile_chart + + if data + data[:chart] = @chart + data[:chart_unit] = options[:chart_unit] + return render json: data end - return render :json => data if data + raise ActiveRecord::RecordNotFound end @@ -115,28 +104,37 @@ class AgileChartsController < ApplicationController end def retrieve_charts_query - if params[:set_filter] || session[:agile_charts_query].nil? || session[:agile_charts_query][:project_id] != (@project ? @project.id : nil) + if params[:query_id].present? + @query = AgileChartsQuery.find(params[:query_id]) + raise ::Unauthorized unless @query.visible? + @query.project = @project + elsif params[:set_filter] || session[:agile_charts_query].nil? || session[:agile_charts_query][:project_id] != (@project ? @project.id : nil) # Give it a name, required to be valid - @query = AgileChartsQuery.new(:name => "_") + @query = AgileChartsQuery.new(:name => '_') @query.project = @project @query.build_from_params(params) - session[:agile_charts_query] = {:project_id => @query.project_id, - :filters => @query.filters, - :group_by => @query.group_by, - :column_names => @query.column_names, - :date_from => @query.date_from, - :date_to => @query.date_to} + session[:agile_charts_query] = { project_id: @query.project_id, + filters: @query.filters, + group_by: @query.group_by, + column_names: @query.column_names, + date_from: @query.date_from, + date_to: @query.date_to, + interval_size: @query.interval_size, + chart: @query.chart, + chart_unit: @query.chart_unit } else # retrieve from session - @query = AgileChartsQuery.new(:name => "_", - :filters => session[:agile_charts_query][:filters] || session[:agile_query][:filters], - :group_by => session[:agile_charts_query][:group_by], - :column_names => session[:agile_charts_query][:column_names], - :date_from => session[:agile_charts_query][:date_from], - :date_to => session[:agile_charts_query][:date_to] - ) + @query = AgileChartsQuery.new(name: '_', + filters: session[:agile_charts_query][:filters] || session[:agile_query][:filters], + group_by: session[:agile_charts_query][:group_by], + column_names: session[:agile_charts_query][:column_names], + date_from: session[:agile_charts_query][:date_from], + date_to: session[:agile_charts_query][:date_to], + interval_size: session[:agile_charts_query][:interval_size], + chart: session[:agile_charts_query][:chart], + chart_unit: session[:agile_charts_query][:chart_unit]) @query.project = @project end - @chart = params[:chart] || "issues_burndown" + @chart = params[:chart] || @query.chart end end diff --git a/plugins/redmine_agile/app/controllers/agile_colors_controller.rb b/plugins/redmine_agile/app/controllers/agile_colors_controller.rb deleted file mode 100644 index 0b6f5ff..0000000 --- a/plugins/redmine_agile/app/controllers/agile_colors_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -class AgileColorsController < ApplicationController - unloadable - - layout 'admin' - - before_action :require_admin - before_action :find_coloreds, :only => [:index, :update] - - def index - end - - def update - @colored_class.transaction do - params[:coloreds].each do |colored| - @colored_class.update(colored[:id], :color => colored[:color]) - end - flash[:notice] = l(:notice_successful_update) - end - redirect_to :action => :index, :object_type => params[:object_type] - end - - private - - def find_coloreds - klass = Object.const_get(params[:object_type].camelcase) rescue nil - @colored_class = klass - @coloreds = klass.sorted if klass && klass.new.respond_to?('color') - render_404 unless @coloreds.present? - end - -end diff --git a/plugins/redmine_agile/app/controllers/agile_journal_details_controller.rb b/plugins/redmine_agile/app/controllers/agile_journal_details_controller.rb index 8eaf878..3d2c6c0 100644 --- a/plugins/redmine_agile/app/controllers/agile_journal_details_controller.rb +++ b/plugins/redmine_agile/app/controllers/agile_journal_details_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -24,6 +24,7 @@ class AgileJournalDetailsController < ApplicationController helper :issues helper :agile_support + include AgileSupportHelper def done_ratio @done_ratios = @issue.journals.map(&:details).flatten.select {|detail| 'done_ratio' == detail.prop_key }.sort_by {|a| a.journal.created_on } @@ -32,9 +33,13 @@ class AgileJournalDetailsController < ApplicationController end def status - @statuses = @issue.journals.map(&:details).flatten.select {|detail| 'status_id' == detail.prop_key }.sort_by {|a| a.journal.created_on } - @statuses.unshift(JournalDetail.new(:property => 'attr', :prop_key => 'status_id', :value => history_initial_value(@statuses) || @issue.status.id, - :journal => Journal.new(:user => @issue.author, :created_on => @issue.created_on))) + @statuses_collector = AgileStatusesCollector.new(@issue) + @group = params[:group_by] if params[:group_by].present? + + respond_to do |format| + format.html + format.csv { send_data(issue_statuses_to_csv(@statuses_collector), type: 'text/csv; header=present', filename: "issue_#{@issue.id}_statuses.csv") } + end end def assignee diff --git a/plugins/redmine_agile/app/controllers/agile_queries_controller.rb b/plugins/redmine_agile/app/controllers/agile_queries_controller.rb deleted file mode 100644 index ab63020..0000000 --- a/plugins/redmine_agile/app/controllers/agile_queries_controller.rb +++ /dev/null @@ -1,124 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -class AgileQueriesController < ApplicationController - unloadable - - menu_item :agile - - before_action :find_query, :except => [:new, :create, :index] - before_action :find_optional_project, :only => [:new, :create] - - include QueriesHelper - helper :queries - helper :agile_boards - - def index - @limit = per_page_option - @query_count = AgileQuery.visible.count - @query_pages = Paginator.new @query_count, @limit, params['page'] - @queries = AgileQuery.visible. - order("#{Query.table_name}.name"). - limit(@limit). - offset(@offset). - all - end - - def new - @query = AgileQuery.new - @query.user = User.current - @query.project = @project - @query.visibility = AgileQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - @query.build_from_params(params) - end - - def create - @query = AgileQuery.new - @query.user = User.current - @query.project = params[:query_is_for_all] ? nil : @project - @query.build_from_params(params) - @query.name = params[:query] && params[:query][:name] - if User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - @query.visibility = (params[:query] && params[:query][:visibility]) || AgileQuery::VISIBILITY_PRIVATE - @query.role_ids = params[:query] && params[:query][:role_ids] if Redmine::VERSION.to_s > '2.4' - else - @query.visibility = AgileQuery::VISIBILITY_PRIVATE - end - @query.column_names = nil if params[:default_columns] - - if @query.save - flash[:notice] = l(:notice_successful_create) - redirect_to_agile_board(:query_id => @query) - else - render :action => 'new', :layout => !request.xhr? - end - end - - def edit - end - - def update - @query.project = nil if params[:query_is_for_all] - @query.build_from_params(params) - @query.name = params[:query] && params[:query][:name] - if User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - @query.visibility = (params[:query] && params[:query][:visibility]) || AgileQuery::VISIBILITY_PRIVATE - @query.role_ids = params[:query] && params[:query][:role_ids] if Redmine::VERSION.to_s > '2.4' - else - @query.visibility = AgileQuery::VISIBILITY_PRIVATE - end - @query.column_names = nil if params[:default_columns] - - if @query.save - flash[:notice] = l(:notice_successful_update) - redirect_to_agile_board(:query_id => @query) - else - render :action => 'edit' - end - end - - def destroy - @query.destroy - redirect_to_agile_board(:set_filter => 1) - end - -private - def find_query - @query = AgileQuery.find(params[:id]) - @project = @query.project - render_403 unless @query.editable_by?(User.current) - rescue ActiveRecord::RecordNotFound - render_404 - end - - def find_optional_project - @project = Project.find(params[:project_id]) if params[:project_id] - render_403 unless User.current.allowed_to?(:add_agile_queries, @project, :global => true) - rescue ActiveRecord::RecordNotFound - render_404 - end - - def redirect_to_agile_board(options) - if @project - redirect_to agile_board_path(options.merge(:project_id => @project)) - else - redirect_to agile_board_path(options) - end - end -end diff --git a/plugins/redmine_agile/app/controllers/agile_versions_controller.rb b/plugins/redmine_agile/app/controllers/agile_versions_controller.rb deleted file mode 100644 index 611b72f..0000000 --- a/plugins/redmine_agile/app/controllers/agile_versions_controller.rb +++ /dev/null @@ -1,85 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -class AgileVersionsController < ApplicationController - unloadable - - menu_item :agile - - before_action :find_project_by_project_id, :only => [:index, :autocomplete, :load] - before_action :find_version, :only => [:load] - before_action :authorize, :except => [:autocomplete, :load] - before_action :find_no_version_issues, :only => [:index, :autocomplete] - - include QueriesHelper - helper :queries - include RedmineAgile::AgileHelper - - def index - retrieve_versions_query - if @query.valid? - @backlog_version = @query.backlog_version - @backlog_version_issues = @query.backlog_version_issues - - @current_version = @query.current_version - @current_version_issues = @query.current_version_issues - end - respond_to do |format| - format.html - format.js - end - rescue ActiveRecord::RecordNotFound - render_404 - end - - def autocomplete - render :layout => false - end - - def load - retrieve_versions_query - @version_issues = @query.version_issues(@version) - @version_type = params[:version_type] - @other_version_type = @version_type == "backlog" ? "current" : "backlog" - @other_version_id = params[:other_version_id] - respond_to do |format| - format.js - end - end - - private - - def find_version - @version = Version.visible.find(params[:version_id]) - @project ||= @version.project - rescue ActiveRecord::RecordNotFound - render_404 - end - - def find_no_version_issues - retrieve_versions_query - if @query.valid? - scope = @query.no_version_issues(params) - @issue_count = scope.count - @issue_pages = Redmine::Pagination::Paginator.new @issue_count, 20, params['page'] - @version_issues = scope.offset(@issue_pages.offset).limit(@issue_pages.per_page).all - end - end - -end diff --git a/plugins/redmine_agile/app/helpers/agile_boards_helper.rb b/plugins/redmine_agile/app/helpers/agile_boards_helper.rb index b05cf67..5a11b6f 100644 --- a/plugins/redmine_agile/app/helpers/agile_boards_helper.rb +++ b/plugins/redmine_agile/app/helpers/agile_boards_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -21,133 +21,41 @@ module AgileBoardsHelper def agile_color_class(issue, options={}) - if options[:color_base] - color = case options[:color_base] - when AgileColor::COLOR_GROUPS[:issue] - issue.color - when AgileColor::COLOR_GROUPS[:tracker] - issue.tracker.color - when AgileColor::COLOR_GROUPS[:priority] - issue.priority.color - when AgileColor::COLOR_GROUPS[:spent_time] - AgileColor.for_spent_time(issue.estimated_hours, issue.spent_hours) - when AgileColor::COLOR_GROUPS[:project] - issue.project.color - end - else - color = if RedmineAgile.tracker_colors? - issue.tracker.color - elsif RedmineAgile.issue_colors? - issue.color - elsif RedmineAgile.priority_colors? - issue.priority.color - elsif RedmineAgile.spent_time_colors? - AgileColor.for_spent_time(issue.estimated_hours, issue.spent_hours) + '' end - end - "#{RedmineAgile.color_prefix}-#{color}" if color && RedmineAgile.use_colors? - end def agile_user_color(user, options={}) - return if Redmine::VERSION.to_s < '2.4' - user_color = user.color rescue nil - user_color ||= AgileColor.for_user(user.login) - if options[:color_base] - "border-left: 5px solid #{user_color}".html_safe if options[:color_base] == AgileColor::COLOR_GROUPS[:user] - elsif RedmineAgile.user_color? - "border-left: 5px solid #{user_color}".html_safe - end end def header_th(name, rowspan = 1, colspan = 1, leaf = nil) th_attributes = {} if leaf # th_attributes[:style] = "" - th_attributes[:style] = "border-bottom: 4px solid; border-bottom-color: #{color_by_name(leaf.name)};" if RedmineAgile.status_colors? th_attributes[:"data-column-id"] = leaf.id issue_count = leaf.instance_variable_get("@issue_count") || 0 - if Redmine::VERSION.to_s > '2.4' - wp_count = leaf.instance_variable_get("@wp_max") - unless wp_count.blank? - th_attributes[:class] = leaf.wp_class - issue_count_tag = content_tag(:span, issue_count, :class => leaf.wp_class) - count_tag = " (#{content_tag(:span, issue_count_tag + "/#{wp_count}", :class => 'count')})".html_safe - else - count_tag = " (#{content_tag(:span, issue_count.to_i, :class => 'count')})".html_safe - end - else - count_tag = " (#{content_tag(:span, issue_count.to_i, :class => 'count')})".html_safe - end - + count_tag = " (#{content_tag(:span, issue_count.to_i, :class => 'count')})".html_safe + # estimated hours total story_points_count = leaf.instance_variable_get("@story_points") || 0 hours_count = leaf.instance_variable_get("@estimated_hours_sum") || 0 - if story_points_count > 0 - hours_tag = " #{content_tag(:span, (story_points_count).to_s + 'sp', - :class => 'hours', :title => l(:field_estimated_hours))}".html_safe - else - hours_tag = " #{content_tag(:span, ("%.2fh" % hours_count.to_f).to_s, - :class => 'hours', :title => l(:field_estimated_hours))}".html_safe if hours_count > 0 + values = [] + values << '%.2fh' % hours_count.to_f if hours_count > 0 + values << "#{story_points_count}sp" if story_points_count > 0 + if values.present? + hours_tag = content_tag(:span, values.join('/').html_safe, class: 'hours', title: l(:field_estimated_hours)) end end - th_attributes[:rowspan] = rowspan if rowspan > 1 - th_attributes[:colspan] = colspan if colspan > 1 content_tag :th, h(name) + count_tag + hours_tag, th_attributes end def render_board_headers(columns) - tree = HeaderTree.new - - columns.map do |column| - path = column.name.split(':').map(&:strip) - tree.put path, column - end - - # puts tree - - maxdepth = tree.depth - - ret = tree.render - - ret[1..-1].map do |row| - row.map do |th_params| - header_th *th_params + "<tr>#{columns.map{|column| header_th(column.name, 1, 1, column)}.join}</tr>".html_safe end - end.map{|x| "<tr>#{x.join('')}</tr>" }.join.html_safe - end def color_by_name(name) "##{"%06x" % (name.unpack('H*').first.hex % 0xffffff)}" end - def format_swimlane_object(object, html=true) - case object.class.name - when 'Array' - object.map {|o| format_swimlane_object(o, html)}.join(', ').html_safe - when 'Time' - format_time(object) - when 'Date' - format_date(object) - when 'Fixnum' - object.to_s - when 'Float' - sprintf "%.2f", object - when 'User' - html ? link_to_user(object) : object.to_s - when 'Project' - html ? link_to_project(object) : object.to_s - when 'Version' - html ? link_to(object.name, version_path(object)) : object.to_s - when 'TrueClass' - l(:general_text_Yes) - when 'FalseClass' - l(:general_text_No) - when 'Issue' - object.visible? && html ? link_to_issue(object) : "##{object.id}" - else - html ? h(object) : object.to_s - end - end def render_board_fields_selection(query) query.available_inline_columns.reject(&:frozen?).reject{ |c| c.name == :story_points && !RedmineAgile.use_story_points? }.map do |column| @@ -160,13 +68,9 @@ module AgileBoardsHelper current_statuses = query.options[:f_status] || IssueStatus.where(:is_closed => false).pluck(:id).map(&:to_s) wp = query.options[:wp] || {} status_tags = available_statuses.map do |status| - content_tag(:span, - label_tag('', check_box_tag('f_status[]', status.id, current_statuses.include?(status.id.to_s) - ) + content_tag(:span, status.to_s), :title => status.to_s) + text_field_tag("wp[#{status.id}]", wp[status.id.to_s], - :size => 5, :class => 'wp_input', :placeholder => "WIP", - :title => l(:label_agile_wip_limit)), :class => 'floating' - ) - end.join(' ').html_safe + label_tag('', check_box_tag('f_status[]', status.id, current_statuses.include?(status.id.to_s) + ) + status.to_s, :class => 'floating') + end.join(' ').html_safe hidden_field_tag('f[]', 'status_id').html_safe + hidden_field_tag('op[status_id]', "=").html_safe + status_tags @@ -177,7 +81,7 @@ module AgileBoardsHelper hours << "%.2f" % issue.total_spent_hours.to_f if query.has_column_name?(:spent_hours) && issue.total_spent_hours > 0 hours << "%.2f" % issue.estimated_hours.to_f if query.has_column_name?(:estimated_hours) && issue.estimated_hours hours = [hours.join('/') + "h"] unless hours.blank? - hours << "#{issue.story_points}sp" if query.has_column_name?(:story_points) && issue.story_points + hours << "#{issue.story_points}sp" if RedmineAgile.use_story_points? && query.has_column_name?(:story_points) && issue.story_points content_tag(:span, "(#{hours.join('/')})", :class => 'hours') unless hours.blank? end @@ -216,8 +120,8 @@ module AgileBoardsHelper "#{I18n.t('datetime.distance_in_words.x_days', :count => (hours/24).to_i)}" end - def class_for_closed_issue(issue) - return '' if !RedmineAgile.hide_closed_issues_data? + def class_for_closed_issue(issue, is_version_board) + return '' if !RedmineAgile.hide_closed_issues_data? && !is_version_board return 'closed-issue' if issue.closed? '' end @@ -234,11 +138,22 @@ module AgileBoardsHelper javascript_tag(js_code) end + def estimated_value(issue) + return (issue.story_points || 0) if RedmineAgile.use_story_points? + issue.estimated_hours.to_f || 0 + end + + def estimated_time_value(query, issue) + issue.estimated_hours.to_f if query.has_column_name?(:estimated_hours) + end + + def story_points_value(query, issue) + issue.story_points.to_f if query.has_column_name?(:story_points) && RedmineAgile.use_story_points? + end + def show_checklist?(issue) RedmineAgile.use_checklist? && issue.checklists.any? && User.current.allowed_to?(:view_checklists, issue.project) rescue false end - - end diff --git a/plugins/redmine_agile/app/helpers/agile_charts_helper.rb b/plugins/redmine_agile/app/helpers/agile_charts_helper.rb index b813026..679997a 100644 --- a/plugins/redmine_agile/app/helpers/agile_charts_helper.rb +++ b/plugins/redmine_agile/app/helpers/agile_charts_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -22,16 +22,11 @@ module AgileChartsHelper def render_agile_charts_breadcrumb links = [] - links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil}) - links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project + links << link_to(l(:label_project_all), project_id: nil, issue_id: nil) + links << link_to(h(@project), project_id: @project, issue_id: nil) if @project if @version - if @version.visible? - links << link_to(@version.name, version_path(@version)) - else - links << @version.name - end + links << @version.visible? ? link_to(@version.name, version_path(@version)) : @version.name end breadcrumb links end - end diff --git a/plugins/redmine_agile/app/helpers/agile_support_helper.rb b/plugins/redmine_agile/app/helpers/agile_support_helper.rb index a18d86d..aba1c22 100644 --- a/plugins/redmine_agile/app/helpers/agile_support_helper.rb +++ b/plugins/redmine_agile/app/helpers/agile_support_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -20,6 +20,8 @@ # along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. module AgileSupportHelper + include ActionView::Helpers::DateHelper + # Returns a h2 tag and sets the html title with the given arguments def title(*args) strings = args.map do |arg| @@ -37,4 +39,32 @@ module AgileSupportHelper end_time = next_event ? next_event.journal.created_on : Time.now distance_of_time_in_words(end_time, event.journal.created_on).html_safe end + + def issue_statuses_to_csv(collector) + decimal_separator = l(:general_csv_decimal_separator) + encoding = 'utf-8' + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + headers = [ "#", + l(:field_created_on, locale: :en), + l(:field_status, locale: :en), + l(:field_duration, locale: :en), + l(:field_author, locale: :en), + l(:field_assigned_to, locale: :en) + ] + csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + + collector.data.each_with_index do |data, index| + issue_status = IssueStatus.where(id: data.status_id).first + fields = [index + 1, + format_time(data.journal.created_on), + issue_status.name, + distance_of_time_in_words(data.end_time, data.start_time), + data.journal.user.name, + Principal.where(id: data.assigned_to_id).first.try(:name) + ] + csv << fields.collect { |c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + end + end + export + end end diff --git a/plugins/redmine_agile/app/helpers/agile_versions_helper.rb b/plugins/redmine_agile/app/helpers/agile_versions_helper.rb deleted file mode 100644 index 9b306e6..0000000 --- a/plugins/redmine_agile/app/helpers/agile_versions_helper.rb +++ /dev/null @@ -1,54 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module AgileVersionsHelper - def version_select_tag(version, option={}) - return "" if version.blank? - version_id = version.is_a?(Version) && version.id || version - other_version_id = option[:other_version].is_a?(Version) && option[:other_version].id || option[:other_version] - select_tag('version_id', - options_for_select(versions_collection_for_select, - {:selected => version_id, :disabled => other_version_id}), - :data => {:remote => true, - :method => 'get', - :url => load_agile_versions_path(:version_type => option[:version_type], - :other_version_id => other_version_id, - :project_id => @project)}) + - content_tag(:span, '', :class => "hours header-hours #{option[:version_type]}-hours") - end - - def versions_collection_for_select - @project.shared_versions.open.map{|version| [format_version_name(version), version.id.to_s]} - end - - def estimated_hours(issue) - "%.2fh" % issue.estimated_hours.to_f - end - - def estimated_value(issue) - return (issue.story_points || 0) if RedmineAgile.use_story_points? - issue.estimated_hours.to_f || 0 - end - - def estimated_unit - RedmineAgile.use_story_points? ? 'sp' : 'h' - end -end diff --git a/plugins/redmine_agile/app/models/agile_charts_query.rb b/plugins/redmine_agile/app/models/agile_charts_query.rb index 85fce06..c38ffff 100644 --- a/plugins/redmine_agile/app/models/agile_charts_query.rb +++ b/plugins/redmine_agile/app/models/agile_charts_query.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -22,136 +22,67 @@ class AgileChartsQuery < AgileQuery validate :validate_query_dates - def initialize(attributes=nil, *args) + attr_writer :date_from, :date_to + + def initialize(attributes = nil, *args) super attributes self.filters.delete('status_id') + self.filters['chart_period'] = { operator: 'm', values: [''] } unless has_filter?('chart_period') end - self.operators_by_filter_type[:chart_period] = [ "><", "w", "lw", "l2w", "m", "lm", "y"] + self.operators_by_filter_type[:chart_period] = ['><', 'w', 'lw', 'l2w', 'm', 'lm', 'y'] def initialize_available_filters - principals = [] - subprojects = [] - versions = [] - categories = [] - issue_custom_fields = [] - - # add_available_filter "chart_period", :type => :chart_period, :name => l(:label_agile_chart_period) - - if project - principals += project.principals.sort - unless project.leaf? - subprojects = project.descendants.visible.all - principals += Principal.member_of(subprojects) - end - versions = project.shared_versions.all - categories = project.issue_categories.all - issue_custom_fields = project.all_issue_custom_fields - else - if all_projects.any? - principals += Principal.member_of(all_projects) - end - versions = Version.visible.where(:sharing => 'system').all - issue_custom_fields = IssueCustomField.where(:is_for_all => true) - end - principals.uniq! - principals.sort! - users = principals.select {|p| p.is_a?(User)} - - if project.nil? - project_values = [] - if User.current.logged? && User.current.memberships.any? - project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] - end - project_values += all_projects_values - add_available_filter("project_id", - :type => :list, :values => project_values - ) unless project_values.empty? - end - - add_available_filter "tracker_id", - :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } - add_available_filter "priority_id", - :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } - - author_values = [] - author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - author_values += users.collect{|s| [s.name, s.id.to_s] } - add_available_filter("author_id", - :type => :list, :values => author_values - ) unless author_values.empty? - - assigned_to_values = [] - assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - assigned_to_values += (Setting.issue_group_assignment? ? - principals : users).collect{|s| [s.name, s.id.to_s] } - add_available_filter("assigned_to_id", - :type => :list_optional, :values => assigned_to_values - ) unless assigned_to_values.empty? - - if versions.any? - add_available_filter "fixed_version_id", - :type => :list_optional, - :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } - end - - if categories.any? - add_available_filter "category_id", - :type => :list_optional, - :values => categories.collect{|s| [s.name, s.id.to_s] } - end - - add_available_filter "subject", :type => :text - add_available_filter "created_on", :type => :date_past - add_available_filter "updated_on", :type => :date_past - add_available_filter "closed_on", :type => :date_past - add_available_filter "start_date", :type => :date - add_available_filter "due_date", :type => :date - add_available_filter "estimated_hours", :type => :float - add_available_filter "done_ratio", :type => :integer - - if subprojects.any? - add_available_filter "subproject_id", - :type => :list_subprojects, - :values => subprojects.collect{|s| [s.name, s.id.to_s] } - end + super - add_custom_fields_filters(issue_custom_fields) + add_available_filter 'chart_period', type: :date_past, name: l(:label_date) + end - add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version + def sprint_values + return [] unless project - Tracker.disabled_core_fields(trackers).each {|field| - delete_available_filter field - } + project.shared_agile_sprints.available.map { |s| [s.to_s, s.id.to_s] } end def default_columns_names @default_columns_names = [:id, :subject, :estimated_hours, :spent_hours, :done_ratio, :assigned_to] end - def sql_for_chart_period_field(field, operator, value) - "1=1" + def sql_for_chart_period_field(_field, _operator, _value) + '1=1' end - def date_from - @date_from + def chart + @chart ||= RedmineAgile::Charts.valid_chart_name_by(options[:chart]) end - def date_from=(arg) - @date_from = Date.parse(arg.to_s) rescue nil + def chart=(arg) + options[:chart] = arg + end + + def date_from + @date_from ||= chart_period[:from] end def date_to - @date_to + @date_to ||= chart_period[:to] + end + + def interval_size + if RedmineAgile::AgileChart::TIME_INTERVALS.include?(options[:interval_size]) + options[:interval_size] + else + RedmineAgile::AgileChart::DAY_INTERVAL + end end - def date_to=(arg) - @date_to = Date.parse(arg.to_s) rescue nil + def interval_size=(value) + options[:interval_size] = value end def build_from_params(params) if params[:fields] || params[:f] - self.filters = {} + self.filters = {}.merge(chart_period_filter(params)) add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) else available_filters.keys.each do |field| @@ -160,33 +91,77 @@ class AgileChartsQuery < AgileQuery end self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) - self.date_from = params[:date_from] || (params[:query] && params[:query][:date_from]) self.date_to = params[:date_to] || (params[:query] && params[:query][:date_to]) + self.chart = params[:chart] || (params[:query] && params[:query][:chart]) || params[:default_chart] || RedmineAgile.default_chart + self.interval_size = params[:interval_size] || (params[:query] && params[:query][:interval_size]) || RedmineAgile::AgileChart::DAY_INTERVAL + self.chart_unit = params[:chart_unit] || (params[:query] && params[:query][:chart_unit]) || RedmineAgile::Charts::UNIT_ISSUES + self end -private + def condition_for_status + '1=1' + end + + private - def issue_scope - Issue.visible. - eager_load(:status, - :project, - :assigned_to, - :tracker, - :priority, - :category, - :fixed_version, - :agile_data). - where(statement) + def chart_period_filter(params) + return {} if (params[:fields] || params[:f]).include?('chart_period') + { 'chart_period' => { operator: 'm', values: [''] } } end def validate_query_dates - if (self.date_from && self.date_to && self.date_from >= self.date_to) || - (self.date_from && self.date_to.blank?) - m = l(:label_agile_chart_dates) + " " + l(:invalid, :scope => 'activerecord.errors.messages') - errors.add(:base, m) + if (self.date_from && self.date_to && self.date_from >= self.date_to) + errors.add(:base, l(:label_agile_chart_dates) + ' ' + l(:invalid, scope: 'activerecord.errors.messages')) + end + end + + def db_timestamp_regex + /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:.\d*))/ + end + + def chart_period + @chart_period ||= { + from: chart_period_statement.match("chart_period > '#{db_timestamp_regex}") { |m| Time.zone.parse(m[1]) }, + to: chart_period_statement.match("chart_period <= '#{db_timestamp_regex}") { |m| Time.zone.parse(m[1]) } + } + end + + def chart_period_statement + @chart_period_statement ||= build_chart_period_statement + end + + def build_chart_period_statement + field = 'chart_period' + operator = filters[field][:operator] + values = filters[field][:values] + date = User.current.today + + case operator + when 'w' + first_day_of_week = (Setting.start_of_week || l(:general_first_day_of_week)).to_i + day_of_week = date.cwday + days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql_for_field(field, '><t-', [days_ago], Issue.table_name, field) + when 'm' + days_ago = date - date.beginning_of_month + sql_for_field(field, '><t-', [days_ago], Issue.table_name, field) + when 'y' + days_ago = date - date.beginning_of_year + sql_for_field(field, '><t-', [days_ago], Issue.table_name, field) + when '><' + sql_for_field(field, '><', adjusted_values(values), Issue.table_name, field) + else + sql_for_field(field, operator, values, Issue.table_name, field) end end + def adjusted_values(values) + return values unless values.is_a?(Array) + + from = values[0].present? ? Date.parse(values[0]) : Date.today + to = values[1].present? ? Date.parse(values[1]) : Date.today + [from.to_s, (to < from ? from : to).to_s] + end end diff --git a/plugins/redmine_agile/app/models/agile_color.rb b/plugins/redmine_agile/app/models/agile_color.rb deleted file mode 100644 index 1bfeb46..0000000 --- a/plugins/redmine_agile/app/models/agile_color.rb +++ /dev/null @@ -1,68 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -class AgileColor < ActiveRecord::Base - unloadable - include Redmine::SafeAttributes - - COLOR_GROUPS = { - :issue => 'issue', - :priority => 'priority', - :tracker => 'tracker', - :spent_time => 'spent_time', - :user => 'user', - :project => 'project' - } - - AGILE_COLORS = { - :green => 'green', - :blue => 'blue', - :turquoise => 'turquoise', - :light_green => 'lightgreen', - :yellow => 'yellow', - :orange => 'orange', - :red => 'red', - :purple => 'purple', - :gray => 'gray' - } - - belongs_to :container, :polymorphic => true - - attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 - safe_attributes 'color' - - def self.for_user(user_name) - '0xffffff'.html_safe if !user_name - "##{"%06x" % (user_name.unpack('H*').first.hex % 0xffffff)}".html_safe - end - - def self.for_spent_time(est_time = nil, spent_time = nil) - return AGILE_COLORS[:gray] if !est_time || !spent_time || est_time.to_f.zero? || (spent_time.to_f.zero? && est_time.to_f.zero?) - percent = ((spent_time / est_time.to_f) * 100).to_i - if percent <= 80 - return AGILE_COLORS[:green] - elsif percent > 80 && (spent_time < est_time) - return AGILE_COLORS[:yellow] - elsif (spent_time >= est_time) && (spent_time < est_time * 2) - return AGILE_COLORS[:red] - elsif spent_time >= est_time * 2 - return AGILE_COLORS[:purple] - end - end -end diff --git a/plugins/redmine_agile/app/models/agile_data.rb b/plugins/redmine_agile/app/models/agile_data.rb old mode 100755 new mode 100644 index f5685f4..87db483 --- a/plugins/redmine_agile/app/models/agile_data.rb +++ b/plugins/redmine_agile/app/models/agile_data.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -20,5 +20,6 @@ class AgileData < ActiveRecord::Base unloadable belongs_to :issue + validates :story_points, :numericality => {:only_integer => true, :greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} end diff --git a/plugins/redmine_agile/app/models/agile_query.rb b/plugins/redmine_agile/app/models/agile_query.rb index f465bc9..1cb03d1 100644 --- a/plugins/redmine_agile/app/models/agile_query.rb +++ b/plugins/redmine_agile/app/models/agile_query.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -18,7 +18,6 @@ # along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. class AgileQuery < Query - unloadable include Redmine::SafeAttributes attr_reader :truncated @@ -27,36 +26,26 @@ class AgileQuery < Query self.view_permission = :view_issues if Redmine::VERSION.to_s >= '3.4' self.available_columns = [ - QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => :label_agile_issue_id), - QueryColumn.new(:project, :groupable => "#{Issue.table_name}.project_id", :sortable => "#{Project.table_name}.id"), - QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), - QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), - QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), - QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), - QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("users")}, :groupable => true), - QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => "#{Issue.table_name}.category_id"), - QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => "#{Issue.table_name}.fixed_version_id"), - QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), - QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), - QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"), - QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"), - QueryColumn.new(:thumbnails, :caption => :label_agile_board_thumbnails), - QueryColumn.new(:description), - QueryColumn.new(:sub_issues, :caption => :label_agile_sub_issues), - QueryColumn.new(:day_in_state, :caption => :label_agile_day_in_state), - QueryColumn.new(:parent, :groupable => "#{Issue.table_name}.parent_id", :sortable => "#{AgileData.table_name}.position", :caption => :field_parent_issue), - QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => "#{Issue.table_name}.assigned_to_id"), - QueryColumn.new(:relations, :caption => :label_related_issues), - QueryColumn.new(:last_comment, :caption => :label_agile_last_comment), - QueryColumn.new(:story_points, :caption => :label_agile_story_points) + QueryColumn.new(:id, sortable: "#{Issue.table_name}.id", default_order: 'desc', caption: :label_agile_issue_id), + QueryColumn.new(:project, groupable: "#{Issue.table_name}.project_id", sortable: "#{Project.table_name}.id"), + QueryColumn.new(:tracker, sortable: "#{Tracker.table_name}.position", groupable: true), + QueryColumn.new(:estimated_hours, sortable: "#{Issue.table_name}.estimated_hours"), + QueryColumn.new(:done_ratio, sortable: "#{Issue.table_name}.done_ratio"), + QueryColumn.new(:day_in_state, caption: :label_agile_day_in_state), + QueryColumn.new(:parent, groupable: "#{Issue.table_name}.parent_id", sortable: "#{AgileData.table_name}.position", caption: :field_parent_issue), + QueryColumn.new(:assigned_to, sortable: lambda { User.fields_for_order_statement }, groupable: "#{Issue.table_name}.assigned_to_id"), + QueryColumn.new(:relations, caption: :label_related_issues), + QueryColumn.new(:last_comment, caption: :label_agile_last_comment), + QueryColumn.new(:story_points, caption: :label_agile_story_points) ] - if RedmineAgile.use_checklist? - self.available_columns << QueryColumn.new(:checklists, :caption => :label_checklist_plural) + self.available_columns << QueryColumn.new(:checklists, caption: :label_checklist_plural) if RedmineAgile.use_checklist? + + def self.build_from_params(params, attributes = {}) + new(attributes).build_from_params(params) end - before_save :set_default_when_appropriate - scope :visible, lambda {|*args| + scope :visible, lambda { |*args| user = args.shift || User.current base = Project.allowed_to_condition(user, :view_issues, *args) scope = eager_load(:project).where("#{table_name}.project_id IS NULL OR (#{base})") @@ -80,17 +69,17 @@ class AgileQuery < Query end } - def initialize(attributes=nil, *args) + def initialize(attributes = nil, *args) super attributes unless Redmine::VERSION.to_s > '2.4' - self.filters ||= { 'status_id' => {:operator => "*", :values => [""]} } + self.filters ||= { 'status_id' => { operator: '*', values: [''] } } end - self.filters ||= { } + self.filters ||= {} @truncated = false end def card_columns - self.inline_columns.select{|c| !%w(day_in_state tracker thumbnails description assigned_to done_ratio spent_hours estimated_hours project id sub_issues checklists last_comment story_points).include?(c.name.to_s)} + self.inline_columns.select { |c| !%w(day_in_state tracker thumbnails description assigned_to done_ratio spent_hours estimated_hours project id sub_issues checklists last_comment story_points).include?(c.name.to_s) } end def visible?(user=User.current) @@ -103,7 +92,7 @@ class AgileQuery < Query if project (user.roles_for_project(project) & roles).any? else - Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any? + Member.where(user_id: user.id).joins(:roles).where(member_roles: {role_id: roles.map(&:id)}).any? end else user == self.user @@ -119,34 +108,35 @@ class AgileQuery < Query end def color_base - options[:color_base] || RedmineAgile.color_base end def color_base=(value) - options[:color_base] = value end - def is_default? - !!options[:is_default] + + def default_chart + end + + def default_chart=(value) end - def is_default=(value) - options[:is_default] = !!value + def chart_unit + @chart_unit ||= RedmineAgile::Charts.valid_chart_unit_by(options[:chart], options[:chart_unit]) end - def set_as_default - AgileQuery.where(:project_id => self.project_id).where(:visibility => self.visibility).where("#{AgileQuery.table_name}.id <> ?", self.id).each do |query| - query.is_default = false - query.save - end if self.is_default? + def chart_unit=(value) + options[:chart_unit] = value end - def self.default_query(project=nil) - board_scope = AgileQuery.visible - board_scope = board_scope.where(:project_id => project) - default_query = board_scope.where("#{table_name}.visibility = ? AND #{table_name}.user_id = ?", VISIBILITY_PRIVATE, User.current).detect{|q| q.is_default?} - default_query ||= board_scope.eager_load(:user => :memberships).where("#{table_name}.visibility = ?", VISIBILITY_ROLES).detect{|q| q.is_default?} - default_query ||= board_scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC).detect{|q| q.is_default?} - default_query + def draw_relations + r = options[:draw_relations] + r.nil? || r == '1' + end + + def draw_relations=(arg) + options[:draw_relations] = (arg == '0' ? '0' : nil) + end + + def with_totals? end def build_from_params(params) @@ -161,7 +151,6 @@ class AgileQuery < Query self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) self.color_base = params[:color_base] || (params[:query] && params[:query][:color_base]) - self.is_default = params[:is_default] || (params[:query] && params[:query][:is_default]) self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations]) if params[:f_status] || params[:wp] self.options = options.merge({ :f_status => params[:f_status], :wp => params[:wp] }) @@ -169,11 +158,6 @@ class AgileQuery < Query self end - # Builds a new query from the given params and attributes - def self.build_from_params(params, attributes={}) - new(attributes).build_from_params(params) - end - def initialize_available_filters principals = [] subprojects = [] @@ -194,16 +178,16 @@ class AgileQuery < Query if all_projects.any? principals += Principal.member_of(all_projects) end - versions = Version.visible.where(:sharing => 'system').all - issue_custom_fields = IssueCustomField.where(:is_for_all => true) + versions = Version.visible.where(sharing: 'system').all + issue_custom_fields = IssueCustomField.where(is_for_all: true) end principals.uniq! principals.sort! - users = principals.select {|p| p.is_a?(User)} + users = principals.select { |p| p.is_a?(User) } unless Redmine::VERSION.to_s > '2.4' - add_available_filter "status_id", - :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } + add_available_filter 'status_id', + type: :list_status, values: IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } end if project.nil? @@ -213,20 +197,20 @@ class AgileQuery < Query end project_values += all_projects_values add_available_filter("project_id", - :type => :list, :values => project_values + type: :list, values: project_values ) unless project_values.empty? end add_available_filter "tracker_id", - :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } + type: :list, values: trackers.collect{|s| [s.name, s.id.to_s] } add_available_filter "priority_id", - :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } + type: :list, values: IssuePriority.all.collect{|s| [s.name, s.id.to_s] } author_values = [] author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? author_values += users.collect{|s| [s.name, s.id.to_s] } add_available_filter("author_id", - :type => :list, :values => author_values + type: :list, values: author_values ) unless author_values.empty? assigned_to_values = [] @@ -234,17 +218,17 @@ class AgileQuery < Query assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } add_available_filter("assigned_to_id", - :type => :list_optional, :values => assigned_to_values + type: :list_optional, values: assigned_to_values ) unless assigned_to_values.empty? - group_values = Group.all.collect {|g| [g.name, g.id.to_s] } + group_values = Group.visible.all.collect {|g| [g.name, g.id.to_s] } add_available_filter("member_of_group", - :type => :list_optional, :values => group_values + type: :list_optional, values: group_values ) unless group_values.empty? role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } add_available_filter("assigned_to_role", - :type => :list_optional, :values => role_values + type: :list_optional, values: role_values ) unless role_values.empty? if versions.any? @@ -252,39 +236,39 @@ class AgileQuery < Query fixed_versions << ["<< #{l(:label_current_version)} >>", 'current_version'] versions.sort.each{ |s| fixed_versions << ["#{s.project.name} - #{s.name}", s.id.to_s] } add_available_filter "fixed_version_id", - :type => :list_optional, - :values => fixed_versions + type: :list_optional, + values: fixed_versions end if categories.any? add_available_filter "category_id", - :type => :list_optional, - :values => categories.collect{|s| [s.name, s.id.to_s] } - end - - add_available_filter "subject", :type => :text - add_available_filter "created_on", :type => :date_past - add_available_filter "updated_on", :type => :date_past - add_available_filter "closed_on", :type => :date_past - add_available_filter "start_date", :type => :date - add_available_filter "due_date", :type => :date - add_available_filter "estimated_hours", :type => :float - add_available_filter "done_ratio", :type => :integer - add_available_filter "parent_issue_id", :type => :relation - add_available_filter "has_sub_issues", :type => :list, - :values => [ l(:general_text_yes), l(:general_text_no)], - :label => :label_agile_has_sub_issues - add_available_filter "version_status", :type => :list, - :name => l("label_attribute_of_fixed_version", :name => 'status'), - :values => Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} - add_available_filter "parent_issue_tracker_id", :type => :list, - :label => :label_agile_parent_issue_tracker_id, - :values => Tracker.pluck(:name) + type: :list_optional, + values: categories.collect{|s| [s.name, s.id.to_s] } + end + + add_available_filter "subject", type: :text + add_available_filter "created_on", type: :date_past + add_available_filter "updated_on", type: :date_past + add_available_filter "closed_on", type: :date_past + add_available_filter "start_date", type: :date + add_available_filter "due_date", type: :date + add_available_filter "estimated_hours", type: :float + add_available_filter "done_ratio", type: :integer + add_available_filter "parent_issue_id", type: :relation, values: all_projects_values + add_available_filter "has_sub_issues", type: :list, + values: [l(:general_text_yes), l(:general_text_no)], + label: :label_agile_has_sub_issues + add_available_filter "version_status", type: :list, + name: l("label_attribute_of_fixed_version", name: 'status'), + values: Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} + add_available_filter "parent_issue_tracker_id", type: :list, + label: :label_agile_parent_issue_tracker_id, + values: Tracker.pluck(:name) if subprojects.any? add_available_filter "subproject_id", - :type => :list_subprojects, - :values => subprojects.collect{|s| [s.name, s.id.to_s] } + type: :list_subprojects, + values: subprojects.collect{|s| [s.name, s.id.to_s] } end @@ -293,24 +277,23 @@ class AgileQuery < Query add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version IssueRelation::TYPES.each do |relation_type, options| - add_available_filter relation_type, :type => :relation, :label => options[:name] + add_available_filter relation_type, type: :relation, label: options[:name], values: all_projects_values end - Tracker.disabled_core_fields(trackers).each {|field| + Tracker.disabled_core_fields(trackers).each { |field| delete_available_filter field } - add_available_filter "issue_id", :type => :integer, :label => :label_issue - add_available_filter 'description', :type => :text + add_available_filter "issue_id", type: :integer, label: :label_issue - if User.current.allowed_to?(:set_issues_private, nil, :global => true) || - User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + if User.current.allowed_to?(:set_issues_private, nil, global: true) || + User.current.allowed_to?(:set_own_issues_private, nil, global: true) add_available_filter 'is_private', type: :list, values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']] end if User.current.logged? - add_available_filter 'watcher_id', type: :list, values: [["<< #{l(:label_me)} >>", 'me']] + add_available_filter 'watcher_id', type: :list, values: author_values end end @@ -319,29 +302,29 @@ class AgileQuery < Query @available_columns = self.class.available_columns.dup @available_columns += (project ? project.all_issue_custom_fields : IssueCustomField).visible.collect { |cf| QueryCustomFieldColumn.new(cf) } - if User.current.allowed_to?(:view_time_entries, project, :global => true) + if User.current.allowed_to?(:view_time_entries, project, global: true) index = nil - @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} + @available_columns.each_with_index { |column, i| index = i if column.name == :estimated_hours} index = (index ? index + 1 : -1) # insert the column after estimated_hours or at the end @available_columns.insert index, QueryColumn.new(:spent_hours, - :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)", - :default_order => 'desc', - :caption => :label_spent_time + sortable: "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)", + default_order: 'desc', + caption: :label_spent_time ) end - if User.current.allowed_to?(:set_issues_private, nil, :global => true) || - User.current.allowed_to?(:set_own_issues_private, nil, :global => true) - @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") + if User.current.allowed_to?(:set_issues_private, nil, global: true) || + User.current.allowed_to?(:set_own_issues_private, nil, global: true) + @available_columns << QueryColumn.new(:is_private, sortable: "#{Issue.table_name}.is_private") end - disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} - @available_columns.reject! {|column| + disabled_fields = Tracker.disabled_core_fields(trackers).map { |field| field.sub(/_id$/, '')} + @available_columns.reject! { |column| disabled_fields.include?(column.name.to_s) } - @available_columns.reject! {|column| column.name == :done_ratio} unless Issue.use_field_for_done_ratio? + @available_columns.reject! { |column| column.name == :done_ratio} unless Issue.use_field_for_done_ratio? @available_columns end @@ -359,11 +342,12 @@ class AgileQuery < Query end def has_column_name?(name) - columns.detect{|c| c.name == name} + columns.detect { |c| c.name == name} end def groupable_columns - available_columns.select {|c| c.groupable && !c.is_a?(QueryCustomFieldColumn)} + groupable_method = Redmine::VERSION.to_s > '4.2' ? :groupable? : :groupable + available_columns.select { |c| c.public_send(groupable_method) && !c.is_a?(QueryCustomFieldColumn) } end def sql_for_issue_id_field(field, operator, value) @@ -387,7 +371,7 @@ class AgileQuery < Query end def sql_for_version_status_field(field, operator, value) - sql_for_field(field, operator, value, Version.table_name, "status") + sql_for_field(field, operator, value, Version.table_name, "status") end def sql_for_has_sub_issues_field(field, operator, value) @@ -398,22 +382,25 @@ class AgileQuery < Query end def sql_for_parent_issue_id_field(field, operator, value, options={}) - value = value.first.split(",") if value.is_a? Array - value = value.split(",") if value.is_a? String + value = value.first.split(',') if value.is_a? Array + value = value.split(',') if value.is_a? String sql = case operator - when "*", "!*", "=", "!" - sql_for_field(field, operator, value, queried_table_name, "parent_id") - when "=p", "=!p", "!p" - op = (operator == "!p" ? 'NOT IN' : 'IN') - comp = (operator == "=!p" ? '<>' : '=') - "#{Issue.table_name}.parent_id #{op} (SELECT DISTINCT #{Issue.table_name}.id FROM #{Issue.table_name} WHERE #{Issue.table_name}.project_id #{comp} #{value.first.to_i})" - end + when '*', '!*', '=', '!' + sql_for_field(field, operator, value, queried_table_name, 'parent_id') + when '=p', '=!p', '!p' + op = (operator == '!p' ? 'NOT IN' : 'IN') + comp = (operator == '=!p' ? '<>' : '=') + "#{Issue.table_name}.parent_id #{op} (SELECT DISTINCT #{Issue.table_name}.id FROM #{Issue.table_name} WHERE #{Issue.table_name}.project_id #{comp} #{value.first.to_i})" + when '*o', '!o' + op = (operator == '!o' ? 'NOT IN' : 'IN') + "#{Issue.table_name}.parent_id #{op} (SELECT DISTINCT #{Issue.table_name}.id FROM #{Issue.table_name} WHERE #{Issue.table_name}.status_id IN (SELECT DISTINCT #{IssueStatus.table_name}.id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))" + end "(#{sql})" end def sql_for_parent_issue_tracker_id_field(field, operator, value) cond = if operator == '=' then '' else 'NOT' end - selected_trackers_ids = Tracker.where(:name => value).pluck(:id).join(',') + selected_trackers_ids = Tracker.where(name: value).pluck(:id).join(',') "( EXISTS (SELECT * FROM #{Issue.table_name} AS parents WHERE parents.tracker_id #{cond} IN (#{selected_trackers_ids}) AND parents.id = issues.parent_id ) )" end @@ -455,16 +442,58 @@ class AgileQuery < Query end end + def sql_for_relations(field, operator, value, options={}) + relation_options = IssueRelation::TYPES[field] + return relation_options unless relation_options + + relation_type = field + join_column, target_join_column = "issue_from_id", "issue_to_id" + if relation_options[:reverse] || options[:reverse] + relation_type = relation_options[:reverse] || relation_type + join_column, target_join_column = target_join_column, join_column + end + + sql = case operator + when "*", "!*" + op = (operator == "*" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')" + when "=", "!" + op = (operator == "=" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" + when "=p", "=!p", "!p" + op = (operator == "!p" ? 'NOT IN' : 'IN') + comp = (operator == "=!p" ? '<>' : '=') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" + when "*o", "!o" + op = (operator == "!o" ? 'NOT IN' : 'IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))" + end + + if relation_options[:sym] == field && !options[:reverse] + sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] + sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") + end + "(#{sql})" + end + + IssueRelation::TYPES.keys.each do |relation_type| + alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations + end + def condition_for_status if Redmine::VERSION.to_s > '2.4' - return {:status_id => options[:f_status] || IssueStatus.where(:is_closed => false)} + return { status_id: options[:f_status] || IssueStatus.where(is_closed: false) } end '1=1' end def issues(options={}) + @issues_cache ||= {} + return @issues_cache[options.to_s] if @issues_cache.has_key?(options.to_s) + order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) - scope = issue_scope. + scope = options[:scope] ? options[:scope] : issue_scope + scope = scope. joins(:status). eager_load((options[:include] || []).uniq). where(options[:conditions]). @@ -474,20 +503,7 @@ class AgileQuery < Query offset(options[:offset]) scope = scope.preload(:custom_values) - if has_column?(:author) - scope = scope.preload(:author) - end - if color_base == AgileColor::COLOR_GROUPS[:issue] - scope = scope.preload(:agile_color) - end - - if color_base == AgileColor::COLOR_GROUPS[:priority] - scope = scope.preload(:priority => :agile_color) - end - - if color_base == AgileColor::COLOR_GROUPS[:tracker] - scope = scope.preload(:tracker => :agile_color) - end + scope = scope.preload(:author) if has_column?(:author) if has_column_name?(:checklists) scope = scope.preload(:checklists) @@ -516,8 +532,7 @@ class AgileQuery < Query :journal_details => {:prop_key => 'status_id'}).order("created_on DESC") end - - scope + @issues_cache[options.to_s] = scope rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end @@ -526,6 +541,10 @@ class AgileQuery < Query @issues_ids ||= scope.map(&:id) end + def issues_paginator(issues, page = nil) + Redmine::Pagination::Paginator.new(issues.count, 20, page) + end + def journals_for_state @journals_for_state end @@ -542,62 +561,41 @@ class AgileQuery < Query if Redmine::VERSION.to_s >= '3.4' && project project.rolled_up_statuses else - IssueStatus.where(:id => Tracker.eager_load(:issues => [:status, :project, :fixed_version]).where(statement).map(&:issue_statuses).flatten.uniq.map(&:id)) + IssueStatus.where(id: Tracker.eager_load(issues: [:status, :project, :fixed_version]).where(statement).map(&:issue_statuses).flatten.uniq.map(&:id)) end status_filter_values = (options[:f_status] if options) if status_filter_values - result_statuses = statuses.where(:id => status_filter_values) + result_statuses = statuses.where(id: status_filter_values) else - result_statuses = statuses.where(:is_closed => false) + result_statuses = statuses.where(is_closed: false) end result_statuses.sorted.map do |s| s.instance_variable_set "@issue_count", self.issue_count_by_status[s.id].to_i if has_column_name?(:estimated_hours) s.instance_variable_set "@estimated_hours_sum", self.issue_count_by_estimated_hours[s.id].to_f end - if has_column_name?(:story_points) + if RedmineAgile.use_story_points? && has_column_name?(:story_points) s.instance_variable_set "@story_points", self.issue_count_by_story_points[s.id].to_i end - if options && options[:wp] - wp_string = options[:wp][s.id.to_s] - if /(\d+)-?(\d*)/i =~ wp_string - s.instance_variable_set("@wp_max", $2.blank? ? $1.to_i : $2.to_i) - s.instance_variable_set("@wp_min", $1.to_i) if !$1.blank? && !$2.blank? - end - end - - def s.over_wp_limit? - return false if @wp_max.blank? - @wp_max.to_i < @issue_count - end - - def s.under_wp_limit? - return false if @wp_min.blank? - @wp_min.to_i > @issue_count - end - - def s.wp_class - return 'over_wp_limit' if over_wp_limit? - 'under_wp_limit' if under_wp_limit? - end s end else status_filter_operator = filters.fetch("status_id", {}).fetch(:operator, nil) status_filter_values = filters.fetch("status_id", {}).fetch(:values, []) - statuses = IssueStatus.where(:id => Tracker.eager_load(:issues => [:status, :project, :fixed_version]).where(statement).map(&:issue_statuses).flatten.uniq.map(&:id)) - result_statuses = case status_filter_operator - when "o" - statuses.where(:is_closed => false).sorted - when "c" - statuses.where(:is_closed => true).sorted - when "=" - statuses.where(:id => status_filter_values).sorted - when "!" - statuses.where("#{IssueStatus.table_name}.id NOT IN (" + status_filter_values.map{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")").sorted - else - statuses.sorted - end + statuses = IssueStatus.where(id: Tracker.eager_load(issues: [:status, :project, :fixed_version]).where(statement).map(&:issue_statuses).flatten.uniq.map(&:id)) + result_statuses = + case status_filter_operator + when "o" + statuses.where(is_closed: false).sorted + when "c" + statuses.where(is_closed: true).sorted + when "=" + statuses.where(id: status_filter_values).sorted + when "!" + statuses.where("#{IssueStatus.table_name}.id NOT IN (" + status_filter_values.map{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")").sorted + else + statuses.sorted + end result_statuses.map do |s| s.instance_variable_set "@issue_count", self.issue_count_by_status[s.id].to_i if has_column_name?(:estimated_hours) @@ -608,19 +606,6 @@ class AgileQuery < Query s end end - def swimlanes - return [] unless self.grouped? - lane_ids = issue_scope.group(self.group_by_column.groupable).count.keys - lanes = Issue.reflect_on_association(self.group_by_column.name).klass.where(:id => lane_ids).reorder(group_by_sort_order) - lanes = lanes.eager_load(:agile_data) if self.group_by_column.name == :parent - lanes = lanes.to_a - lanes << nil if lane_ids.include?(nil) - lanes - end - - def issue_count_by_swimlane - @issue_count_by_swimlane ||= issue_scope.group("#{Issue.table_name}.#{self.group_by_column.name}_id").count if self.grouped? - end def issue_count_by_status @issue_count_by_status ||= issue_scope.group("#{Issue.table_name}.status_id").count @@ -634,68 +619,17 @@ class AgileQuery < Query @issue_count_by_story_points ||= issue_scope.group("#{Issue.table_name}.status_id").sum("#{AgileData.table_name}.story_points") end - def issue_board(options={}) + def issue_board @truncated = RedmineAgile.board_items_limit <= issue_scope.count all_issues = self.issues.limit(RedmineAgile.board_items_limit).sorted_by_rank - grouped_issues = grouped? ? all_issues.group_by{|i| [i.status_id, i.send("#{self.group_by_column.name}_id")]} : all_issues.group_by{|i| [i.status_id]} - grouped_issues.values.each{|x|x.sort!{|a,b| a.position.to_i <=> b.position.to_i}} if grouped? - grouped_issues - end - - # Function for filter with relations - - def sql_for_relations(field, operator, value, options={}) - relation_options = IssueRelation::TYPES[field] - return relation_options unless relation_options - - relation_type = field - join_column, target_join_column = "issue_from_id", "issue_to_id" - if relation_options[:reverse] || options[:reverse] - relation_type = relation_options[:reverse] || relation_type - join_column, target_join_column = target_join_column, join_column - end - - sql = case operator - when "*", "!*" - op = (operator == "*" ? 'IN' : 'NOT IN') - "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')" - when "=", "!" - op = (operator == "=" ? 'IN' : 'NOT IN') - "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" - when "=p", "=!p", "!p" - op = (operator == "!p" ? 'NOT IN' : 'IN') - comp = (operator == "=!p" ? '<>' : '=') - "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" - when "*o", "!o" - op = (operator == "!o" ? 'NOT IN' : 'IN') - "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))" + all_issues.group_by{|i| [i.status_id]} end - if relation_options[:sym] == field && !options[:reverse] - sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] - sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") - end - "(#{sql})" - end - - IssueRelation::TYPES.keys.each do |relation_type| - alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations - end - - def draw_relations - r = options[:draw_relations] - r.nil? || r == '1' - end - - def draw_relations=(arg) - options[:draw_relations] = (arg == '0' ? '0' : nil) - end - def statement if values_for('fixed_version_id') == ['current_version'] && project version = current_version # substitute id for current version - filters['fixed_version_id'][:values] = [version.id.to_s] if version + version ? filters['fixed_version_id'][:values] = [version.id.to_s] : filters.delete('fixed_version_id') end clauses = super if version @@ -705,31 +639,41 @@ class AgileQuery < Query clauses end -private + private + + def base_agile_query_scope + Issue.visible + .eager_load(:status, :project, :assigned_to, :tracker, :priority, :category, :fixed_version, :agile_data) + .where(agile_projects) + .where(statement) + .where(condition_for_status) + end + + def agile_projects + return '1=1' unless project + + p_ids = [project.id] + p_ids += project.descendants.select { |sub| sub.module_enabled?('agile') }.map(&:id) if Setting.display_subprojects_issues? + + "#{Project.table_name}.id IN (#{p_ids.join(',')})" + end + def issue_scope - Issue.visible. - eager_load(:status, - :project, - :assigned_to, - :tracker, - :priority, - :category, - :fixed_version, - :agile_data). - where(statement). - where(condition_for_status) + return @agile_scope if @agile_scope + + @agile_scope = base_agile_query_scope + @agile_scope + end + + def project_statement + return super end def current_version return @current_version if @current_version + versions = project.shared_versions.open.where("LOWER(#{Version.table_name}.name) NOT LIKE LOWER(?)", 'backlog') versions -= versions.select(&:completed?).reverse @current_version = versions.to_a.uniq.sort.first end - def set_default_when_appropriate - if options[:is_default] - set_as_default - end - end - end diff --git a/plugins/redmine_agile/app/models/agile_versions_query.rb b/plugins/redmine_agile/app/models/agile_versions_query.rb deleted file mode 100644 index 53c18b9..0000000 --- a/plugins/redmine_agile/app/models/agile_versions_query.rb +++ /dev/null @@ -1,150 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - - -class AgileVersionsQuery < Query - unloadable - - self.queried_class = Issue - self.view_permission = :view_issues if Redmine::VERSION.to_s >= '3.4' - - self.available_columns = [ - QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), - QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), - QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), - QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("users")}, :groupable => true), - QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => "#{Issue.table_name}.category_id"), - QueryColumn.new(:status, :groupable => true, :caption => :field_invoice_status), - QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => "#{Issue.table_name}.assigned_to_id") - ] - - def initialize(attributes=nil, *args) - super attributes - self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } - end - - def initialize_available_filters - principals = [] - categories = [] - issue_custom_fields = [] - - if project - principals += project.principals.sort - unless project.leaf? - subprojects = project.descendants.visible.all - principals += Principal.member_of(subprojects) - end - categories = project.issue_categories.all - issue_custom_fields = project.all_issue_custom_fields - else - if all_projects.any? - principals += Principal.member_of(all_projects) - end - issue_custom_fields = IssueCustomField.where(:is_for_all => true) - end - principals.uniq! - principals.sort! - users = principals.select {|p| p.is_a?(User)} - - add_available_filter "tracker_id", - :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } - add_available_filter "priority_id", - :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } - - author_values = [] - author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - author_values += users.collect{|s| [s.name, s.id.to_s] } - add_available_filter("author_id", - :type => :list, :values => author_values - ) unless author_values.empty? - - assigned_to_values = [] - assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - assigned_to_values += (Setting.issue_group_assignment? ? - principals : users).collect{|s| [s.name, s.id.to_s] } - add_available_filter("assigned_to_id", - :type => :list_optional, :values => assigned_to_values - ) unless assigned_to_values.empty? - - if categories.any? - add_available_filter "category_id", - :type => :list_optional, - :values => categories.collect{|s| [s.name, s.id.to_s] } - end - - add_available_filter "status_id", - :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } - - add_available_filter "estimated_hours", :type => :float - add_custom_fields_filters(issue_custom_fields) - - add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version - end - - def backlog_version - @backlog_version = project.shared_versions.open.where("LOWER(#{Version.table_name}.name) LIKE LOWER(?)", "backlog").first || - project.shared_versions.open.where(:effective_date => nil).first || - project.shared_versions.open.order("effective_date ASC").first - end - - def backlog_version_issues - return [] if backlog_version.blank? - backlog_version.fixed_issues.visible.joins(query_includes).where(statement).sorted_by_rank - end - - def current_version - @current_version = Version.open. - where(:project_id => project). - where("#{Version.table_name}.id <> ?", self.backlog_version). - order("effective_date DESC").first - end - - def current_version_issues - return [] if current_version.blank? - current_version.fixed_issues.visible.joins(query_includes).where(statement).sorted_by_rank - end - - def no_version_issues(params={}) - q = (params[:q] || params[:term]).to_s.strip - scope = Issue.visible.joins(query_includes) - if project - project_ids = [project.id] - project_ids += project.descendants.collect(&:id) if Setting.display_subprojects_issues? - scope = scope.where(:project_id => project_ids) - end - scope = scope.where(statement).where(:fixed_version_id => nil).sorted_by_rank - if q.present? - if q.match(/^#?(\d+)\z/) - scope = scope.where("(#{Issue.table_name}.id = ?) OR (LOWER(#{Issue.table_name}.subject) LIKE LOWER(?))", $1.to_i,"%#{q}%") - else - scope = scope.where("LOWER(#{Issue.table_name}.subject) LIKE LOWER(?)", "%#{q}%") - end - end - scope - end - - def version_issues(version) - version.fixed_issues.visible.joins(query_includes).where(statement).sorted_by_rank - end - private - - def query_includes - [:project] - end -end diff --git a/plugins/redmine_agile/app/views/agile_boards/_add_issue_card.html.erb b/plugins/redmine_agile/app/views/agile_boards/_add_issue_card.html.erb index bb7b80e..8b13789 100644 --- a/plugins/redmine_agile/app/views/agile_boards/_add_issue_card.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/_add_issue_card.html.erb @@ -1,6 +1 @@ -<% if User.current.allowed_to?(:add_issues, @project, :global => true) && @project && RedmineAgile.allow_create_card? && @project.trackers.any? %> - <div class="add-issue"> - <input class="new-card__input" placeholder='<%= l(:label_agile_add_new_issue) %>' > - </div> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_boards/_board.html.erb b/plugins/redmine_agile/app/views/agile_boards/_board.html.erb index ea98ab8..c60d1b5 100644 --- a/plugins/redmine_agile/app/views/agile_boards/_board.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/_board.html.erb @@ -1,7 +1,7 @@ + <%= form_tag({}) do -%> <%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> <%= hidden_field_tag 'project_id', @project.id if @project %> - <% board_statuses = IssueStatus.sorted %> <div class="agile-board autoscroll"> <div class="flash error" style="display: none;" id="agile-board-errors"> </div> @@ -10,42 +10,17 @@ <thead> <%= render_board_headers(@board_columns) %> </thead> - <% if @swimlanes.size > 0 %> - <% @swimlanes.each do |swimlane| %> - <tr class="group open swimlane" data-id="<%= swimlane && swimlane.id || swimlane %>"> - <td colspan="<%= @board_columns.size %>"> - <span class="expander" onclick="toggleRowGroup(this);"> </span> - <%= swimlane.blank? ? l(:label_none) : format_swimlane_object(swimlane) %><span class="count"><%= @query.issue_count_by_swimlane[swimlane && swimlane.id || swimlane] %></span> - <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", - "toggleAllRowGroups(this)", :class => 'toggle-all') %> - </td> - </tr> - - <tr style="text-align: center;white-space: nowrap;" class="swimlane issue <%= cycle('odd', 'even') %>" data-id="<%= swimlane && swimlane.id || swimlane %>" data-field="<%= @query.grouped? && "#{@query.group_by_column.name}_id" %>"> - <% @board_columns.each do |column| %> - <td class="issue-status-col <%= 'closed' if column.is_closed? %> <%= column.wp_class if column.respond_to?(:wp_class) %>" data-id="<%= column.id %>"> - <% @issue_board[[column.id, swimlane && swimlane.id || swimlane]].each do |issue| %> - <%= render :partial => 'issue_card', :locals => {:issue => issue} %> - <% end if @issue_board[[column.id, swimlane && swimlane.id || swimlane]] %> - </td> - <% end %> - </tr> - <% end %> - <% else %> - <tr style="text-align: center;white-space: nowrap;" class="issue <%= cycle('odd', 'even') %>"> - <% @board_columns.each do |column| %> - <td class="issue-status-col <%= 'closed' if column.is_closed? %> <%= 'collapse' if RedmineAgile.hide_closed_issues_data? %> <%= column.wp_class if column.respond_to?(:wp_class) %>" data-id="<%= column.id %>"> - <% @issue_board[[column.id]].each do |issue| %> + <tr style="text-align: center;white-space: nowrap;" class="issue <%= cycle('odd', 'even') %>"> + <% @board_columns.each do |column| %> + <% column_issues = @issue_board[[column.id]] || [] %> + <td class="issue-status-col <%= 'closed' if column.is_closed? %> <%= 'collapse' if RedmineAgile.hide_closed_issues_data? %> <%= column.wp_class if column.respond_to?(:wp_class) %> <%= 'empty' if column_issues.empty? %>" data-id="<%= column.id %>"> + <% column_issues.each do |issue| %> <%= render :partial => 'issue_card', :locals => {:issue => issue} %> <% end if @issue_board[[column.id]] %> - <%= render(:partial => 'add_issue_card') unless column.is_closed? %> + <%= render(:partial => 'add_issue_card') if @allowed_statuses.include?(column) && !column.is_closed? %> </td> - - <% end %> - </tr> - <% end %> - + <% end %> + </tr> </table> </div> - <% end %> diff --git a/plugins/redmine_agile/app/views/agile_boards/_index.html.erb b/plugins/redmine_agile/app/views/agile_boards/_index.html.erb index 3bd1aa0..8afe358 100644 --- a/plugins/redmine_agile/app/views/agile_boards/_index.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/_index.html.erb @@ -1,78 +1,55 @@ <div class="contextual"> -<% if !@query.new_record? && @query.editable_by?(User.current) %> - <%= link_to l(:button_edit), edit_agile_query_path(@query), :class => 'icon icon-edit' %> - <%= delete_link agile_query_path(@query) %> -<% end %> + <%= link_to l(:label_agile_charts), @project ? project_agile_charts_path(project_id: @project) : agile_charts_path, class: 'icon icon-stats agile_charts_link', onclick: 'chartLinkGenerator();' %> + <% if !@query.new_record? && @query.editable_by?(User.current) %> + <%= link_to l(:button_edit), edit_agile_query_path(@query), class: 'icon icon-edit' %> + <%= delete_link agile_query_path(@query) %> + <% end %> </div> <% html_title(@query.new_record? ? l(:label_agile_board) : @query.name) %> -<h2> - <%= @query.new_record? ? l(:label_agile_board) : h(@query.name) %> - <span class="live_search"> - <%= text_field_tag(:search, '', :id => 'agile_live_search', :class => 'live_search_field', :placeholder => l(:label_cards_search)) %> - </span> -</h2> +<%= form_tag({ controller: 'agile_boards', action: 'index', project_id: @project }, method: :get, id: 'query_form', onsubmit: 'DisableNullFields()') do %> + <h2> + <%= @query.new_record? ? l(:label_agile_board) : h(@query.name) %> + <span class="live_search"> + <%= text_field_tag(:search, '', :id => 'agile_live_search', :class => 'live_search_field', :placeholder => l(:label_cards_search)) %> + </span> + </h2> -<%= form_tag({ :controller => 'agile_boards', :action => 'index', :project_id => @project }, - :method => :get, :id => 'query_form', :onsubmit => 'DisableNullFields()') do %> <div id="query_form_with_buttons" class="hide-when-print"> <%= hidden_field_tag 'set_filter', '1' %> <div id="query_form_content"> <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> - <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> + <legend class="icon icon-<%= @query.new_record? ? 'expended' : 'collapsed' %>" onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> <div style="<%= @query.new_record? ? "" : "display: none;" %>"> <%= render :partial => 'queries/filters', :locals => {:query => @query} %> </div> </fieldset> <fieldset id="options" class="collapsible collapsed"> - <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> + <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend> <div style="display: none;"> - <table class="options"> - <% if Redmine::VERSION.to_s > '2.4' %> - <tr> - <td> - <%= l(:label_agile_board_columns) %> - </td> - <td class="card-fields"> - <%= render_board_fields_status(@query) %> - </td> - </tr> - <% end %> + <table class="options agile_options"> <tr> - <td><%= l(:label_agile_fields) %></td> - <td class="card-fields"> - <%= render_board_fields_selection(@query) %> - </td> + <td colspan="2"> + <fieldset class="card-fields"> + <legend><%= l(:label_agile_board_columns) %></legend> + <%= render_board_fields_status(@query) %> + </fieldset> </tr> <tr> - <td><label for='group_by'><%= l(:label_agile_swimlanes) %></label></td> - <td><%= select_tag('group_by', - options_for_select( - [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, - @query.group_by) - ) %></td> + <td colspan="2"> + <fieldset class="card-fields"> + <legend><%= l(:label_agile_fields) %></legend> + <%= render_board_fields_selection(@query) %> + </fieldset> + </td> </tr> - <% if RedmineAgile.use_colors? && @query.respond_to?(:color_base) %> - <tr> - <td><label for='color_base'><%= l(:label_agile_color_based_on) %></label></td> - <td><%= select_tag 'color_base', options_card_colors_for_select(@query.color_base) %></td> - </tr> - <% end %> </table> </div> </fieldset> </div> <p class="buttons"> - <span class="contextual"> - <%= link_to_function l(:label_agile_fullscreen), '$("html").toggleClass("agile-board-fullscreen"); saveFullScreenState()', :class => 'icon icon-fullscreen' %> - </span> <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> - <% if @query.new_record? && User.current.allowed_to?(:add_agile_queries, @project, :global => true) %> - <%= link_to_function l(:button_save), - "$('#query_form').attr('action', '#{ @project ? new_project_agile_query_path(@project) : new_agile_query_path }').submit()", - :class => 'icon icon-save' %> - <% end %> </p> </div> @@ -80,7 +57,8 @@ <%= error_messages_for 'query' %> <% if @query.valid? %> - <% if @issues.empty? || @board_columns.empty? %> + <% empty_data = @issues.empty? || @board_columns.empty? %> + <% if empty_data %> <p class="nodata"><%= l(:label_no_data) %></p> <% else %> <% if @query.truncated %> @@ -91,6 +69,9 @@ <% end %> <% content_for :sidebar do %> + + <%= render :partial => 'upgrade_to_pro' %> + <%= render :partial => 'issues_links' %> <% if @project && @project.assignable_users.any? %> <%= render :partial => 'members' %> @@ -99,22 +80,19 @@ <% end %> <% html_title l(:label_agile_board) %> -<%= javascript_tag "agileContextMenuInit('#{ url_for(issues_context_menu_path) }')" %> <% content_for :header_tags do %> - <%= javascript_include_tag "redmine_agile", :plugin => 'redmine_agile' %> - <%= javascript_include_tag "jquery.ui.touch-punch.js", :plugin => 'redmine_agile' %> - <%= javascript_include_tag "visibility.min.js", :plugin => 'redmine_agile' %> - <%= javascript_include_tag "redmine_agile_context_menu", :plugin => 'redmine_agile' %> + <%= javascript_include_tag "redmine_agile", plugin: 'redmine_agile' %> + <%= javascript_include_tag "jquery.ui.touch-punch.js", plugin: 'redmine_agile' %> <%= stylesheet_link_tag 'context_menu' %> - <%= stylesheet_link_tag "redmine_agile.css", :plugin => "redmine_agile", :media => "print" %> + <%= stylesheet_link_tag "redmine_agile.css", plugin: "redmine_agile", media: "print" %> <% end %> -<% if User.current.allowed_to?(:edit_issues, @project, :global => true) %> +<% if User.current.allowed_to?(:edit_issues, @project, global: true) %> <script type="text/javascript"> var agileBoard = new AgileBoard({ project_id: '<%= @project && @project.id %>', update_agile_board_path: '<%= escape_javascript update_agile_board_path %>', issues_path: '<%= escape_javascript issues_path %>', - create_issue_path: '<%= escape_javascript(agile_create_issue_path(:project_id => @project)) if @project %>' + create_issue_path: '<%= escape_javascript(agile_create_issue_path(project_id: @project)) if @project %>' }); </script> <% end %> diff --git a/plugins/redmine_agile/app/views/agile_boards/_issue_card.html.erb b/plugins/redmine_agile/app/views/agile_boards/_issue_card.html.erb index a674c5c..9dc7ec1 100644 --- a/plugins/redmine_agile/app/views/agile_boards/_issue_card.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/_issue_card.html.erb @@ -1,5 +1,10 @@ -<div class="issue-card hascontextmenu <%= agile_color_class(issue, :color_base => @query.respond_to?(:color_base) && @query.color_base) %> <%= class_for_closed_issue(issue) %>" data-id="<%= issue.id %>" style="<%= agile_user_color(issue.assigned_to, :color_base => @query.respond_to?(:color_base) && @query.color_base) if issue.assigned_to %>"> - <% if issue.closed? && RedmineAgile.hide_closed_issues_data? %> +<div class="issue-card hascontextmenu <%= agile_color_class(issue, :color_base => @query.respond_to?(:color_base) && @query.color_base) %> <%= class_for_closed_issue(issue, @version_board) %>" + data-id="<%= issue.id %>" + data-estimated-hours="<%= estimated_time_value(@query, issue).to_f %>" + data-story-points="<%= story_points_value(@query, issue).to_f %>" + style="<%= agile_user_color(issue.assigned_to, :color_base => @query.respond_to?(:color_base) && @query.color_base) if issue.assigned_to %>"> + + <% if issue.closed? && RedmineAgile.hide_closed_issues_data? && !@version_board %> <span class="fields"> <div class="tooltip"> <p class="issue-id <%= 'without-tracker' if @query.has_column_name?(:tracker).blank? %>"> @@ -11,10 +16,9 @@ </span> <% else %> <span class="fields"> - <% if User.current.allowed_to?(:edit_issues, @project, :global => true) && RedmineAgile.allow_inline_comments? %> + <% if User.current.allowed_to?(:edit_issues, @project, :global => true) && !@version_board %> <div class="quick-edit-card"> - <%# link_to image_tag("/images/edit.png"), edit_issue_path(issue), :class => "", :style => "margin-right: 3px;", :title => l(:button_edit) %> - <%= link_to image_tag("/images/comment.png", :alt => "Edit"), "#", :onclick => "showInlineComment(this, '#{j(agile_inline_comment_path(:id => issue))}'); return false;", :title => l(:label_comment_add), :class => 'add-comment' %> + <%= link_to image_tag("/images/comment.png", alt: 'Comment'), '#', onclick: "showInlineComment(this, '#{j(agile_inline_comment_path(:id => issue))}'); return false;", title: l(:label_comment_add), class: 'add-comment' if RedmineAgile.allow_inline_comments? %> </div> <% end %> <% if @query.has_column_name?(:project) %> @@ -131,7 +135,7 @@ </script> <% end %> - <% if @update %> + <% if @update && !@version_board %> <script type="text/javascript"> $("table.issues-board thead").html("<%=escape_javascript render_board_headers(@query.board_statuses) %>"); </script> @@ -139,19 +143,6 @@ <script type="text/javascript"> agileBoard.initDroppable(); </script> - <script type="text/javascript"> - $("td.issue-status-col[data-id=<%= issue.status.id %>]").removeClass("over_wp_limit under_wp_limit"); - $("td.issue-status-col[data-id=<%= issue.status.id %>]").addClass("<%= @wp_class %>"); - <% if @old_status %> - $("td.issue-status-col[data-id=<%= @old_status.id %>]").removeClass("over_wp_limit under_wp_limit"); - $("td.issue-status-col[data-id=<%= @old_status.id %>]").addClass("<%= @wp_class_for_old_status %>"); - <% end %> - </script> - <% end %> - <% if @not_in_scope %> - <script type="text/javascript"> - $(".issue-card[data-id='<%= issue.id %>']").toggle( "highlight" ); - </script> <% end %> <% end %> <%= init_agile_tooltip_info %> diff --git a/plugins/redmine_agile/app/views/agile_boards/_issues_links.html.erb b/plugins/redmine_agile/app/views/agile_boards/_issues_links.html.erb index 60ac827..d25a0ff 100644 --- a/plugins/redmine_agile/app/views/agile_boards/_issues_links.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/_issues_links.html.erb @@ -1,21 +1,17 @@ <h3><%= l(:label_issue_plural) %></h3> - <ul> +<ul> <li><%= link_to l(:label_issue_view_all), _project_issues_path(@project, :set_filter => 1) %></li> <% if @project %> - <li><%= link_to l(:field_summary), project_issues_report_path(@project) %></li> + <li><%= link_to l(:field_summary), project_issues_report_path(@project) %></li> <% end %> - <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %> - <li><%= link_to l(:label_calendar), _project_calendar_path(@project) %></li> + <li><%= link_to l(:label_calendar), _project_calendar_path(@project) %></li> <% end %> <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %> - <li><%= link_to l(:label_gantt), _project_gantt_path(@project) %></li> + <li><%= link_to l(:label_gantt), _project_gantt_path(@project) %></li> <% end %> <% if User.current.allowed_to?(:view_agile_queries, @project) %> - <li><%= link_to l(:label_agile_board), {:controller => "agile_boards", :action => "index", :project_id => @project} %></li> - <% end %> - <% if User.current.allowed_to?(:manage_agile_verions, @project) %> - <li><%= link_to l(:label_agile_version_planning), {:controller => "agile_versions", :action => "index", :project_id => @project} %></li> + <li><%= link_to l(:label_agile_board), {:controller => "agile_boards", :action => "index", :project_id => @project} %></li> <% end %> - </ul> +</ul> diff --git a/plugins/redmine_agile/app/views/agile_boards/_issues_sidebar.html.erb b/plugins/redmine_agile/app/views/agile_boards/_issues_sidebar.html.erb deleted file mode 100755 index 77c8cc8..0000000 --- a/plugins/redmine_agile/app/views/agile_boards/_issues_sidebar.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% if User.current.allowed_to?(:view_agile_queries, @project, :global => true) %> - <%= link_to l(:label_agile_board), {:controller => "agile_boards", :action => "index", :project_id => @project} %> - <br> -<% end %> -<% if User.current.allowed_to?(:manage_agile_verions, @project) && @project.versions.open.any? %> - <%= link_to l(:label_agile_version_planning), {:controller => "agile_versions", :action => "index", :project_id => @project} %> - <br> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_boards/index.html.erb b/plugins/redmine_agile/app/views/agile_boards/index.html.erb old mode 100755 new mode 100644 index 269b466..e798b5b --- a/plugins/redmine_agile/app/views/agile_boards/index.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/index.html.erb @@ -1,52 +1,3 @@ <%= render 'index' %> -<%= javascript_tag do %> - var fullScreenState = localStorage.getItem('full-screen-board'); - if (fullScreenState == "true") { - $("html").toggleClass("agile-board-fullscreen"); - } - - function updateBoard() { - if ( $('html').hasClass('agile-board-fullscreen') ) { - if ( $('.ui-sortable-helper').length == 0 && - $('textarea:focus').length == 0 && $('.lock:visible').size() == 0 ) - { - var lastScrollPosition = $('.agile-board.autoscroll').scrollTop(); - $('.lock').show(); - $.ajax(location.href, { - dataType: 'script', - contentType: 'text/javascript', - success: function(){ - $(".agile-board.autoscroll").scrollTop(lastScrollPosition); - }, - error: function(){ - $(".agile-board.autoscroll").scrollTop(lastScrollPosition); - }, - complete: function(){ - $('.lock').hide(); - } - }); - } - }; - } - - - Visibility.every(20 * 1000, function () { - updateBoard(); - }); - - Visibility.change(function (e, state) { - if (state == 'visible') { - updateBoard(); - } - }); - - function checkBoardVisibility() { - if (Visibility.state() != 'visible') { - updateBoard(); - } - } - - setInterval(checkBoardVisibility, 1200000); -<% end %> <%= init_agile_tooltip_info %> <%= call_hook(:view_agile_board_bottom, { :issues => @issues, :project => @project, :query => @query }) %> diff --git a/plugins/redmine_agile/app/views/agile_boards/inline_comment.html.erb b/plugins/redmine_agile/app/views/agile_boards/inline_comment.html.erb index 62349b2..a030bdf 100644 --- a/plugins/redmine_agile/app/views/agile_boards/inline_comment.html.erb +++ b/plugins/redmine_agile/app/views/agile_boards/inline_comment.html.erb @@ -1,3 +1,3 @@ <%= text_area_tag "issue[notes]", "", :cols => 60, :rows => 5, :class => 'wiki-edit', :no_label => true %> -<button type="button" onclick="agileBoard.saveInlineComment(this, '<%= update_agile_board_path(:id => @issue) %>'); return false;"> <%= l(:button_submit) %> </button> +<button type="button" onclick="saveInlineComment(this, '<%= update_agile_board_path(:id => @issue) %>'); return false;"> <%= l(:button_submit) %> </button> <%= link_to l(:button_cancel), "#", :onclick => "cancelInlineComment(this);return false;" %> diff --git a/plugins/redmine_agile/app/views/agile_charts/_agile_charts.html.erb b/plugins/redmine_agile/app/views/agile_charts/_agile_charts.html.erb index 9621af2..acbe13e 100644 --- a/plugins/redmine_agile/app/views/agile_charts/_agile_charts.html.erb +++ b/plugins/redmine_agile/app/views/agile_charts/_agile_charts.html.erb @@ -1,13 +1,5 @@ <% if User.current.allowed_to?(:view_agile_charts, @project, :global => true) %> -<h3><%= l(:label_agile_chart_plural) %></h3> -<ul> - <li><%= link_to l(:label_agile_charts_issues_burndown), {:controller => "agile_charts", :action => "show", :chart => "issues_burndown", :project_id => @project}, :class => "#{'selected' if @chart == 'issues_burndown'}" %></li> - <li><%= link_to l(:label_agile_charts_burnup), {:controller => "agile_charts", :action => "show", :chart => "burnup", :project_id => @project}, :class => "#{'selected' if @chart == 'burnup'}" %></li> - <li><%= link_to l(:label_agile_charts_issues_velocity), {:controller => "agile_charts", :action => "show", :chart => "issues_velocity", :project_id => @project}, :class => "#{'selected' if @chart == 'issues_velocity'}" %></li> - <li><%= link_to l(:label_agile_charts_lead_time), {:controller => "agile_charts", :action => "show", :chart => "lead_time", :project_id => @project}, :class => "#{'selected' if @chart == 'lead_time'}" %></li> - <li><%= link_to l(:label_agile_charts_trackers_cumulative_flow), {:controller => "agile_charts", :action => "show", :chart => "trackers_cumulative_flow", :project_id => @project}, :class => "#{'selected' if @chart == 'trackers_cumulative_flow'}" %></li> -</ul> -<% end %> -<% if User.current.allowed_to?(:view_agile_queries, @project, :global => true) %> - <%= render_sidebar_agile_queries %> + + <ul class=agile-chart-queries> + </ul> <% end %> diff --git a/plugins/redmine_agile/app/views/agile_charts/_chart.html.erb b/plugins/redmine_agile/app/views/agile_charts/_chart.html.erb index 6a30b07..8fb57da 100644 --- a/plugins/redmine_agile/app/views/agile_charts/_chart.html.erb +++ b/plugins/redmine_agile/app/views/agile_charts/_chart.html.erb @@ -5,15 +5,29 @@ <% else %> <div class="agile-chart-container"> <canvas id="agile-chart"></canvas> + <div style="clear: both;"></div> </div> <% end %> <%= javascript_tag do %> $(document).ready(function(){ + Chart.plugins.register({ + beforeDraw: function(chartInstance, easing) { + if (chartInstance.config.options.tooltips.onlyShowForDatasetIndex) { + var tooltipsToDisplay = chartInstance.config.options.tooltips.onlyShowForDatasetIndex; + var active = chartInstance.tooltip._active || []; + if (active.length > 0) { + if (tooltipsToDisplay.indexOf(active[0]._datasetIndex) === -1) { + chartInstance.tooltip._model.opacity = 0; + } + } + } + } + }); if (document.getElementById("agile-chart")) { - $.getJSON(<%= raw url_for(:controller => 'agile_charts', :action => 'render_chart', :project_id => @project, - :chart => chart, :version_id => @version).to_json %>, function(data){ + $.getJSON(<%= raw url_for(controller: 'agile_charts', action: 'render_chart', project_id: @project, + chart: chart, version_id: @version, query_id: @query.try(:id), chart_unit: chart_unit).to_json %>, function(data){ Chart.defaults.global.defaultFontColor = 'black'; Chart.defaults.global.defaultFontFamily = '"Arial", sans-serif'; @@ -25,40 +39,58 @@ stacked: data['stacked'] }; - new Chart(document.getElementById("agile-chart").getContext("2d"), { - type: 'bar', - data: chartData, - options: { - maintainAspectRatio: false, - legend: { position: 'right' }, - title: { - display: true, - fontSize: 16, - fontStyle: 'normal', - text: data['title'] - }, - elements: { - line: { - tension: 0, // disables bezier curves + new Chart(document.getElementById("agile-chart").getContext("2d"), { + type: data['type'] || 'bar', + data: chartData, + options: { + tooltips: { + onlyShowForDatasetIndex: data['show_tooltips'], + callbacks: chartTooltipCallbacks(data['type']) + }, + maintainAspectRatio: false, + legend: { + position: 'right', + labels: { + filter: function(legendItem, chartData) { + if (legendItem.text) { return true } } - }, - scales: { - yAxes: [{ - stacked: data['stacked'], - scaleLabel: { - display: true, - fontColor: 'rgba(255, 0, 0 ,1)', - fontSize: 14, - labelString: data['y_title'] - } - }], - xAxes: [{ - ticks: { - autoSkip: true, - maxRotation: 0 - } - }] } + }, + title: { + display: true, + fontSize: 16, + fontStyle: 'normal', + text: data['title'] + }, + elements: { + line: { + tension: 0, // disables bezier curves + } + }, + scales: { + yAxes: [{ + stacked: data['stacked'], + scaleLabel: { + display: true, + fontColor: 'rgba(255, 0, 0 ,1)', + fontSize: 14, + labelString: data['y_title'] + } + }], + xAxes: [{ + ticks: { + autoSkip: true, + maxRotation: 0, + userCallback: function(value, index, values) { + if (data['type'] == 'scatter') { + return data['labels'][value] + } else { + return value + } + } + } + }] + }, } }); }); diff --git a/plugins/redmine_agile/app/views/agile_charts/_versions_show.html.erb b/plugins/redmine_agile/app/views/agile_charts/_versions_show.html.erb index 16f2117..79c9725 100644 --- a/plugins/redmine_agile/app/views/agile_charts/_versions_show.html.erb +++ b/plugins/redmine_agile/app/views/agile_charts/_versions_show.html.erb @@ -1,17 +1,31 @@ <% if @issues.any? && User.current.allowed_to?(:view_agile_charts, @project) %> <fieldset> <legend> - <%= l(:label_agile_chart) %> - <%= select_tag('chart', options_charts_for_select(params[:chart] || RedmineAgile.default_chart), - :id => 'chart_by_select', - :data => {:remote => true, :method => 'get', :url => agile_charts_select_version_chart_path(:version_id => @version)}) %> + <%= l(:label_agile_chart) %> + <%= select_tag('chart', options_charts_for_select(params[:chart] || RedmineAgile.default_chart), + id: 'chart_by_select', + onchange: "toggleChartUnit($(this).val(), 'chart-unit-row'); updateVersionAgileChart('#{agile_charts_select_version_chart_path(version_id: @version)}');") %> + + <span id="chart-unit-row"> + <label for='chart_unit'><%= l(:label_agile_chart_units) %></label> + <%= select_tag 'chart_unit', options_chart_units_for_select, + onchange: "updateVersionAgileChart('#{agile_charts_select_version_chart_path(version_id: @version)}');" %> + </span> </legend> <div id='agile_chart'> - <%= render_agile_chart(params[:chart] || RedmineAgile.default_chart, @version.fixed_issues) %> + <%= render_agile_chart(RedmineAgile::Charts.valid_chart_name_by(params[:chart] || RedmineAgile.default_chart), @version.fixed_issues) %> </div> </fieldset> <% end %> <% content_for :header_tags do %> <%= chartjs_assets %> + <%= javascript_include_tag 'redmine_agile', plugin: 'redmine_agile' %> +<% end %> + +<%= javascript_tag do %> + var chartsWithUnits = <%= raw RedmineAgile::Charts::CHARTS_WITH_UNITS.to_json %> + $(document).ready(function() { + toggleChartUnit($('#chart_by_select').val(), 'chart-unit-row'); + }); <% end %> diff --git a/plugins/redmine_agile/app/views/agile_charts/show.html.erb b/plugins/redmine_agile/app/views/agile_charts/show.html.erb index f9d0804..a92392f 100644 --- a/plugins/redmine_agile/app/views/agile_charts/show.html.erb +++ b/plugins/redmine_agile/app/views/agile_charts/show.html.erb @@ -3,32 +3,33 @@ <h2><%= @query.new_record? ? l(:label_agile_chart_plural) : h(@query.name) %></h2> <% html_title(@query.new_record? ? l(:label_agile_chart_plural) : @query.name) %> - <%= form_tag({ :controller => 'agile_charts', :action => 'show', :project_id => @project }, :method => :get, :id => 'query_form') do %> <div id="query_form_with_buttons" class="hide-when-print"> <%= hidden_field_tag 'set_filter', '1' %> <div id="query_form_content"> - <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> - <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> - <div style="<%= @query.new_record? ? "" : "display: none;" %>"> + <fieldset id="filters" class="collapsible"> + <legend class="icon icon-expended" onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> + <div> <%= render :partial => 'queries/filters', :locals => {:query => @query} %> </div> </fieldset> - <fieldset class="collapsible collapsed"> - <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> + <fieldset class="collapsible"> + <legend class="icon icon-expended" onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> <div> <table> <tr> <td><label for='chart'><%= l(:label_agile_chart) %></label></td> - <td><%= select_tag('chart', options_charts_for_select(@chart)) %></td> + <td><%= select_tag 'chart', options_charts_for_select(@chart), onchange: "toggleChartUnit($(this).val(), 'chart-unit-row');" %></td> + <td id="chart-unit-row"> + <label for='chart_unit'><%= l(:label_agile_chart_units) %></label> + <%= select_tag 'chart_unit', options_chart_units_for_select(@query.chart_unit) %> + </td> </tr> - <tr> - <td><%= l(:label_agile_date_from) %></td> + <tr id="interval-size"> + <td><%= l(:label_agile_interval_size) %></td> <td> - <%= text_field_tag 'date_from', @query.date_from, :size => 10 %><%= calendar_for('date_from') %> - <%= l(:label_agile_date_to) %> - <%= text_field_tag 'date_to', @query.date_to, :size => 10 %><%= calendar_for('date_to') %> + <%= select_tag 'interval_size', options_for_select(RedmineAgile::AgileChart::TIME_INTERVALS.map { |i| [l(:"label_agile_#{i}"), i] }, @query.interval_size) %> </td> </tr> </table> @@ -48,9 +49,19 @@ <% content_for :header_tags do %> <%= chartjs_assets %> + <%= javascript_include_tag 'redmine_agile', plugin: 'redmine_agile' %> <% end %> <% content_for :sidebar do %> <%= render :partial => 'agile_boards/issues_links' %> <%= render :partial => 'agile_charts/agile_charts' %> <% end %> + +<%= javascript_tag do %> + var chartsWithUnits = <%= raw RedmineAgile::Charts::CHARTS_WITH_UNITS.to_json %> + $(document).ready(function() { + toggleChartUnit($('#chart').val(), 'chart-unit-row'); + /* Hide chart_period checkbox so that it couldn't be unchecked */ + hideChartPeriodCheckbox(); + }); +<% end %> diff --git a/plugins/redmine_agile/app/views/agile_colors/index.html.erb b/plugins/redmine_agile/app/views/agile_colors/index.html.erb deleted file mode 100644 index 8440f02..0000000 --- a/plugins/redmine_agile/app/views/agile_colors/index.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<h2><%=l(:label_agile_color) %> <%= select_tag "object_type", options_for_select([[l(:label_agile_tracker_colors), "tracker"], [l(:label_agile_issue_priority_colors), "issue_priority"]], params[:object_type]), :onchange => "location = this.options[this.options.selectedIndex].value" %></h2> - -<%= form_tag(update_agile_colors_path(:object_type => params[:object_type]), :method => :put) do %> - <% if @coloreds.any? %> - <table class="list"><thead> - <tr> - <th><%= l(:field_name) %></th> - </tr></thead> - <% @coloreds.each do |colored| %> - <tr class="<%= cycle('odd', 'even') %>"> - <td class="name"> - <%= hidden_field_tag 'coloreds[][id]', colored.id %> - <%= select_tag 'coloreds[][color]', options_for_select(AgileColor::AGILE_COLORS, colored.color), :include_blank => true, :id => "color_select_#{colored.id}", :class => "agile-color" %> <%= colored %> - </td> - </tr> - <% end %> - </table> - <%= javascript_tag "$('.agile-color').simplecolorpicker({picker: true});"%> - - <% end %> - <%= submit_tag l(:button_save) %> -<% end %> - -<% content_for :header_tags do %> - <%= javascript_include_tag 'jquery.simplecolorpicker.js', :plugin => "redmine_agile" %> - <%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_agile' %> -<% end %> - -<% html_title(l(:enumeration_issue_priorities)) -%> diff --git a/plugins/redmine_agile/app/views/agile_journal_details/status.html.erb b/plugins/redmine_agile/app/views/agile_journal_details/status.html.erb index 90f6d07..10dcb43 100644 --- a/plugins/redmine_agile/app/views/agile_journal_details/status.html.erb +++ b/plugins/redmine_agile/app/views/agile_journal_details/status.html.erb @@ -2,7 +2,30 @@ <% html_title(l(:label_issue_status_plural)) %> -<% if @statuses.any? %> +<%= form_tag({ controller: 'agile_journal_details', action: 'status' }, method: :get, id: 'query_form') do %> + <div id="query_form_with_buttons" class="hide-when-print"> + <div id="query_form_content"> + <fieldset id="options" class="collapsible collapsed"> + <legend onclick="toggleFieldset(this);" class="icon icon-expended"><%= l(:label_options) %></legend> + <div style="display: none;"> + <table> + <tr> + <td><label for='group_by'><%= l(:field_group_by) %></label></td> + <td><%= select_tag('group_by', options_for_select([[]] + [[l(:field_status), 'status']], params[:group_by])) %></td> + </tr> + </table> + </div> + </fieldset> + </div> + </div> + + <p class="buttons hide-when-print"> + <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { }, :class => 'icon icon-reload' %> + </p> +<% end %> + +<% if @statuses_collector.data.any? %> <table class="list"><thead> <tr> <th>#</th> @@ -10,18 +33,38 @@ <th><%= l(:field_status) %></th> <th><%= l(:field_duration) %></th> <th><%= l(:field_author) %></th> + <th><%= l(:field_assigned_to) %></th> </tr></thead> - <% @statuses.each_with_index do |status, index| %> - <% issue_status = IssueStatus.where(:id => status.value).first %> - <tr class="<%= cycle('odd', 'even') %>"> - <td class="index"><%= index + 1 %></td> - <td class="name"><%= format_time(status.journal.created_on) %></td> - <td><%= issue_status.name %></td> - <td><%= event_duration(status, @statuses[index + 1]) %></td> - <td><%= status.journal.user.name %></td> - </tr> + <% if @group %> + <% @statuses_collector.grouped_by(@group).each do |group_id, group_data| %> + <% group_object = @statuses_collector.object_for(@group).where(:id => group_id).first %> + <tr class="group open"> + <td colspan="6"> + <span class="expander icon icon-expended" onclick="toggleRowGroup(this);"> </span> + <span class="name"><%= group_object.name %></span> + <span class="badge badge-count count"><%= group_data.count %></span> + <span class="totals"> + <%= l('datetime.distance_in_words.x_days', count: @statuses_collector.group_total_for(@group, group_data)) %> + </span> + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", + "toggleAllRowGroups(this)", :class => 'toggle-all') %> + </td> + </tr> + <% group_data.each_with_index do |data, index| %> + <%= render partial: 'status_detail', locals: { issue_status: @statuses_collector.issue_status_for(@group, group_id), data: data, index: index } %> + <% end %> + <% end %> + <% else %> + <% @statuses_collector.data.each_with_index do |data, index| %> + <% issue_status = IssueStatus.where(:id => data.status_id).first %> + <%= render partial: 'status_detail', locals: { issue_status: issue_status, data: data, index: index } %> + <% end %> <% end %> </table> <% else %> -<p class="nodata"><%= l(:label_no_data) %></p> + <p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% other_formats_links do |f| %> + <%= f.link_to 'CSV', :url => params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> <% end %> diff --git a/plugins/redmine_agile/app/views/agile_queries/_columns.html.erb b/plugins/redmine_agile/app/views/agile_queries/_columns.html.erb deleted file mode 100644 index 117acd8..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/_columns.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<table class="query-columns"> - <tr> - <td style="padding-left:0"> - <%= label_tag "available_columns", l(:description_available_columns) %> - <br /> - <%= select_tag 'available_columns', - options_for_select(query_available_inline_columns_options(query)), - :multiple => true, :size => 10, :style => "width:150px", - :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %> - </td> - <td class="buttons"> - <input type="button" value="→" - onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br /> - <input type="button" value="←" - onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" /> - </td> - <td> - <%= label_tag "selected_columns", l(:description_selected_columns) %> - <br /> - <%= select_tag tag_name, - options_for_select(query_selected_inline_columns_options(query)), - :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px", - :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);" %> - </td> - <td class="buttons"> - <input type="button" value="↑" onclick="moveOptionUp(this.form.selected_columns);" /><br /> - <input type="button" value="↓" onclick="moveOptionDown(this.form.selected_columns);" /> - </td> - </tr> -</table> - -<%= javascript_tag do %> -$(document).ready(function(){ - $('.query-columns').closest('form').submit(function(){ - $('#selected_columns option').prop('selected', true); - }); -}); -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_queries/_filters.html.erb b/plugins/redmine_agile/app/views/agile_queries/_filters.html.erb deleted file mode 100644 index f12d2b0..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/_filters.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%= javascript_tag do %> -var operatorLabels = <%= raw_json Query.operators_labels %>; -var operatorByType = <%= raw_json Query.operators_by_filter_type %>; -var availableFilters = <%= raw_json query.available_filters_as_json %>; -var labelDayPlural = <%= raw_json l(:label_day_plural) %>; -var allProjects = <%= raw_json query.all_projects_values %>; -$(document).ready(function(){ - initFilters(); - <% query.filters.each do |field, options| %> - addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>); - <% end %> -}); -<% end %> - -<table style="width:100%"> -<tr> -<td> -<table id="filters-table"> -</table> -</td> -<td class="add-filter" style="width: initial;"> -<%= label_tag('add_filter_select', l(:label_filter_add)) %> -<%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> -</td> -</tr> -</table> -<%= hidden_field_tag 'f[]', '' %> -<% include_calendar_headers_tags %> diff --git a/plugins/redmine_agile/app/views/agile_queries/_form.html.erb b/plugins/redmine_agile/app/views/agile_queries/_form.html.erb deleted file mode 100644 index 455ace0..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/_form.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<%= error_messages_for 'query' %> - -<div class="box"> -<div class="tabular"> - <%= hidden_field_tag 'gantt', '1' if params[:gantt] %> - - <p><label for="query_name"><%=l(:field_name)%></label> - <%= text_field 'query', 'name', :size => 80 %></p> - - <% if User.current.admin? || User.current.allowed_to?(:manage_public_agile_queries, @project) %> - <% if Redmine::VERSION.to_s < "2.4" %> - <p><label for="query_is_public"><%=l(:field_is_public)%></label> - <%= check_box 'query', 'is_public', - :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %></p> - <% else %> - <p><label><%=l(:field_visible)%></label> - <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label> - <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label> - <% Role.givable.sorted.each do |role| %> - <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label> - <% end %> - <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label> - <%= hidden_field_tag 'query[role_ids][]', '' %> - </p> - <% end %> - <% end %> - - <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label> - <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, - :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p> - <% if Redmine::VERSION.to_s > "2.4" %> - <p> - <label> - <%= l(:label_agile_default_board) %> - </label> - <%= check_box_tag 'query[is_default]', 1, @query.is_default? %> - </p> - <% end %> - <fieldset><legend><%= l(:label_options) %></legend> - <p><label for="query_default_columns"><%=l(:label_agile_board_default_fields)%></label> - <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', - :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p> - - <% if RedmineAgile.use_colors? && @query.respond_to?(:color_base) %> - <p><label for='color_base'><%= l(:label_agile_color_based_on) %></label> - <%= select 'query', 'color_base', options_for_select(options_card_colors_for_select(@query.color_base)) %></p> - <% end %> - <p><label for="query_group_by"><%= l(:label_agile_swimlanes) %></label> - <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %> - </p> - </fieldset> -</div> - -<fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> -<%= render :partial => 'agile_queries/filters', :locals => {:query => query}%> -</fieldset> - -<% if Redmine::VERSION.to_s > '2.4' %> - <%= content_tag 'fieldset', :class => "card-fields" do %> - <legend><%= l(:label_agile_board_columns) %></legend> - <%= render_board_fields_status(query) %> - <% end %> -<% end %> - - -<%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil), :class => "card-fields" do %> -<legend><%= l(:label_agile_fields) %></legend> -<%= render_board_fields_selection(query) %> -<% end %> - -</div> - -<%= javascript_tag do %> -$(document).ready(function(){ - $("input[name='query[visibility]']").change(function(){ - var checked = $('#query_visibility_1').is(':checked'); - $("input[name='query[role_ids][]'][type=checkbox]").attr('disabled', !checked); - }).trigger('change'); -}); -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_queries/edit.html.erb b/plugins/redmine_agile/app/views/agile_queries/edit.html.erb deleted file mode 100644 index d2b3111..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%= l(:label_agile_board_edit) %></h2> - -<%= form_tag(agile_query_path(@query), :method => :put) do %> - <%= render :partial => 'form', :locals => {:query => @query} %> - <%= submit_tag l(:button_save) %> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_queries/index.html.erb b/plugins/redmine_agile/app/views/agile_queries/index.html.erb deleted file mode 100644 index e545c5e..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/index.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<div class="contextual"> -<%= link_to l(:label_query_new), new_agile_query_path(:project_id => @project), :class => 'icon icon-add' if User.current.allowed_to?(:add_agile_queries, nil, :global => true)%> -</div> - -<h2><%= l(:label_agile_board_plural) %></h2> - -<% if @queries.empty? %> - <p><i><%=l(:label_no_data)%></i></p> -<% else %> - <table class="list"> - <% @queries.each do |query| %> - <tr class="<%= cycle('odd', 'even') %>"> - <td class="name"> - <%= link_to h(query.name), :controller => 'agile_boards', :action => 'index', :project_id => query.project_id, :query_id => query %> - </td> - <td class="buttons"> - <% if query.editable_by?(User.current) %> - <%= link_to l(:button_edit), edit_agile_query_path(query), :class => 'icon icon-edit' %> - <%= delete_link agile_query_path(query) %> - <% end %> - </td> - </tr> - <% end %> - </table> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_queries/new.html.erb b/plugins/redmine_agile/app/views/agile_queries/new.html.erb deleted file mode 100644 index 870ed12..0000000 --- a/plugins/redmine_agile/app/views/agile_queries/new.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%= l(:label_agile_board_new) %></h2> - -<%= form_tag(@project ? project_agile_queries_path(@project) : agile_queries_path) do %> - <%= render :partial => 'form', :locals => {:query => @query} %> - <%= submit_tag l(:button_save) %> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_versions/_board.html.erb b/plugins/redmine_agile/app/views/agile_versions/_board.html.erb deleted file mode 100644 index e62b5d1..0000000 --- a/plugins/redmine_agile/app/views/agile_versions/_board.html.erb +++ /dev/null @@ -1,43 +0,0 @@ -<%= form_tag({}) do -%> - <%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> - <%= hidden_field_tag 'project_id', @project.id if @project %> -<div class="autoscroll"> - <div class="flash error" style="display: none;" id="agile-board-errors"> - </div> - <table class="list versions-planning-board"> - <thead> - <tr> - <th style="width: 33%; text-align: left;"> - <%= text_field_tag "search", '', :placeholder => l(:label_agile_no_version_issues) %> - <%= javascript_tag "observeIssueSearchfield('search', '#{escape_javascript autocomplete_agile_versions_path(:project_id => @project)}');" %> - </th> - <th style="width: 33%; text-align: left;" id="backlog_version_header" data-estimated-unit="<%= estimated_unit%>"> - <%= version_select_tag(@backlog_version, :version_type => "backlog", :other_version => @current_version) %> - </th> - <th style="width: 33%; text-align: left;" id="current_version_header" data-estimated-unit="<%= estimated_unit%>"> - <%= version_select_tag(@current_version, :version_type => "current", :other_version => @backlog_version) %> - </th> - </tr> - </thead> - <tbody> - <tr> - <td id="no_version" class="issue-version-col" data-id=""> - <%= render :partial => 'version_issues', :object => @version_issues %> - </td> - <td id="backlog_version" class="<%= 'issue-version-col' if @backlog_version %>" data-id="<%= @backlog_version && @backlog_version.id %>"> - <%= render :partial => 'version_issues', :object => @backlog_version_issues if @backlog_version %> - </td> - <td id="current_version" class="<%= 'issue-version-col' if @current_version %>" data-id="<%= @current_version && @current_version.id %>"> - <%= render :partial => 'version_issues', :object => @current_version_issues if @current_version %> - </td> - </tr> - </tbody> - </table> -</div> - -<% end %> - -<%= javascript_tag do %> - recalculateHours(); - $(document).ajaxSuccess(recalculateHours); -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_versions/_version_issues.html.erb b/plugins/redmine_agile/app/views/agile_versions/_version_issues.html.erb deleted file mode 100644 index 278ce44..0000000 --- a/plugins/redmine_agile/app/views/agile_versions/_version_issues.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% if version_issues.any? %> - <% version_issues.each do |issue| %> - <% issue_estmated_value = estimated_value(issue) %> - <div class="issue-card hascontextmenu" data-id="<%= issue.id %>" data-estimated-hours="<%= issue_estmated_value %>"> - <% if issue_estmated_value > 0 %> - <span class="hours"><%= ("%.2f" % issue_estmated_value) + estimated_unit %></span> - <% end %> - <%= check_box_tag("ids[]", issue.id, false, :id => nil, :class => 'checkbox') %> - <%= link_to_issue(issue, :project => (@project != issue.project)) %> - </div> - <% end %> - - <% if version_issues.first.fixed_version_id.blank? %> - <div class="pagination-wrapper"> - <span class="pagination"> - <% pagination_links_full(@issue_pages, @issue_count, :per_page_links => false) do |text, parameters, options| %> - <%= link_to text, autocomplete_agile_versions_path(parameters.merge(:q => params[:q], :project_id => @project)), :remote => true %> - <% end %> - </span> - </div> - <% end %> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_versions/autocomplete.js.erb b/plugins/redmine_agile/app/views/agile_versions/autocomplete.js.erb deleted file mode 100644 index 3c6c1ac..0000000 --- a/plugins/redmine_agile/app/views/agile_versions/autocomplete.js.erb +++ /dev/null @@ -1 +0,0 @@ -$('#no_version').html('<%= escape_javascript(render :partial => "version_issues", :object => @version_issues) %>') diff --git a/plugins/redmine_agile/app/views/agile_versions/index.html.erb b/plugins/redmine_agile/app/views/agile_versions/index.html.erb deleted file mode 100644 index 318d206..0000000 --- a/plugins/redmine_agile/app/views/agile_versions/index.html.erb +++ /dev/null @@ -1,56 +0,0 @@ -<h2><%= l(:label_agile_version_planning) %></h2> -<div class="contextual"> -<% if !@query.new_record? && @query.editable_by?(User.current) %> - <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %> - <%= delete_link query_path(@query) %> -<% end %> -</div> - -<%= form_tag({ :controller => 'agile_versions', :action => 'index', :project_id => @project }, - :method => :get, :id => 'query_form') do %> - <div id="query_form_with_buttons" class="hide-when-print"> - <%= hidden_field_tag 'set_filter', '1' %> - <div id="query_form_content"> - <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> - <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> - <div style="<%= @query.new_record? ? "" : "display: none;" %>"> - <%= render :partial => 'queries/filters', :locals => {:query => @query} %> - </div> - </fieldset> - </div> - <p class="buttons"> - <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> - <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> - </p> - </div> -<% end %> - -<% if User.current.allowed_to?(:edit_issues, @project) %> - <%= javascript_tag do %> - new PlanningBoard().init({ - project_id: '<%= @project && @project.id %>', - update_agile_board_path: '<%= escape_javascript update_agile_board_path %>', - issues_path: '<%= escape_javascript issues_path %>' - }); - <% end %> -<% end %> - -<% if @project.shared_versions.empty? %> - <p class="nodata"><%= l(:label_no_data) %></p> -<% else %> - <%= render :partial => 'board' %> -<% end %> - -<% content_for :sidebar do %> - <%= render :partial => 'agile_boards/issues_links' %> - <%= render :partial => 'agile_charts/agile_charts' %> -<% end %> - -<% html_title l(:label_agile_version_planning) %> -<%= javascript_tag "agileContextMenuInit('#{ url_for(issues_context_menu_path) }')" %> -<% content_for :header_tags do %> - <%= javascript_include_tag "redmine_agile", :plugin => 'redmine_agile' %> - <%= javascript_include_tag "jquery.ui.touch-punch.js", :plugin => 'redmine_agile' %> - <%= javascript_include_tag "redmine_agile_context_menu", :plugin => 'redmine_agile' %> - <%= stylesheet_link_tag 'context_menu' %> -<% end %> diff --git a/plugins/redmine_agile/app/views/agile_versions/load.js.erb b/plugins/redmine_agile/app/views/agile_versions/load.js.erb deleted file mode 100644 index 496b465..0000000 --- a/plugins/redmine_agile/app/views/agile_versions/load.js.erb +++ /dev/null @@ -1,5 +0,0 @@ -$('#<%= @version_type %>_version').html('<%= escape_javascript(render :partial => "version_issues", :object => @version_issues) %>'); -$('#<%= @version_type %>_version').attr('data-id', '<%= @version.id %>'); - -$('#<%= @version_type %>_version_header').html('<%= escape_javascript(version_select_tag(@version, :version_type => @version_type, :other_version => @other_version_id)) %>'); -$('#<%= @other_version_type %>_version_header').html('<%= escape_javascript(version_select_tag(@other_version_id, :version_type => @other_version_type, :other_version => @version)) %>'); diff --git a/plugins/redmine_agile/app/views/context_menus/_agile_colors.html.erb b/plugins/redmine_agile/app/views/context_menus/_agile_colors.html.erb deleted file mode 100644 index c719d73..0000000 --- a/plugins/redmine_agile/app/views/context_menus/_agile_colors.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% if @safe_attributes.include?('agile_color_attributes') %> -<li class="folder"> - <a href="#" class="submenu"><%= l(:label_agile_color) %></a> - <ul> - <% AgileColor::AGILE_COLORS.each do |k, color| %> - <li><%= context_menu_link "<span style=\"background-color: #{color}; width: 8em; height: 1.2em; display: block;border: 1px solid #222;\"></span>".html_safe, bulk_update_issues_path(:ids => @issue_ids, :issue => {:agile_color_attributes => {:color => color}}, :back_url => @back), :method => :post, - :selected => (@issue && @issue.color == color), :disabled => (!@can[:edit]) %></li> - <% end -%> - </ul> -</li> -<% end %> diff --git a/plugins/redmine_agile/app/views/issues/_agile_data_fields.html.erb b/plugins/redmine_agile/app/views/issues/_agile_data_fields.html.erb index d32aa6e..be8cdbf 100644 --- a/plugins/redmine_agile/app/views/issues/_agile_data_fields.html.erb +++ b/plugins/redmine_agile/app/views/issues/_agile_data_fields.html.erb @@ -1,9 +1,9 @@ <div class="attributes"> - <div class="splitcontent"> - <%= render :partial => 'issue_color_form', :locals => { :form => form } %> - <%= render :partial => 'issue_story_points_form', :locals => { :form => form } %> - </div> + <div class="splitcontent"> + <%= render :partial => 'issue_story_points_form', :locals => { :form => form } %> + </div> </div> + <% content_for :header_tags do %> - <%= javascript_include_tag "redmine_agile", :plugin => 'redmine_agile' %> + <%= javascript_include_tag "redmine_agile", :plugin => 'redmine_agile' %> <% end %> diff --git a/plugins/redmine_agile/app/views/issues/_issue_color.html.erb b/plugins/redmine_agile/app/views/issues/_issue_color.html.erb deleted file mode 100644 index 467ad9b..0000000 --- a/plugins/redmine_agile/app/views/issues/_issue_color.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% if User.current.allowed_to?(:view_agile_queries, @project) && @issue.color && RedmineAgile.issue_colors? %> - -<%= issue_fields_rows do |rows| - rows.left l(:label_agile_color), "<span class=\"simplecolorpicker button\" style=\"background-color: #{@issue.color};\"></span>".html_safe -end %> - - <% content_for :header_tags do %> - <%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_agile' %> - <% end %> - -<% end %> diff --git a/plugins/redmine_agile/app/views/issues/_issue_color_form.html.erb b/plugins/redmine_agile/app/views/issues/_issue_color_form.html.erb deleted file mode 100644 index 2cc4f25..0000000 --- a/plugins/redmine_agile/app/views/issues/_issue_color_form.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<div class="splitcontentleft"> - <% if @issue.safe_attribute? 'agile_color_attributes' -%> - <%= form.fields_for :agile_color do |f| %> - <p><%= f.select :color, options_for_select(AgileColor::AGILE_COLORS, @issue.color), {:label => l(:label_agile_color), :include_blank => true} %></p> - <% end %> - - <%= javascript_tag "$('#issue_agile_color_attributes_color').simplecolorpicker({picker: true});"%> - <% content_for :header_tags do %> - <%= javascript_include_tag 'jquery.simplecolorpicker.js', :plugin => "redmine_agile" %> - <%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_agile' %> - <% end %> - - <% end %> -</div> diff --git a/plugins/redmine_agile/app/views/issues/_issue_story_points_form.html.erb b/plugins/redmine_agile/app/views/issues/_issue_story_points_form.html.erb index ab0f94d..9761dd2 100644 --- a/plugins/redmine_agile/app/views/issues/_issue_story_points_form.html.erb +++ b/plugins/redmine_agile/app/views/issues/_issue_story_points_form.html.erb @@ -1,14 +1,9 @@ -<div class="splitcontentright"> - <% if @issue.project.module_enabled?('agile') && RedmineAgile.use_story_points? && RedmineAgile.use_story_points_for?(@issue.tracker) %> - <%= form.fields_for :agile_data do |f| %> - <p> - <% if RedmineAgile.sp_values.any? %> - <%= f.select :story_points, RedmineAgile.sp_values.unshift(@issue.story_points).uniq, - :include_blank => @issue.story_points, :selected => @issue.story_points %> - <% else %> - <%= f.text_field :story_points, :size => 3, :required => @issue.required_attribute?('agile_data_attributes') %> +<% if @issue.project.module_enabled?('agile') && RedmineAgile.use_story_points? && RedmineAgile.use_story_points_for?(@issue.tracker) %> + <div class="splitcontentright"> + <%= form.fields_for :agile_data do |f| %> + <p> + <%= f.text_field :story_points, :size => 3, :required => @issue.required_attribute?('agile_data_attributes') %> + </p> <% end %> - </p> - <% end %> - <% end %> -</div> + </div> +<% end %> diff --git a/plugins/redmine_agile/app/views/projects/_project_color_form.html.erb b/plugins/redmine_agile/app/views/projects/_project_color_form.html.erb deleted file mode 100644 index 5b5d658..0000000 --- a/plugins/redmine_agile/app/views/projects/_project_color_form.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% if @project.safe_attribute? 'agile_color_attributes' -%> - <%= form.fields_for :agile_color do |fr| %> - <p><%= fr.select :color, options_for_select(AgileColor::AGILE_COLORS, @project.color), {:label => l(:label_agile_color), :include_blank => true} %></p> - <% end %> - - <%= javascript_tag "$('#project_agile_color_attributes_color').simplecolorpicker({picker: true});"%> - <% content_for :header_tags do %> - <%= javascript_include_tag 'jquery.simplecolorpicker.js', :plugin => "redmine_agile" %> - <%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_agile' %> - <% end %> -<% end %> diff --git a/plugins/redmine_agile/app/views/settings/agile/_general.html.erb b/plugins/redmine_agile/app/views/settings/agile/_general.html.erb old mode 100755 new mode 100644 index 5373c1c..f0059ee --- a/plugins/redmine_agile/app/views/settings/agile/_general.html.erb +++ b/plugins/redmine_agile/app/views/settings/agile/_general.html.erb @@ -3,16 +3,6 @@ <label><%= l(:label_agile_board_items_limit) %></label> <%= text_field_tag 'settings[board_items_limit]', RedmineAgile.board_items_limit, :size => 3 %> </p> -<p> - <label for="settings_status_colors"><%= l(:label_agile_status_colors) %></label> - <%= hidden_field_tag 'settings[status_colors]', 0, :id => nil %> - <%= check_box_tag 'settings[status_colors]', 1, RedmineAgile.status_colors? %> -</p> - -<p> - <label><%= l(:label_agile_color_based_on) %></label> - <%= select_tag 'settings[color_on]', options_card_colors_for_select(@settings["color_on"]) %> <%= link_to l(:label_agile_manage_colors), agile_colors_path("tracker") %> -</p> <p> <label><%= l(:label_agile_board_default_fields) %></label> @@ -20,31 +10,21 @@ </p> <p> - <label><%= l(:label_agile_esitmate_units) %></label> - <%= select_tag 'settings[estimate_units]', options_for_select(RedmineAgile::ESTIMATE_UNITS.map{ |eu| [l("label_agile_#{eu}"), eu] }, - RedmineAgile.estimate_units), :onchange => "toggleTrackerForSP(this); return false" %> - <script type="text/javascript"> - function toggleTrackerForSP (node) { - if ($(node).val() == 'story_points') - $('#trackers_for_sp').show(); - else - $('#trackers_for_sp').hide(); - } - </script> + <label><%= l(:label_agile_story_points) %></label> + <%= hidden_field_tag 'settings[story_points_on]', 0, id: nil %> + <%= check_box_tag 'settings[story_points_on]', 1, RedmineAgile.use_story_points?, onchange: "$('#trackers_for_sp').toggle(this.checked);" %> </p> + <div id="trackers_for_sp" style="display:<%= RedmineAgile.use_story_points? ? 'block' : 'none' %>"> <p> <label><%= l(:label_agile_trackers_for_sp) %></label> <%= select_tag 'settings[trackers_for_sp]', options_for_select(Tracker.all.map{|tr| [tr.name, tr.id]}, RedmineAgile.trackers_for_sp), :prompt => "All", :style => "width:150px" %> </p> - <p> - <label><%= l(:label_agile_sp_values) %></label> - <%= text_field_tag 'settings[sp_values]', RedmineAgile.sp_values.join(", ") %> - </p> </div> + <p> <label><%= l(:label_agile_default_chart) %></label> - <%= select_tag 'settings[default_chart]', options_charts_for_select(RedmineAgile.default_chart) %> + <%= select_tag 'settings[default_chart]', grouped_options_charts_for_select(RedmineAgile.default_chart) %> </p> <p> @@ -61,11 +41,6 @@ <label for="settings_hide_closed_issues_data"><%= l(:label_agile_hide_closed_issues_data) %></label> <%= check_box_tag 'settings[hide_closed_issues_data]', 1, RedmineAgile.hide_closed_issues_data? %> </p> -<p> - <label><%= l(:label_agile_allow_create_cards) %></label> - <%= hidden_field_tag 'settings[allow_create_card]', 0, :id => nil %> - <%= check_box_tag 'settings[allow_create_card]', 1, RedmineAgile.allow_create_card? %> -</p> <p> <label for="settings_auto_assign_on_move"><%= l(:label_agile_auto_assign_on_move) %></label> <%= hidden_field_tag 'settings[auto_assign_on_move]', 0, :id => nil %> diff --git a/plugins/redmine_agile/app/views/users/_user_color_form.html.erb b/plugins/redmine_agile/app/views/users/_user_color_form.html.erb deleted file mode 100644 index d2ae41c..0000000 --- a/plugins/redmine_agile/app/views/users/_user_color_form.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% if @user.safe_attribute? 'agile_color_attributes' -%> - <%= form.fields_for :agile_color do |fr| %> - <p><%= fr.select :color, options_for_select(AgileColor::AGILE_COLORS, @user.color), {:label => l(:label_agile_color), :include_blank => true} %></p> - <% end %> - - <%= javascript_tag "$('#user_agile_color_attributes_color').simplecolorpicker({picker: true});"%> - <% content_for :header_tags do %> - <%= javascript_include_tag 'jquery.simplecolorpicker.js', :plugin => "redmine_agile" %> - <%= stylesheet_link_tag 'jquery.simplecolorpicker.css', :plugin => 'redmine_agile' %> - <% end %> -<% end %> diff --git a/plugins/redmine_agile/assets/images/pro_version_agile.png b/plugins/redmine_agile/assets/images/pro_version_agile.png deleted file mode 100644 index 5288542f4b91df719ddb392848fe95a2211083d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49763 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�e 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࿎X-N0RXMHk0g=;)A`N7btYJS?k6?XX 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 PfPNHYRHf@A&BOl}p8#!3 diff --git a/plugins/redmine_agile/assets/javascripts/jquery.simplecolorpicker.js b/plugins/redmine_agile/assets/javascripts/jquery.simplecolorpicker.js old mode 100755 new mode 100644 diff --git a/plugins/redmine_agile/assets/javascripts/redmine_agile.js b/plugins/redmine_agile/assets/javascripts/redmine_agile.js old mode 100755 new mode 100644 index 9f86d08..d4ea53c --- a/plugins/redmine_agile/assets/javascripts/redmine_agile.js +++ b/plugins/redmine_agile/assets/javascripts/redmine_agile.js @@ -45,7 +45,7 @@ } }, - errorSortable: function($oldColumn, responseText) { + errorSortable: function(responseText) { var alertMessage = parseErrorResponse(responseText); if (alertMessage) { setErrorMessage(alertMessage); @@ -54,25 +54,25 @@ initSortable: function() { var self = this; - var $issuesCols = $(".issue-version-col"); + var $issuesCols = $(".column-issues"); $issuesCols.sortable({ - connectWith: ".issue-version-col", + connectWith: ".column-issues", start: function(event, ui) { var $item = $(ui.item); - $item.attr('oldColumnId', $item.parent().data('id')); + $item.attr('oldColumnId', $item.parent().data('version-id')); + $item.attr('oldSprintId', $item.parent().data('sprint-id')); $item.attr('oldPosition', $item.index()); }, stop: function(event, ui) { var $item = $(ui.item); - var sender = ui.sender; - var $column = $item.parents('.issue-version-col'); + var $column = $item.parents('.column-issues'); var issue_id = $item.data('id'); - var version_id = $column.attr("data-id"); - var order = $column.sortable('serialize'); + var version_id = $column.attr('data-version-id'); + var sprint_id = $column.attr('data-sprint-id'); var positions = {}; var oldId = $item.attr('oldColumnId'); - var $oldColumn = $('.ui-sortable[data-id="' + oldId + '"]'); + var $oldColumn = $('.ui-sortable[data-version-id="' + oldId + '"]'); if(!self.hasChange($item)){ self.backSortable($column); @@ -84,13 +84,15 @@ positions[$e.data('id')] = { position: $e.index() }; }); + var issueParams = {}; + if (version_id != undefined) issueParams['fixed_version_id'] = version_id || ""; + if (sprint_id != undefined) issueParams['sprint_id'] = sprint_id || ""; + $.ajax({ url: self.routes.update_agile_board_path, type: 'PUT', data: { - issue: { - fixed_version_id: version_id - }, + issue: issueParams, positions: positions, id: issue_id }, @@ -98,20 +100,21 @@ self.successSortable($oldColumn, $column); }, error: function(xhr, status, error) { - self.errorSortable($oldColumn, xhr.responseText); + self.errorSortable(xhr.responseText); + self.backSortable($oldColumn); } }); } }).disableSelection(); - $issuesCols.sortable( "option", "cancel", "div.pagination-wrapper" ); - + $issuesCols.sortable('option', 'cancel', 'div.pagination-wrapper'); }, hasChange: function($item){ - var column = $item.parents('.issue-version-col'); - return $item.attr('oldColumnId') != column.data('id') || // Checks a version change - $item.attr('oldPosition') != $item.index(); + var column = $item.parents('.column-issues'); + return $item.attr('oldColumnId') != column.data('version-id') || // Checks a version change + $item.attr('oldSprintId') != column.data('sprint-id') || // Checks a sprint change; + $item.attr('oldPosition') != $item.index() }, } @@ -176,6 +179,7 @@ var oldSwimLaneId = $item.attr('oldSwimLaneId'); var oldSwimLaneField = $item.attr('oldSwimLaneField'); var $oldColumn = $('.ui-sortable[data-id="' + oldStatusId + '"]'); + var $sprintField = $('#sprint_id'); if(!self.hasChange($item)){ self.backSortable($column); @@ -204,6 +208,17 @@ } params['issue'][swimLaneField] = swimLaneId; + + if ($sprintField) { + if (oldStatusId == '' && newStatusId != '') { + params['issue'].sprint_id = $sprintField.val(); + } + if (oldStatusId != '' && newStatusId == '') { + delete(params['issue'].status_id) + params['issue'].sprint_id = ''; + } + } + $.ajax({ url: self.routes.update_agile_board_path, type: 'PUT', @@ -212,7 +227,7 @@ self.successSortable(oldStatusId, newStatusId, oldSwimLaneId, swimLaneId); $($item).replaceWith(data); estimatedHours = $($item).find("span.hours"); - if(estimatedHours.size() > 0){ + if(estimatedHours.length > 0){ hours = $(estimatedHours).html().replace(/(\(|\)|h)?/g, ''); // self.recalculateEstimateHours(oldStatusId, newStatusId, hours); } @@ -311,64 +326,39 @@ }); } - this.saveInlineComment = function(node, url){ - var node = node; - var comment = $(node).siblings("textarea").val(); - if ($.trim(comment) === "") return false; - $(node).prop('disabled', true); - $('.lock').show(); - var card = $(node).parents(".issue-card"); - $.ajax({ - url: url, - type: "PUT", - dataType: "html", - data: { issue: { notes: comment } }, - success: function(data, status, xhr){ - $(card).replaceWith(data); - }, - error: function(xhr, status, error){ - var alertMessage = parseErrorResponse(xhr.responseText); - if (alertMessage) { - setErrorMessage(alertMessage); - } - }, - complete: function(xhr, status){ - $(node).prop('disabled', false); - $('.lock').hide(); - } - }); - } - - this.createIssue = function(url){ - $('.add-issue').click(function(){ - $(this).children('.new-card__input').focus(); - }); - $('.new-card__input').keyup(function(evt){ - var node = this; - evt = evt || window.event; - subject = $.trim($(node).val()); + this.createIssue = function (url) { + $(".add-issue").click(function () { + $(this).children(".new-card__input").focus() + }) + $(".new-card__input").keyup(function (evt) { + var node = this + var $sprintField = $("#sprint_id") + evt = evt || window.event + subject = $.trim($(node).val()) + sprint_id = $sprintField ? $sprintField.val() : "" if (evt.keyCode == 13 && subject.length != 0) { $.ajax({ url: url, type: "POST", data: { subject: subject, - status_id: $(node).parents('td').data('id') + status_id: $(node).parents("td").data("id"), + sprint_id: sprint_id }, dataType: "html", - success: function(data, status, xhr){ - $(node).parent().before(data); - $(node).val(''); + success: function (data, status, xhr) { + $(node).parent().before(data) + $(node).val("") }, - error:function(xhr, status, error) { - var alertMessage = parseErrorResponse(xhr.responseText); + error: function (xhr, status, error) { + var alertMessage = parseErrorResponse(xhr.responseText) if (alertMessage) { - setErrorMessage(alertMessage); + setErrorMessage(alertMessage) } - } - }); + }, + }) } - }); + }) } this.routes = routes; @@ -546,7 +536,6 @@ function changeHtmlNumber(element, number){ } } - function observeIssueSearchfield(fieldId, url) { $('#'+fieldId).each(function() { var $this = $(this); @@ -554,44 +543,90 @@ function observeIssueSearchfield(fieldId, url) { $this.attr('data-value-was', $this.val()); var check = function() { var val = $this.val(); + if ($this.attr('data-value-was') != val){ + var request_data = {} + $.map($('#query_form').serializeArray(), function(n, i){ + if (request_data[n['name']]) { + if ($.isArray(request_data[n['name']])) { + request_data[n['name']].push(n['value']) + } else { + request_data[n['name']] = [request_data[n['name']], n['value']]; + } + } else { + request_data[n['name']] = n['value']; + } + }); + request_data['q'] = val + $this.attr('data-value-was', val); $.ajax({ url: url, type: 'get', - data: {q: $this.val()}, + data: request_data, beforeSend: function(){ $this.addClass('ajax-loading'); }, complete: function(){ $this.removeClass('ajax-loading'); } }); } }; - var reset = function() { + var reset = function(e) { if (timer) { clearInterval(timer); timer = setInterval(check, 300); } }; + var skipSpecialKeys = function(e) { + if (e.keyCode === 13) { e.preventDefault() } + } var timer = setInterval(check, 300); + var skipSubmit = function(e) { + if (e.which == 13 || e.keyCode == 13) { + e.preventDefault(); + return false + } + } + $this.bind('keydown', skipSubmit); $this.bind('keyup click mousemove', reset); + $this.bind('keydown', skipSpecialKeys); }); } function recalculateHours() { - var backlogSum = 0; - var unit = $("#backlog_version_header").data('estimated-unit'); - - $('.versions-planning-board td:nth-child(2) .issue-card').each(function(i, elem){ - hours = parseFloat($(elem).data('estimated-hours')); - backlogSum += hours; - }) - $('.versions-planning-board .backlog-hours').text('(' + backlogSum.toFixed(2) + unit +')'); - - var currentSum = 0; - $('.versions-planning-board td:nth-child(3) .issue-card').each(function(i, elem){ - hours = parseFloat($(elem).data('estimated-hours')); - currentSum += hours; - }) - $('.versions-planning-board .current-hours').text('(' + currentSum.toFixed(2) + unit + ')'); + $('.version-column').each(function (i, elem) { + var estimatedHours = 0; + var storyPoints = 0; + $(elem).find('.issue-card').each(function (j, issue) { + estimatedHours += parseFloat($(issue).data('estimated-hours')); + storyPoints += parseFloat($(issue).data('story-points')); + }); + + var values = []; + if (estimatedHours > 0) { + values.push(estimatedHours.toFixed(2) + 'h'); + } + + if (storyPoints > 0) { + values.push(storyPoints.toFixed(2) + 'sp'); + } + + if (values.length > 0) { + $(elem).find('.version-estimate').text('(' + values.join('/') + ')'); + } + }); +} + +function recalculateSprintHours() { + var unit = $(".planning-board").data('estimated-unit'); + var dataAttr = unit == 'sp' ? 'story-points' : 'estimated-hours'; + + $('.sprint-column').each(function(i, elem){ + var versionEstimationSum = 0; + $(elem).find('.issue-card').each(function(j, issue){ + hours = parseFloat($(issue).data(dataAttr)); + versionEstimationSum += hours; + }); + $(elem).find('.sprint-estimate').text('(' + versionEstimationSum.toFixed(2) + unit + ')'); + }); } function showInlineCommentNode(quick_comment){ @@ -627,6 +662,35 @@ function showInlineComment(node, url){ }; } +function saveInlineComment(node, url){ + var node = node; + var comment = $(node).siblings("textarea").val(); + if ($.trim(comment) === "") return false; + $(node).prop('disabled', true); + $('.lock').show(); + var card = $(node).parents(".issue-card"); + var version_board = $('.planning-board').length; + $.ajax({ + url: url, + type: "PUT", + dataType: "html", + data: { issue: { notes: comment }, version_board: version_board }, + success: function(data, status, xhr){ + $(card).replaceWith(data); + }, + error: function(xhr, status, error){ + var alertMessage = parseErrorResponse(xhr.responseText); + if (alertMessage) { + setErrorMessage(alertMessage); + } + }, + complete: function(xhr, status){ + $(node).prop('disabled', false); + $('.lock').hide(); + } + }); +} + function cancelInlineComment(node){ $(node).parent().hide(); $(node).parent().siblings(".last_comment").show(); @@ -651,7 +715,7 @@ $(document).ready(function(){ var searchTerm = this.value; cards.removeClass("filtered"); cards.filter(function() { - return $(this).find(".name").text().toLowerCase().indexOf(searchTerm) === -1; + return $(this).find(".name").text().toLowerCase().indexOf(searchTerm.toLowerCase()) === -1; }).addClass("filtered"); }); }); @@ -679,3 +743,45 @@ function linkableAttributeFields() { var progress_label = $('.progress.attribute .label') progress_label.html(linkGenerator('/done_ratio', progress_label.html())); }; + +function chartLinkGenerator() { + var filter_values = $("#query_form").serialize(); + event.preventDefault(); + window.location.href = $('.agile_charts_link').prop('href') + '?' + filter_values; +} + +function hideChartPeriodCheckbox() { + $("#cb_chart_period").hide(); + $("label[for=cb_chart_period]").removeAttr("for"); +}; + +function toggleChartUnit(chart, target) { + var showTarget = chartsWithUnits.indexOf(chart) > -1; + $('#' + target).toggle(showTarget); +}; + +function updateVersionAgileChart(url) { + $.ajax(url + '&chart=' + $('#chart_by_select').val() + '&chart_unit=' + $('#chart_unit').val()); +}; + +function chartTooltipCallbacks(chartType) { + if (chartType === 'scatter') { + return scatterChartTooltipCallbacks() + } else { + return {} + } +}; + +function scatterChartTooltipCallbacks() { + return { + title: function (tooltipItem, data) { + return data.labels[tooltipItem[0].xLabel] || ''; + }, + label: function (tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + if (label) { label += ': ' } + label += tooltipItem.yLabel; + return label; + } + } +}; diff --git a/plugins/redmine_agile/assets/javascripts/redmine_agile_context_menu.js b/plugins/redmine_agile/assets/javascripts/redmine_agile_context_menu.js deleted file mode 100644 index 0d757e6..0000000 --- a/plugins/redmine_agile/assets/javascripts/redmine_agile_context_menu.js +++ /dev/null @@ -1,222 +0,0 @@ -var agileContextMenuObserving; -var agileContextMenuUrl; - -function agileContextMenuRightClick(event) { - var target = $(event.target); - if (target.is('a')) {return;} - var tr = target.parents('div').first(); - if (!tr.hasClass('hascontextmenu')) {return;} - event.preventDefault(); - if (!agileContextMenuIsSelected(tr)) { - agileContextMenuUnselectAll(); - agileContextMenuAddSelection(tr); - agileContextMenuSetLastSelected(tr); - } - agileContextMenuShow(event); -} - -function agileContextMenuClick(event) { - var target = $(event.target); - var lastSelected; - - if (target.is('a') && target.hasClass('submenu')) { - event.preventDefault(); - return; - } - agileContextMenuHide(); - if (target.is('a') || target.is('img')) { return; } - if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) { - var tr = target.parents('div').first(); - if (tr.length && tr.hasClass('hascontextmenu')) { - // a row was clicked, check if the click was on checkbox - if (target.is('input.checkbox')) { - // a checkbox may be clicked - if (target.attr('checked')) { - tr.addClass('context-menu-selection'); - } else { - tr.removeClass('context-menu-selection'); - } - } else { - if (event.ctrlKey || event.metaKey) { - agileContextMenuToggleSelection(tr); - } else if (event.shiftKey) { - lastSelected = agileContextMenuLastSelected(); - if (lastSelected.length) { - var toggling = false; - $('.hascontextmenu').each(function(){ - if (toggling || $(this).is(tr)) { - agileContextMenuAddSelection($(this)); - } - if ($(this).is(tr) || $(this).is(lastSelected)) { - toggling = !toggling; - } - }); - } else { - agileContextMenuAddSelection(tr); - } - } else { - agileContextMenuUnselectAll(); - agileContextMenuAddSelection(tr); - } - agileContextMenuSetLastSelected(tr); - } - } else { - // click is outside the rows - if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) { - event.preventDefault(); - } else { - agileContextMenuUnselectAll(); - } - } - } -} - -function agileContextMenuCreate() { - if ($('#context-menu').length < 1) { - var menu = document.createElement("div"); - menu.setAttribute("id", "context-menu"); - menu.setAttribute("style", "display:none;"); - document.getElementById("content").appendChild(menu); - } -} - -function agileContextMenuShow(event) { - var mouse_x = event.pageX; - var mouse_y = event.pageY; - var render_x = mouse_x; - var render_y = mouse_y; - var dims; - var menu_width; - var menu_height; - var window_width; - var window_height; - var max_width; - var max_height; - - $('#context-menu').css('left', (render_x + 'px')); - $('#context-menu').css('top', (render_y + 'px')); - $('#context-menu').html(''); - - $.ajax({ - url: agileContextMenuUrl, - data: $(event.target).parents('form').first().serialize(), - success: function(data, textStatus, jqXHR) { - $('#context-menu').html(data); - menu_width = $('#context-menu').width(); - menu_height = $('#context-menu').height(); - max_width = mouse_x + 2*menu_width; - max_height = mouse_y + menu_height; - - var ws = window_size(); - window_width = ws.width; - window_height = ws.height; - - /* display the menu above and/or to the left of the click if needed */ - if (max_width > window_width) { - render_x -= menu_width; - $('#context-menu').addClass('reverse-x'); - } else { - $('#context-menu').removeClass('reverse-x'); - } - if (max_height > window_height) { - render_y -= menu_height; - $('#context-menu').addClass('reverse-y'); - } else { - $('#context-menu').removeClass('reverse-y'); - } - if (render_x <= 0) render_x = 1; - if (render_y <= 0) render_y = 1; - $('#context-menu').css('left', (render_x + 'px')); - $('#context-menu').css('top', (render_y + 'px')); - $('#context-menu').show(); - - //if (window.parseStylesheets) { window.parseStylesheets(); } // IE - - } - }); -} - -function agileContextMenuSetLastSelected(tr) { - $('.cm-last').removeClass('cm-last'); - tr.addClass('cm-last'); -} - -function agileContextMenuLastSelected() { - return $('.cm-last').first(); -} - -function agileContextMenuUnselectAll() { - $('.hascontextmenu').each(function(){ - agileContextMenuRemoveSelection($(this)); - }); - $('.cm-last').removeClass('cm-last'); -} - -function agileContextMenuHide() { - $('#context-menu').hide(); -} - -function agileContextMenuToggleSelection(tr) { - if (agileContextMenuIsSelected(tr)) { - agileContextMenuRemoveSelection(tr); - } else { - agileContextMenuAddSelection(tr); - } -} - -function agileContextMenuAddSelection(tr) { - tr.addClass('context-menu-selection'); - agileContextMenuCheckSelectionBox(tr, true); - agileContextMenuClearDocumentSelection(); -} - -function agileContextMenuRemoveSelection(tr) { - tr.removeClass('context-menu-selection'); - agileContextMenuCheckSelectionBox(tr, false); -} - -function agileContextMenuIsSelected(tr) { - return tr.hasClass('context-menu-selection'); -} - -function agileContextMenuCheckSelectionBox(tr, checked) { - tr.find('input.checkbox[type=checkbox]').prop('checked', checked); -} - -function agileContextMenuClearDocumentSelection() { - // TODO - if (document.selection) { - document.selection.empty(); // IE - } else { - window.getSelection().removeAllRanges(); - } -} - -function agileContextMenuInit(url) { - agileContextMenuUrl = url; - agileContextMenuCreate(); - agileContextMenuUnselectAll(); - - if (!agileContextMenuObserving) { - $(document).click(agileContextMenuClick); - $(document).contextmenu(agileContextMenuRightClick); - agileContextMenuObserving = true; - } -} - - -function window_size() { - var w; - var h; - if (window.innerWidth) { - w = window.innerWidth; - h = window.innerHeight; - } else if (document.documentElement) { - w = document.documentElement.clientWidth; - h = document.documentElement.clientHeight; - } else { - w = document.body.clientWidth; - h = document.body.clientHeight; - } - return {width: w, height: h}; -} diff --git a/plugins/redmine_agile/assets/javascripts/visibility.min.js b/plugins/redmine_agile/assets/javascripts/visibility.min.js deleted file mode 100644 index 3733ccd..0000000 --- a/plugins/redmine_agile/assets/javascripts/visibility.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){"use strict";var i=-1,t={onVisible:function(e){var i=t.isSupported();if(!i||!t.hidden())return e(),i;var n=t.change(function(){t.hidden()||(t.unbind(n),e())});return n},change:function(e){if(!t.isSupported())return!1;i+=1;var n=i;return t._callbacks[n]=e,t._listen(),n},unbind:function(e){delete t._callbacks[e]},afterPrerendering:function(e){var i=t.isSupported(),n="prerender";if(!i||n!=t.state())return e(),i;var r=t.change(function(i,d){n!=d&&(t.unbind(r),e())});return r},hidden:function(){return!(!t._doc.hidden&&!t._doc.webkitHidden)},state:function(){return t._doc.visibilityState||t._doc.webkitVisibilityState||"visible"},isSupported:function(){return!(!t._doc.visibilityState&&!t._doc.webkitVisibilityState)},_doc:document||{},_callbacks:{},_change:function(e){var i=t.state();for(var n in t._callbacks)t._callbacks[n].call(t._doc,e,i)},_listen:function(){if(!t._init){var e="visibilitychange";t._doc.webkitVisibilityState&&(e="webkit"+e);var i=function(){t._change.apply(t,arguments)};t._doc.addEventListener?t._doc.addEventListener(e,i):t._doc.attachEvent(e,i),t._init=!0}}};"undefined"!=typeof module&&module.exports?module.exports=t:e.Visibility=t}(this),function(e){"use strict";var i=-1,t=function(t){return t.every=function(e,n,r){t._time(),r||(r=n,n=null),i+=1;var d=i;return t._timers[d]={visible:e,hidden:n,callback:r},t._run(d,!1),t.isSupported()&&t._listen(),d},t.stop=function(e){return t._timers[e]?(t._stop(e),delete t._timers[e],!0):!1},t._timers={},t._time=function(){t._timed||(t._timed=!0,t._wasHidden=t.hidden(),t.change(function(){t._stopRun(),t._wasHidden=t.hidden()}))},t._run=function(i,n){var r,d=t._timers[i];if(t.hidden()){if(null===d.hidden)return;r=d.hidden}else r=d.visible;var a=function(){d.last=new Date,d.callback.call(e)};if(n){var o=new Date,u=o-d.last;r>u?d.delay=setTimeout(function(){a(),d.id=setInterval(a,r)},r-u):(a(),d.id=setInterval(a,r))}else d.id=setInterval(a,r)},t._stop=function(e){var i=t._timers[e];clearInterval(i.id),clearTimeout(i.delay),delete i.id,delete i.delay},t._stopRun=function(){var e=t.hidden(),i=t._wasHidden;if(e&&!i||!e&&i)for(var n in t._timers)t._stop(n),t._run(n,!e)},t};"undefined"!=typeof module&&module.exports?module.exports=t(require("./visibility.core")):t(e.Visibility)}(window); \ No newline at end of file diff --git a/plugins/redmine_agile/assets/stylesheets/jquery.simplecolorpicker.css b/plugins/redmine_agile/assets/stylesheets/jquery.simplecolorpicker.css old mode 100755 new mode 100644 diff --git a/plugins/redmine_agile/assets/stylesheets/redmine_agile.css b/plugins/redmine_agile/assets/stylesheets/redmine_agile.css old mode 100755 new mode 100644 index e319206..d8ce691 --- a/plugins/redmine_agile/assets/stylesheets/redmine_agile.css +++ b/plugins/redmine_agile/assets/stylesheets/redmine_agile.css @@ -53,6 +53,24 @@ table.options tr > td { white-space: nowrap; } +.agile_options_field { + display: inline-block; + min-width: 30%; +} + +.agile_options_label { + display: inline-block; + min-width: 100px; +} + +.selected_sprint { + font-size: 14px; +} + +.selected_sprint > select { + font-size: 14px; +} + .card-fields .floating { text-align: left; width: 200px; @@ -75,6 +93,13 @@ table.options tr > td { padding: 1px 5px !important; } +.card-fields .floating .wp_input:not(:focus) { + background-color: transparent; + border-color: transparent; + font-size: 90%; +} + + /**********************************************************************/ /* ISSUES SIDEBAR /**********************************************************************/ @@ -128,6 +153,7 @@ table.list.issues-board th {overflow: hidden; text-overflow: ellipsis;} background-color: #ffffdd; } +table.issues-board .issue-status-col.empty { padding-bottom: 30px } table.issues-board td.issue-status-col.closed {background-color: #FAFAFA;} table.issues-board tr.group.swimlane {height: 30px;} @@ -364,6 +390,10 @@ table.list.issues tr.issue:not(.context-menu-selection).bk-gray td.id a {backgro width: 100%; } +.issue-card .quick-edit { + margin-right: 3px; +} + .issue-card .last-comment{ font-style: italic; } @@ -452,8 +482,8 @@ table.issues-board tbody td.over_wp_limit {background-color: #FFF6F6;} .agile-chart-container { margin: auto; - height: 400px; - max-width: 800px; + min-height: 400px; + max-width: 1200px; } .issue-card.filtered { @@ -482,6 +512,99 @@ table.list.issues-board.sticky { html.agile-board-fullscreen #wrapper > div.flyout-menu.js-flyout-menu { z-index: 22; } } +.planning-board { + display: flex; + overflow-x: auto; +} + +.planning-board p { + margin: 0; +} + +.planning-board .column-content { + width: 272px; + min-width: 272px; + margin: 0 4px 4px 4px; + background-color: #f6f7f8; + border: 1px solid #d7d7d7; +} + +.planning-board .column-header { + display: block; + justify-content: space-between; + align-items: center; + min-height: 28px; + padding: 5px; + background-color: #eeeeee; + border-bottom: 1px solid #d7d7d7; +} + +.planning-board .column-header .version-name { + color: #555; + font-size: 90%; + font-weight: bold; +} + +.planning-board .column-issues { + padding-bottom: 30px; +} + +.planning-board .closed-issue { + color: #999; + display: block; + white-space: inherit; + background-color: #ededed; + border-color: #d7d7d7; +} + +.planning-board .closed-issue p.name a, .planning-board .closed-issue p.issue-id { + color: #999; + text-decoration: line-through; +} + +.issue-edit-modal { + min-height: initial !important; +} + +.modal-action-button { + height: 28px; +} + +.agile_sprint_list .spring_column { text-align: center; } + +.controller-agile_boards .query-totals { + margin-top: -2.3em; +} + +.agile-board-fullscreen .controller-agile_boards .query-totals { + margin: 0px; + position: fixed; + top: 0px; + background: inherit; + left: 0; + right: 0; + padding-right: 32px; + min-height: 20px; + line-height: 20px; +} + +.backlog-column-header { + min-width: 15%; + border-right: solid 3px #d7d7d7; +} + +.issue-backlog-search { padding: 5px 5px 0px 5px; } +.issue-backlog-search input#search { + max-width: 100%; +} + +.issues-board .backlog-column-header { background-color: #e1f1f8; } + +.issues-board .issue-backlog-col { + background: #e1f1f8; + border-right: solid 3px #d7d7d7 !important; +} + html { overflow-y: inherit !important; } diff --git a/plugins/redmine_agile/config/locales/de.yml b/plugins/redmine_agile/config/locales/de.yml index e576b63..428cde2 100644 --- a/plugins/redmine_agile/config/locales/de.yml +++ b/plugins/redmine_agile/config/locales/de.yml @@ -19,13 +19,16 @@ de: label_agile_board_default_fields: Standardfelder fĂĽr Taskboard-Karten label_agile_charts_issues_burndown: Burndown nach Aufgaben label_agile_charts_work_burndown: Burndown nach erfassten Zeiten + label_agile_charts_work_burndown_sp: Burndown nach Story points + label_agile_charts_work_burndown_hours: Burndown nach Stunden label_agile_charts_number_of_hours: Stunden label_agile_charts_number_of_issues: Anzahl Aufgaben - label_agile_charts_cumulative_flow: Cumulative Flow - label_agile_charts_trackers_cumulative_flow: Cumulative Flow nach Tracker + label_agile_charts_cumulative_flow: Kumulativer Fluss + label_agile_charts_trackers_cumulative_flow: Kumulativer Fluss nach Tracker label_agile_charts_issues_velocity: Velocity - label_agile_charts_lead_time: Lead Time - label_agile_charts_average_lead_time: Durchschnittliche Lead Time + label_agile_charts_cycle_time: Zyklus + label_agile_charts_lead_time: Durchlaufzeit + label_agile_charts_average_lead_time: Durchschnittliche Durchlaufzeit label_agile_chart_plural: Agile Diagramme permission_view_agile_charts: Agile Diagramme ansehen label_agile_ideal_work_remaining: Soll @@ -123,3 +126,108 @@ de: label_agile_inline_comment: Inline-Kommentar label_agile_hours: Stunden field_color: Farbe + + #1.4.0 + label_agile_esitmate_units: Einheit fĂĽr Schätzung + label_agile_trackers_for_sp: Tracker fĂĽr Story points + label_agile_story_points: Story points + field_story_points: Story points + label_agile_charts_number_of_story_points: Anzahl Story points + label_agile_board_columns: Board Spalten + lable_agile_wip_limit_exceeded: Work-in-progress Limit ĂĽberschritten + label_agile_wip_limit: Work-in-progress Limit + label_agile_add_new_issue: '+ NEUES TICKET' + label_agile_allow_create_cards: Ticket aus Taskboard erstellen + label_agile_auto_assign_on_move: Automatische Zuweisung bei + text_agile_create_issue_error: Beim Erstellen der Aufgabe ist ein Fehler aufgetreten + label_agile_inline_comment: Inline Kommentar + label_agile_hours: Stunden + field_color: Farbe + + field_duration: Dauer + + field_closed_on_trendline: Closed trendline + field_created_on_trendline: Created trendline + + label_agile_sp_values: Story points Werte + + label_agile_interval_size: Interval Größe + label_agile_day: Tag + label_agile_week: Woche + label_agile_month: Monat + label_agile_quarter: Quartal + label_agile_year: Jahr + label_cards_search: Suche nach Thema + + field_agile_sprint: Sprint + field_end_date: Enddatum + label_agile_sprints_on: Aktiviere Sprints standardmäßig + label_agile_sprint: Sprint + label_agile_sprint_plural: Sprints + label_agile_sprint_name: Name + label_agile_sprint_description: Beschreibung + label_agile_sprint_status: Status + label_agile_sprint_start_date: Startdatum + label_agile_sprint_end_date: Enddatum + label_agile_sprint_new: Neuer Sprint + label_agile_sprint_status_open: Offen + label_agile_sprint_status_active: Aktiv + label_agile_sprint_status_closed: Geschlossen + label_agile_sprint_duration: Dauer + label_agile_sprint_duration_select: <<Wähle Dauer>> + label_agile_sprint_duration_week_1: 1 Woche + label_agile_sprint_duration_week_2: 2 Wochen + label_agile_sprint_duration_week_3: 3 Wochen + label_agile_sprint_duration_week_4: 4 Wochen + label_agile_sprint_errors_crossed: Sprint Daten ĂĽberschneiden sich mit einem anderen existierenden Sprint + label_agile_sprint_errors_end_more_start: Das Enddatum sollte nach dem Startdatum liegen + label_agile_sprint_default_chart: Diagramm + label_agile_sprint_chart_units: Einheit fĂĽr Schätzung + label_agile_chart_for_sprint: "FĂĽr Sprint: %{sprint}" + + label_agile_charts: Diagramme + label_agile_chart_burndown: Burndown Diagramm + label_agile_chart_burnup: Burnup Diagramm + label_agile_chart_units: Einheiten + + label_agile_planning_board_more: Lade mehr... + label_agile_version_query_new: Neue agile Abfrage + label_agile_version_my_boards: Meine agilen boards + label_agile_version_board_plural: Agile boards + label_agile_version_board_edit: Bearbeite agile Board version + + label_agile_edit_issue: Bearbeite Ticket + + label_agile_board_type: Art des Taskboards + label_agile_board_type_kanban: Kanban board + label_agile_board_type_scrum: Scrum board + label_agile_board_totals: Gesamt + label_agile_board_totals_story_points: Story points + label_agile_board_totals_hours: Stunden + label_agile_board_totals_spent_time: Aufgewendete Zeit + label_agile_board_totals_percent_done: Prozent erledigt + label_agile_board_totals_velocity: Velocity + label_agile_board_totals_interval: Intervall + label_agile_board_totals_remaining: Verbleibend + + label_agile_sprint_list_active: Aktive Sprints + label_agile_sprint_list_future: ZukĂĽnftige Sprints + + label_agile_mixed_trackers: Verschiedene Tracker + label_agile_moving_average: Gleitender Durchschnitt + + label_agile_board_backlog: Backlog + label_agile_board_backlog_column: Backlog Spalten + label_agile_board_search_backlog_issues: Durchsuche Backlog Tickets + label_agile_version_plural: Versionen + label_agile_sprint_add: Sprint hinzufĂĽgen + label_agile_version_add: Version hinzufĂĽgen + + label_agile_sprint_query_new: Neue agile Sprint Abfragen + label_agile_sprint_my_boards: Meine agilen Sprint boards + label_agile_sprint_board_plural: Agile Sprint boards + label_agile_sprint_board_edit: Bearbeite agiles Sprint board + field_sprint: Sprint + label_agile_no_sprint_issues: Tickets ohne Sprint + field_closed_sprints: Geschlossenen Sprints + field_closed_versions: Geschlossene Versionen diff --git a/plugins/redmine_agile/config/locales/en.yml b/plugins/redmine_agile/config/locales/en.yml old mode 100755 new mode 100644 index f74ba5c..a223a40 --- a/plugins/redmine_agile/config/locales/en.yml +++ b/plugins/redmine_agile/config/locales/en.yml @@ -25,9 +25,12 @@ en: label_agile_charts_cumulative_flow: Cumulative flow label_agile_charts_trackers_cumulative_flow: Trackers cumulative flow label_agile_charts_issues_velocity: Velocity + label_agile_charts_cycle_time: Cycle time label_agile_charts_lead_time: Lead time label_agile_charts_average_lead_time: Average lead time label_agile_chart_plural: Agile charts + label_agile_chart_new: New agile chart + label_agile_chart_edit: Edit agile chart permission_view_agile_charts: View agile charts label_agile_ideal_work_remaining: Ideal label_agile_actual_work_remaining: Actual @@ -42,7 +45,7 @@ en: label_agile_too_many_items: "The chart can't be created because it exceeds the maximum number of items that can be displayed (%{max})" label_agile_time_reports_items_limit: Time entries based charts issues limit label_agile_total_work_remaining: Total - label_agile_default_chart: Version default chart + label_agile_default_chart: Default chart #1.1.1 label_agile_charts_average_velocity: Average velocity @@ -134,4 +137,95 @@ en: field_created_on_trendline: Created trendline label_agile_sp_values: Story points values + + label_agile_interval_size: Interval size + label_agile_day: Day + label_agile_week: Week + label_agile_month: Month + label_agile_quarter: Quarter + label_agile_year: Year label_cards_search: Search by subject + + field_agile_sprint: Sprint + field_end_date: End date + label_agile_sprints_on: Activate sprints by default + label_agile_sprint: Sprint + label_agile_sprint_plural: Sprints + label_agile_sprint_name: Name + label_agile_sprint_description: Description + label_agile_sprint_status: Status + label_agile_sprint_sharing: Sharing + label_agile_sprint_start_date: Start date + label_agile_sprint_end_date: End date + label_agile_sprint_new: New sprint + label_agile_sprint_status_open: Open + label_agile_sprint_status_active: Active + label_agile_sprint_status_closed: Closed + label_agile_sprint_duration: Duration + label_agile_sprint_duration_select: <<Select duration>> + label_agile_sprint_duration_week_1: 1 week + label_agile_sprint_duration_week_2: 2 weeks + label_agile_sprint_duration_week_3: 3 weeks + label_agile_sprint_duration_week_4: 4 weeks + label_agile_sprint_errors_crossed: Sprint dates are cross another existed sprint + label_agile_sprint_errors_end_more_start: End date should be more than start date + label_agile_sprint_errors_open_issues: Sprint with open issues can be closed + label_agile_sprint_default_chart: Сhart + label_agile_sprint_chart_units: Estimation + label_agile_chart_for_sprint: "For sprint: %{sprint}" + + label_agile_charts: Charts + label_agile_chart_burndown: Burndown chart + label_agile_chart_burnup: Burnup chart + label_agile_chart_units: Units + + label_agile_planning_board_more: Load more... + label_agile_version_query_new: New Agile versions query + label_agile_version_my_boards: My agile version boards + label_agile_version_board_plural: Agile version boards + label_agile_version_board_edit: Edit agile version board + + label_agile_edit_issue: Edit issue + + label_agile_board_type: Board type + label_agile_board_type_kanban: Kanban board + label_agile_board_type_scrum: Scrum board + label_agile_board_totals: Totals + label_agile_board_totals_story_points: Story points + label_agile_board_totals_hours: Hours + label_agile_board_totals_spent_time: Spent time + label_agile_board_totals_percent_done: Percent done + label_agile_board_totals_velocity: Velocity + label_agile_board_totals_interval: Interval + label_agile_board_totals_remaining: Remaining + + label_agile_sprint_list_active: Active sprints + label_agile_sprint_list_future: Future sprints + label_agile_sprint_list_old: Old sprints + + label_agile_mixed_trackers: Mixed trackers + label_agile_moving_average: Moving average + + label_agile_board_backlog: Backlog + label_agile_board_backlog_column: Backlog column + label_agile_board_search_backlog_issues: Search backlog issues + label_agile_version_plural: Versions + label_agile_sprint_add: Add sprint + label_agile_version_add: Add version + + label_agile_sprint_query_new: New agile sprints query + label_agile_sprint_my_boards: My agile sprint boards + label_agile_sprint_board_plural: Agile sprint boards + label_agile_sprint_board_edit: Edit agile sprint board + field_sprint: Sprint + label_agile_no_sprint_issues: Issues without sprint + field_closed_sprints: Closed sprints + field_closed_versions: Closed versions + + label_agile_sprint_sharing_none: Not shared + label_agile_sprint_sharing_descendants: With subprojects + label_agile_sprint_sharing_hierarchy: With project hierarchy + label_agile_sprint_sharing_tree: With project tree + label_agile_sprint_sharing_system: With all projects + + diff --git a/plugins/redmine_agile/config/locales/es.yml b/plugins/redmine_agile/config/locales/es.yml index 7b6cec1..5891751 100644 --- a/plugins/redmine_agile/config/locales/es.yml +++ b/plugins/redmine_agile/config/locales/es.yml @@ -4,13 +4,12 @@ es: label_agile_board: Tablero ágil label_agile_board_plural: Tableros ágiles label_agile_board_thumbnails: Thumbnails - label_agile_board_issues_per_column: Peticiones por columna label_agile_board_more_issues: Más peticiones error_agile_status_transition: No se puede cambiar el estado de la peticiĂłn label_agile_board_new: Nuevo tablero ágil label_agile_board_edit: Editar tablero ágil label_agile_my_boards: Mis tableros ágiles - label_agile_issue_id: ID de PeticiĂłn + label_agile_issue_id: ID de peticiĂłn permission_manage_public_agile_queries: Administrar tableros ágiles pĂşblicos permission_add_agile_queries: Agregar tableros ágiles @@ -19,27 +18,31 @@ es: #1.1.0 label_agile_board_default_fields: Columnas por defecto label_agile_charts_issues_burndown: Peticiones burndown - label_agile_charts_work_burndown: Trabajo burndown + label_agile_charts_work_burndown: Horas burndown + label_agile_charts_work_burndown_sp: Story points burndown label_agile_charts_number_of_hours: NĂşmero de horas label_agile_charts_number_of_issues: NĂşmero de peticiones label_agile_charts_cumulative_flow: Flujo acumulado label_agile_charts_trackers_cumulative_flow: Seguimiento de flujo acumulado label_agile_charts_issues_velocity: Velocidad + label_agile_charts_cycle_time: Ciclo de tiempo label_agile_charts_lead_time: Tiempo de ejecuciĂłn - label_agile_charts_average_lead_time: Tiempo de ejecuciĂłn promedio - label_agile_chart_plural: Gráficos ágiles - permission_view_agile_charts: Ver gráficos ágiles + label_agile_charts_average_lead_time: Tiempo medio de entrega + label_agile_chart_plural: Gráficas Agile + label_agile_chart_new: Nueva gráfica Agile + label_agile_chart_edit: Editar gráfica Agile + permission_view_agile_charts: Ver gráfica Agile label_agile_ideal_work_remaining: Ideal label_agile_actual_work_remaining: Actual - label_agile_chart: Gráfico + label_agile_chart: Gráfica label_agile_date_from: Desde - label_agile_date_to: Hasta - label_agile_chart_dates: Intervalos gráficos - label_agile_weighed_ideal_work_remaining: Weighed ideal - label_agile_status_colors: Color de estados - label_agile_charts_burnup: "Peticiones burnup" + label_agile_date_to: a + label_agile_chart_dates: Gráfica de interválos + label_agile_weighed_ideal_work_remaining: Ideal ponderado + label_agile_status_colors: Estado de colores + label_agile_charts_burnup: Peticiones burnup label_agile_charts_number_of_days: NĂşmero de dĂas - label_agile_too_many_items: "El gráfico no puede ser creado por exceder el nĂşmero máximo de items que pueden ser mostrados (%{max})" + label_agile_too_many_items: El gráfico no puede ser creado por exceder el nĂşmero máximo de items que pueden ser mostrados (%{max}) label_agile_time_reports_items_limit: Tiempo de entradas basado en gráfico de peticiones lĂmite label_agile_total_work_remaining: Total label_agile_default_chart: VersiĂłn gráfica por defecto @@ -47,4 +50,172 @@ es: #1.1.1 label_agile_charts_average_velocity: Velocidad promedio label_agile_charts_avarate_number_of_issues: NĂşmero de peticiones promedio - label_agile_charts_avarate_number_of_hours: NĂşmero de horas promedio \ No newline at end of file + label_agile_charts_avarate_number_of_hours: NĂşmero de horas promedio + + #1.2.0 + label_agile_color: Color + permission_manage_agile_verions: Administrar planificaciĂłn de versiones + label_agile_version_planning: planificaciĂłn de versiones + error_agile_version_transition: No se puede cambiar la versiĂłn de la peticiĂłn + label_agile_tracker_colors: Colores de tipos + label_agile_issue_priority_colors: Colores de prioridades de la peticiĂłn + label_agile_color_based_on: Color por + label_agile_color_no_colors: Sin colores + label_agile_manage_colors: Administrar colores + label_agile_fullscreen: Pantalla completa + label_agile_no_version_issues: PeticiĂłn sin version + label_agile_charts_work_burnup_hours: burnup de horas + label_agile_charts_work_burnup_sp: burnup de Story points + label_agile_completed: Completado + label_agile_exclude_weekends: Excluir los fines de semana del esfuerzo ideal + label_agile_board_truncated: El tablero se truncĂł porque excede el nĂşmero máximo de elementos que se pueden mostrar (%{max}) + label_agile_board_items_limit: LĂmite de elementos del tablero ágil + label_agile_swimlanes: Swimlanes + label_agile_minimize_closed: minimizar las peticiones cerradas + label_agile_fields: Campos de la tarjeta + label_agile_default_board: tablero por defecto + + #1.3.6 + text_agile_move_not_possible: EstĂ© movimiento no es posible () + + #1.3.8 + label_agile_parent_issue_tracker_id: Tipo peticiĂłn padre + label_agile_sub_issues: SubTareas + label_agile_color_green: Verde + label_agile_color_blue: Azul + label_agile_color_turquoise: Turquesa + label_agile_color_lightgreen: Verde claro + label_agile_color_yellow: Amarillo + label_agile_color_orange: Naranja + label_agile_color_red: Rojo + label_agile_color_purple: Morado + label_agile_color_gray: Gris + label_agile_has_sub_issues: Tiene SubTareas + label_agile_light_free_version: VersiĂłn gratuita Agile + label_agile_link_to_pro: Actualizar a PRO + label_agile_link_to_pro_demo: demo version PRO + label_agile_link_to_more_plugins: Encontrar más extensiones de RedmineUP + label_agile_button_agree: Acuerdo + label_agile_license: Licencia RedmineUP + label_agile_saving_boards: Guardar tablero + label_agile_horizontal_swim_lines: swimlines horizontales + label_agile_board_sub_columns: sub-columns del tablero + label_agile_additional_agile_charts: Additional agile charts + label_agile_coloured_issue_cards: Tarjetas de colores + label_agile_4_more_features: 4 funcionalides más... + label_agile_upgrade_to_pro: Actualice a la version PRO apra usar estas funcionalidades + + label_agile_day_in_state: En estado + + #1.3.10 + label_agile_hide_closed_issues_data: Ocultar datos de las peticiones cerradas + project_module_agile: Agile + + #1.3.13 + label_agile_last_comment: Ăšltimo comentario + + #1.4.0 + label_agile_esitmate_units: Unidades estimadas + label_agile_trackers_for_sp: Tipos para story points + label_agile_story_points: Story points + field_story_points: Story points + label_agile_charts_number_of_story_points: NĂşmero de story points + label_agile_board_columns: Columnas del tablero + lable_agile_wip_limit_exceeded: Excedido lĂmite de dedicaciĂłn + label_agile_wip_limit: lĂmite de dedicaciĂłn + label_agile_add_new_issue: "+ AĂ‘ADIR NUEVA PETICION" + label_agile_allow_create_cards: Crear una tarjeta + label_agile_auto_assign_on_move: Auto-Asignar al mover + text_agile_create_issue_error: Se produjo un error al crear la peticiĂłn + label_agile_inline_comment: Comentario en linea + label_agile_hours: Horas + field_color: Color + + field_duration: DuraciĂłn + + field_closed_on_trendline: Trendline cerrada + field_created_on_trendline: Trendline creada + + label_agile_sp_values: Valores Story points + + label_agile_interval_size: Tamaño intervalo + label_agile_day: DĂa + label_agile_week: Semana + label_agile_month: Mes + label_agile_quarter: Trimestre + label_agile_year: Año + label_cards_search: BĂşsqueda por asunto + + field_agile_sprint: Sprint + field_end_date: Fecha fin + label_agile_sprints_on: Activar sprints por defecto + label_agile_sprint: Sprint + label_agile_sprint_plural: Sprints + label_agile_sprint_name: Nombre + label_agile_sprint_description: DescripciĂłn + label_agile_sprint_status: Estado + label_agile_sprint_start_date: Fecha inicio + label_agile_sprint_end_date: Fecha fin + label_agile_sprint_new: nuevo sprint + label_agile_sprint_status_open: Abierto + label_agile_sprint_status_active: Activo + label_agile_sprint_status_closed: Cerrado + label_agile_sprint_duration: Duraccion + label_agile_sprint_duration_select: <<Seleccionar duraciĂłn>> + label_agile_sprint_duration_week_1: 1 semana + label_agile_sprint_duration_week_2: 2 semanas + label_agile_sprint_duration_week_3: 3 semanas + label_agile_sprint_duration_week_4: 4 semanas + label_agile_sprint_errors_crossed: Las fechas del Sprint se cruza con otro Sprint + label_agile_sprint_errors_end_more_start: La fecha fin debe ser mayor que la fecha de inicio + label_agile_sprint_errors_open_issues: las tareas abiertas del Sprint se puede cerrar + label_agile_sprint_default_chart: Gráfica + label_agile_sprint_chart_units: EstimaciĂłn + label_agile_chart_for_sprint: "Para el sprint: %{sprint}" + + label_agile_charts: Gráficas + label_agile_chart_burndown: Gráficas Burndown + label_agile_chart_burnup: Gráfica Burnup + label_agile_chart_units: Unidades + + label_agile_planning_board_more: cargar más... + label_agile_version_query_new: Nueva consulta de version Agile + label_agile_version_my_boards: Mis tableros Ăgiles + label_agile_version_board_plural: Tablero versiĂłn Agil + label_agile_version_board_edit: Editar tablero versiĂłn Agil + + label_agile_edit_issue: Editar peticiĂłn + + label_agile_board_type: Tipo de tablero + label_agile_board_type_kanban: Tablero Kanban + label_agile_board_type_scrum: Tablero Scrum + label_agile_board_totals: Totales + label_agile_board_totals_story_points: Story points + label_agile_board_totals_hours: Horas + label_agile_board_totals_spent_time: Tiempo dedicado + label_agile_board_totals_percent_done: "% Realizado" + label_agile_board_totals_velocity: Velocidad + label_agile_board_totals_interval: Intervalo + label_agile_board_totals_remaining: Restante + + label_agile_sprint_list_active: Sprints activos + label_agile_sprint_list_future: Sprints futuros + + label_agile_mixed_trackers: Tipos mixtos + label_agile_moving_average: Moving Average + + label_agile_board_backlog: Backlog + label_agile_board_backlog_column: columna Backlog + label_agile_board_search_backlog_issues: Buscar peticiones en backlog + label_agile_version_plural: Versiones + label_agile_sprint_add: Añadir sprint + label_agile_version_add: Añadir versiĂłn + + label_agile_sprint_query_new: Nueva consulta Sprint ágil + label_agile_sprint_my_boards: Mis tableros Sprint ágil + label_agile_sprint_board_plural: Tablero Sprint ágil + label_agile_sprint_board_edit: Editar tablero Sprint ágil + field_sprint: Sprint + label_agile_no_sprint_issues: Peticiones sin sprint + field_closed_sprints: sprints cerrados + field_closed_versions: Versiones cerradas diff --git a/plugins/redmine_agile/config/locales/pt-BR.yml b/plugins/redmine_agile/config/locales/pt-BR.yml index 22a783f..5580ab7 100644 --- a/plugins/redmine_agile/config/locales/pt-BR.yml +++ b/plugins/redmine_agile/config/locales/pt-BR.yml @@ -1,70 +1,222 @@ -# English strings go here for Rails i18n +# Portugues - Brazil strings go here for Rails i18n pt-BR: - label_agile: Agile - label_agile_board: Agile board - label_agile_board_plural: Agile boards + label_agile: Ăgil + label_agile_board: Quadro Ăgil + label_agile_board_plural: Quadros ágeis label_agile_board_thumbnails: Thumbnails - label_agile_board_more_issues: Mais Tarefas - error_agile_status_transition: NĂŁo Ă© possĂvel alterar o status da tarefa - label_agile_board_new: Novo agile board - label_agile_board_edit: Edit agile board - label_agile_my_boards: My agile boards - label_agile_issue_id: NĂşmero da Tarefa + label_agile_board_more_issues: Mais tarefas + error_agile_status_transition: NĂŁo Ă© possĂvel alterar o status do problema + label_agile_board_new: Novo quadro ágil + label_agile_board_edit: Editar quadro ágil + label_agile_my_boards: Meus quadros ágeis + label_agile_issue_id: Id. Tarefa - permission_manage_public_agile_queries: Manage public agile boards - permission_add_agile_queries: Add agile boards - permission_view_agile_queries: View agile boards + permission_manage_public_agile_queries: Gerenciar quadros ágeis publicos + permission_add_agile_queries: Adicionar quadro ágil + permission_view_agile_queries: Visualizar quadros ágeis #1.1.0 - label_agile_board_default_fields: Default card fields - label_agile_charts_issues_burndown: Tarefas burndown - label_agile_charts_work_burndown: Trabalho burndown + label_agile_board_default_fields: Campos padrĂŁo no post-it + label_agile_charts_issues_burndown: Burndow de tarefas + label_agile_charts_work_burndown_hours: Burndow de horas + label_agile_charts_work_burndown_sp: Burndow por pontos por histĂłrias label_agile_charts_number_of_hours: NĂşmero de horas label_agile_charts_number_of_issues: NĂşmero de tarefas - label_agile_charts_cumulative_flow: AfluĂŞncia - label_agile_charts_trackers_cumulative_flow: AfluĂŞncia de tarefas + label_agile_charts_cumulative_flow: Fluxo cumulativo + label_agile_charts_trackers_cumulative_flow: Fluxo comulativo por tipo label_agile_charts_issues_velocity: Velocidade + label_agile_charts_cycle_time: Tempo de ciclo label_agile_charts_lead_time: Tempo de espera label_agile_charts_average_lead_time: Tempo de espera mĂ©dio - label_agile_chart_plural: Agile charts - permission_view_agile_charts: View agile charts + label_agile_chart_plural: Gráficos ágeis + label_agile_chart_new: Novo gráfico ágil + label_agile_chart_edit: Editar gráfico ágil + permission_view_agile_charts: Visualizar gráficos ágeis label_agile_ideal_work_remaining: Ideal - label_agile_actual_work_remaining: Real + label_agile_actual_work_remaining: Atual label_agile_chart: Gráfico label_agile_date_from: De label_agile_date_to: Para - label_agile_chart_dates: Chart intervals - label_agile_weighed_ideal_work_remaining: MĂ©dia ideal - label_agile_status_colors: Status colors - label_agile_charts_burnup: Tarefas burnup + label_agile_chart_dates: Intervalo de gráficos + label_agile_weighed_ideal_work_remaining: Ideal ponderado + label_agile_status_colors: Cor dos status + label_agile_charts_burnup: Burnup tarefas label_agile_charts_number_of_days: NĂşmero de dias label_agile_too_many_items: "O gráfico nĂŁo pode ser criado porque excede o nĂşmero máximo de itens que podem ser exibidos (%{max})" - label_agile_time_reports_items_limit: Time entries based charts issues limit + label_agile_time_reports_items_limit: Limite de problemas de gráficos baseados em entradas de tempo label_agile_total_work_remaining: Total - label_agile_default_chart: Version default chart + label_agile_default_chart: Gráfico padrĂŁo #1.1.1 label_agile_charts_average_velocity: Velocidade mĂ©dia - label_agile_charts_avarate_number_of_issues: NĂşmero mĂ©dio de tarefas - label_agile_charts_avarate_number_of_hours: NĂşmero mĂ©dio de horas + label_agile_charts_avarate_number_of_issues: MĂ©dia de nĂşmero de tarefas + label_agile_charts_avarate_number_of_hours: MĂ©dia nĂşmero de horas #1.2.0 - label_agile_color: Color - permission_manage_agile_verions: Manage version planning + label_agile_color: Cor + permission_manage_agile_verions: Gerencia planejamento da versĂŁo label_agile_version_planning: Planejamento da versĂŁo - error_agile_version_transition: Can’t change issue version - label_agile_tracker_colors: Tracker colors - label_agile_issue_priority_colors: Issue priority colors - label_agile_color_based_on: Colored by - label_agile_color_no_colors: No colors - label_agile_manage_colors: Manage colors - label_agile_fullscreen: Fullscreen - label_agile_no_version_issues: Issues without version - label_agile_charts_work_burnup: Work burnup - label_agile_completed: ConcluĂdo - label_agile_exclude_weekends: Exclude weekends from ideal effort - label_agile_board_truncated: "The board was truncated because it exceeds the maximum number of items that can be displayed (%{max})" - label_agile_board_items_limit: Agile board items limit - label_agile_swimlanes: Swim lanes - label_agile_minimize_closed: Minimize closed issues - label_agile_fields: Card fields \ No newline at end of file + error_agile_version_transition: NĂŁo Ă© possĂvel alterar a versĂŁo do problema + label_agile_tracker_colors: Cores por tipo + label_agile_issue_priority_colors: Cores pela prioridade das tarefas + label_agile_color_based_on: Colorir por + label_agile_color_no_colors: Sem cores + label_agile_manage_colors: Gerenciar cores + label_agile_fullscreen: Tela cheia + label_agile_no_version_issues: Tarefas sem versĂŁo + label_agile_charts_work_burnup_hours: Burnup de horas + label_agile_charts_work_burnup_sp: Burnup de pontos por histĂłrias + label_agile_completed: Completado + label_agile_exclude_weekends: Exclua fins de semana do esforço + label_agile_board_truncated: "O tabuleiro foi truncado porque excede o nĂşmero máximo de itens que podem ser exibidos (%{max})" + label_agile_board_items_limit: Limites de tarefas no quadro ágil + label_agile_swimlanes: Rais do quadro + label_agile_minimize_closed: Minimizar itens concluĂdos + label_agile_fields: Post-its + label_agile_default_board: Quadro padrĂŁo + + #1.3.6 + text_agile_move_not_possible: Esse movimento nĂŁo Ă© possĂvel + + #1.3.8 + label_agile_parent_issue_tracker_id: Tipo parental + label_agile_sub_issues: Sub-tarefas + label_agile_color_green: Verde + label_agile_color_blue: Azul + label_agile_color_turquoise: Turquesa + label_agile_color_lightgreen: Verde claro + label_agile_color_yellow: Amarelo + label_agile_color_orange: Laranja + label_agile_color_red: Vermelho + label_agile_color_purple: Roxo + label_agile_color_gray: Cinza + label_agile_has_sub_issues: Teve Sub-tarefas + label_agile_light_free_version: VersĂŁo gratuita Agile Light + label_agile_link_to_pro: Atualize para PRO + label_agile_link_to_pro_demo: Demo da versĂŁo PRO + label_agile_link_to_more_plugins: Procurar mais plugins no RedmineUP + label_agile_button_agree: Aceito + label_agile_license: RedmineUP Licensa + label_agile_saving_boards: Salvando quadro + label_agile_horizontal_swim_lines: Rais horizontais + label_agile_board_sub_columns: Quadro sub-colunas + label_agile_additional_agile_charts: Quadro ágil adicional + label_agile_coloured_issue_cards: Colorir post-its das tarefas + label_agile_4_more_features: 4 mais funcionalidades... + label_agile_upgrade_to_pro: Atualizar para versĂŁo PRO version para usar estas funcionalidades + + label_agile_day_in_state: Em status + + #1.3.10 + label_agile_hide_closed_issues_data: Ocultar dados de tarefas fechadas + project_module_agile: Agile + + #1.3.13 + label_agile_last_comment: Ăšltimo comentário + + #1.4.0 + label_agile_esitmate_units: Unidades estimadas + label_agile_trackers_for_sp: Tipos por pontos de histĂłrias + label_agile_story_points: Pontos por histĂłrias + field_story_points: Pontos por histĂłrias + label_agile_charts_number_of_story_points: NĂşmero de pontos por histĂłria + label_agile_board_columns: Colunas do quadro + lable_agile_wip_limit_exceeded: Limite de trabalho excedido + label_agile_wip_limit: Limite trabalho WIP + label_agile_add_new_issue: '+ Adicionar nova tarefa' + label_agile_allow_create_cards: Criação do post-it + label_agile_auto_assign_on_move: Adicionar o responsável ao mover + text_agile_create_issue_error: Ocorreu um erro na criação da tarefa + label_agile_inline_comment: Comentários em linha + label_agile_hours: Horas + field_color: Cores + + field_duration: Duração + + field_closed_on_trendline: Linha de tendĂŞncia fechada + field_created_on_trendline: Criar linha de tendĂŞncia + + label_agile_sp_values: Valor dos pontos por histĂłria + + label_agile_interval_size: Tamanho do intervalo + label_agile_day: Dia + label_agile_week: Semana + label_agile_month: MĂŞs + label_agile_quarter: Trimestre + label_agile_year: Ano + label_cards_search: Pesquisar por tĂtulo + + field_agile_sprint: Sprint + field_end_date: Encerramento + label_agile_sprints_on: Ativar sprints por padrĂŁo + label_agile_sprint: Sprint + label_agile_sprint_plural: Sprints + label_agile_sprint_name: Nome + label_agile_sprint_description: Descrição + label_agile_sprint_status: Status + label_agile_sprint_start_date: InĂcio em + label_agile_sprint_end_date: TĂ©rmino em + label_agile_sprint_new: Nova sprint + label_agile_sprint_status_open: Aberta + label_agile_sprint_status_active: Ativa + label_agile_sprint_status_closed: Fechada + label_agile_sprint_duration: Duração + label_agile_sprint_duration_select: <<Select duration>> + label_agile_sprint_duration_week_1: 1 semana + label_agile_sprint_duration_week_2: 2 semanas + label_agile_sprint_duration_week_3: 3 semanas + label_agile_sprint_duration_week_4: 4 semanas + label_agile_sprint_errors_crossed: As datas da sprint sĂŁo cruzadas com outra sprint existente + label_agile_sprint_errors_end_more_start: A data de tĂ©rmino deve ser posterior Ă data de inĂcio + label_agile_sprint_errors_open_issues: Sprint com problemas abertos pode ser fechado + label_agile_sprint_default_chart: Gráfico + label_agile_sprint_chart_units: Estimativa + label_agile_chart_for_sprint: "Para sprint: %{sprint}" + + label_agile_charts: Gráficos + label_agile_chart_burndown: Gráfico burndown + label_agile_chart_burnup: Gráfico burnup + label_agile_chart_units: Unidades + + label_agile_planning_board_more: Carregar mais... + label_agile_version_query_new: Consulta de novas versões do Agile + label_agile_version_my_boards: Meus quadros ágeis + label_agile_version_board_plural: Versões dos quados ágeis + label_agile_version_board_edit: Editar versĂŁo do quadro ágil + + label_agile_edit_issue: Editar tarefa + + label_agile_board_type: Tipo do quadro + label_agile_board_type_kanban: Quadro Kanban + label_agile_board_type_scrum: Quadro Scrum + label_agile_board_totals: Totais + label_agile_board_totals_story_points: Pontos por histĂłria + label_agile_board_totals_hours: Horas + label_agile_board_totals_spent_time: Tempo gasto + label_agile_board_totals_percent_done: Percentual de pronto + label_agile_board_totals_velocity: Velocidade + label_agile_board_totals_interval: Intervalo + label_agile_board_totals_remaining: Remanecente + + label_agile_sprint_list_active: Sprints ativos + label_agile_sprint_list_future: Sprints futuros + label_agile_sprint_list_old: Sprints antigos + + label_agile_mixed_trackers: Tipos mistos + label_agile_moving_average: MĂ©dia mĂłvel + + label_agile_board_backlog: Backlog + label_agile_board_backlog_column: Colunas do Backlog + label_agile_board_search_backlog_issues: Pesquisar tarefas do backlog + label_agile_version_plural: Vesões + label_agile_sprint_add: Adicionar novo Sprint + label_agile_version_add: Adicionar VersĂŁo + + label_agile_sprint_query_new: Nova consulta sprint ágil + label_agile_sprint_my_boards: Meus quadros de sprint + label_agile_sprint_board_plural: Quadros de sprint ágeis + label_agile_sprint_board_edit: Editar quadros de sprint + field_sprint: Sprint + label_agile_no_sprint_issues: Tarefas sem sprint + field_closed_sprints: Sprints fechadas + field_closed_versions: Versões fechadas diff --git a/plugins/redmine_agile/config/locales/ru.yml b/plugins/redmine_agile/config/locales/ru.yml old mode 100755 new mode 100644 index 5a28e54..d4ad02d --- a/plugins/redmine_agile/config/locales/ru.yml +++ b/plugins/redmine_agile/config/locales/ru.yml @@ -24,11 +24,14 @@ ru: label_agile_charts_number_of_hours: Кол-во чаŃов label_agile_charts_number_of_issues: Кол-во задач label_agile_charts_cumulative_flow: Накопительный поток - label_agile_charts_trackers_cumulative_flow: Накопительный поток треккеров + label_agile_charts_trackers_cumulative_flow: Накопительный поток трекеров label_agile_charts_issues_velocity: СкороŃŃ‚ŃŚ закрытия задач + label_agile_charts_cycle_time: Цикл задачи label_agile_charts_lead_time: Цикл задачи label_agile_charts_average_lead_time: Средний цикл задачи label_agile_chart_plural: Диаграммы + label_agile_chart_new: Новая диаграмма + label_agile_chart_edit: Редактировать Đ´Đ¸Đ°ĐłŃ€Đ°ĐĽĐĽŃ permission_view_agile_charts: ПроŃмотр диаграмм label_agile_ideal_work_remaining: Đдеально label_agile_actual_work_remaining: ДейŃтвительно @@ -43,7 +46,7 @@ ru: label_agile_too_many_items: "Диаграмма не может быть Ńоздана ĐżĐľŃ‚ĐľĐĽŃ Ń‡Ń‚Đľ превыŃено ĐĽĐ°ĐşŃимальное кол-во задач (%{max})" label_agile_time_reports_items_limit: МакŃимальное кол-во задач отображаемых на диаграммах label_agile_total_work_remaining: Đ’Ńего - label_agile_default_chart: Диаграмма для верŃии + label_agile_default_chart: Диаграмма по Ńмолчанию #1.1.1 label_agile_charts_average_velocity: Средняя ŃкороŃŃ‚ŃŚ закрытия задач @@ -55,7 +58,7 @@ ru: permission_manage_agile_verions: Планирование верŃий label_agile_version_planning: Планирование верŃий error_agile_version_transition: Невозможно изменить верŃию задачи - label_agile_tracker_colors: Цвета треккеров + label_agile_tracker_colors: Цвета трекеров label_agile_issue_priority_colors: Цвета приоритетов задачи label_agile_color_based_on: Цвета на ĐľŃновании label_agile_color_no_colors: Без цвета @@ -77,7 +80,7 @@ ru: text_agile_move_not_possible: Данное передвижение невозможно #1.3.8 - label_agile_parent_issue_tracker_id: Треккер родителя + label_agile_parent_issue_tracker_id: Трекер родителя label_agile_sub_issues: Подзадачи label_agile_color_green: Зеленый label_agile_color_blue: Синий @@ -130,3 +133,88 @@ ru: field_duration: ПродолжительноŃŃ‚ŃŚ label_cards_search: ПоиŃĐş по теме + + field_agile_sprint: Спринт + field_end_date: Дата окончания + label_agile_sprints_on: Đктивировать Ńпринты по Ńмолчанию + label_agile_sprint: Спринт + label_agile_sprint_plural: Спринты + label_agile_sprint_name: Название + label_agile_sprint_description: ОпиŃание + label_agile_sprint_status: СтатŃŃ + label_agile_sprint_sharing: СовмеŃтное иŃпользование + label_agile_sprint_start_date: Дата начала + label_agile_sprint_end_date: Дата окончания + label_agile_sprint_new: Новый Ńпринт + label_agile_sprint_status_open: Открытый + label_agile_sprint_status_active: Đктивный + label_agile_sprint_status_closed: Закрытый + label_agile_sprint_duration: ПродолжительноŃŃ‚ŃŚ + label_agile_sprint_duration_select: <<Выберите>> + label_agile_sprint_duration_week_1: 1 неделя + label_agile_sprint_duration_week_2: 2 недели + label_agile_sprint_duration_week_3: 3 недели + label_agile_sprint_duration_week_4: 4 недели + label_agile_sprint_errors_crossed: Выбранные Đ´Đ°Ń‚Ń‹ переŃекают Đ´Ń€Ńгой Ńпринт + label_agile_sprint_errors_end_more_start: Дата окончания должна быть больŃе Đ´Đ°Ń‚Ń‹ начала + label_agile_sprint_errors_open_issues: Нельзя закрыть Ńпринт Ńодержащий открытые задачи + label_agile_sprint_default_chart: График + label_agile_sprint_chart_units: Единицы + label_agile_chart_for_sprint: "Для Ńпринта: %{sprint}" + + label_agile_interval_size: Размер интервала + label_agile_day: День + label_agile_week: Неделя + label_agile_month: МеŃяц + label_agile_quarter: Квартал + label_agile_year: Год + + label_agile_charts: Диаграммы + label_agile_chart_units: Тип + + label_agile_planning_board_more: ЗагрŃзить еще... + label_agile_version_query_new: Новый Đ·Đ°ĐżŃ€ĐľŃ Đ˛ĐµŃ€Ńий + label_agile_version_my_boards: Мои Đ´ĐľŃки верŃий + label_agile_version_board_plural: ДоŃки верŃий + label_agile_version_board_edit: Редактировать Đ´ĐľŃĐşŃ Đ˛ĐµŃ€Ńий + + label_agile_edit_issue: Đзменить Đ·Đ°Đ´Đ°Ń‡Ń + + + label_agile_board_type: Тип Đ´ĐľŃки + label_agile_board_type_kanban: Kanban Đ´ĐľŃка + label_agile_board_type_scrum: Scrum Đ´ĐľŃка + label_agile_board_totals: Đтоги + label_agile_board_totals_story_points: Story points + label_agile_board_totals_hours: ЧаŃŃ‹ + label_agile_board_totals_spent_time: Потраченное время + label_agile_board_totals_percent_done: ГотовноŃŃ‚ŃŚ (%) + label_agile_board_totals_velocity: СкороŃŃ‚ŃŚ закрытия + + label_agile_sprint_list_active: Đктивные Ńпринты + label_agile_sprint_list_future: СледŃющие Ńпринты + label_agile_sprint_list_old: ПроŃлые Ńпринты + + label_agile_moving_average: Скользящая Ńредняя + + label_agile_board_backlog: Backlog + label_agile_board_backlog_column: Backlog column + label_agile_board_search_backlog_issues: ПоиŃĐş backlog задач + label_agile_version_plural: ВерŃии + label_agile_sprint_add: Добавить Ńпринт + label_agile_version_add: Добавить верŃию + + label_agile_sprint_query_new: Новый Đ·Đ°ĐżŃ€ĐľŃ Ńпринтов + label_agile_sprint_my_boards: Мои Đ´ĐľŃки Ńпринтов + label_agile_sprint_board_plural: ДоŃки Ńпринтов + label_agile_sprint_board_edit: Редактировать Đ´ĐľŃĐşŃ Ńпринтов + field_sprint: Спринт + label_agile_no_sprint_issues: Задачи без Ńпринта + field_closed_sprints: Закрытые Ńпринты + field_closed_versions: Закрытые верŃии + + label_agile_sprint_sharing_none: Без ŃовмеŃтного иŃпользования + label_agile_sprint_sharing_descendants: С подпроектами + label_agile_sprint_sharing_hierarchy: С иерархией проектов + label_agile_sprint_sharing_tree: С деревом проектов + label_agile_sprint_sharing_system: Со вŃеми проектами diff --git a/plugins/redmine_agile/config/locales/zh-TW.yml b/plugins/redmine_agile/config/locales/zh-TW.yml index 821c70b..1751082 100644 --- a/plugins/redmine_agile/config/locales/zh-TW.yml +++ b/plugins/redmine_agile/config/locales/zh-TW.yml @@ -3,7 +3,7 @@ # Author: zhoutt # Based on file: en.yml -'zh-TW': +zh-TW: label_agile: 敏捷 label_agile_board: 敏捷看板 label_agile_board_plural: 敏捷看板 @@ -89,4 +89,4 @@ label_agile_color_red: 紅色 label_agile_color_purple: 紫色 label_agile_color_gray: ç°č‰˛ - label_agile_has_sub_issues: ĺ問題 + label_agile_has_sub_issues: ĺ問題 \ No newline at end of file diff --git a/plugins/redmine_agile/config/routes.rb b/plugins/redmine_agile/config/routes.rb old mode 100755 new mode 100644 index 2a340d4..21dc7b9 --- a/plugins/redmine_agile/config/routes.rb +++ b/plugins/redmine_agile/config/routes.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -21,28 +21,19 @@ # See: http://guides.rubyonrails.org/routing.html resources :projects do - resources :agile_queries, :only => [:new, :create] - resources :agile_versions, :only => [:index] do - post :index, :on => :collection - end -end - -resources :agile_versions, :only => [:update, :show] do - collection do - get 'load' - get 'autocomplete' - end + resources :agile_queries, only: [:new, :create] end resources :issues do get "done_ratio", :to => "agile_journal_details#done_ratio" get "status", :to => "agile_journal_details#status" get "assignee", :to => "agile_journal_details#assignee" + member do + get "agile_data", :to => "agile_boards#agile_data" + end end resources :agile_queries -get '/agile_colors/:object_type', :to => "agile_colors#index", :as => "agile_colors" -put '/agile_colors/:object_type', :to => "agile_colors#update", :as => "update_agile_colors" get '/projects/:project_id/agile/charts', :to => "agile_charts#show", :as => "project_agile_charts" get '/agile/charts/', :to => "agile_charts#show", :as => "agile_charts" diff --git a/plugins/redmine_agile/db/migrate/001_create_issue_status_orders.rb b/plugins/redmine_agile/db/migrate/001_create_issue_status_orders.rb old mode 100755 new mode 100644 index 24edba7..c345ce3 --- a/plugins/redmine_agile/db/migrate/001_create_issue_status_orders.rb +++ b/plugins/redmine_agile/db/migrate/001_create_issue_status_orders.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/db/migrate/002_create_agile_colors.rb b/plugins/redmine_agile/db/migrate/002_create_agile_colors.rb index c835f2b..1e705e8 100644 --- a/plugins/redmine_agile/db/migrate/002_create_agile_colors.rb +++ b/plugins/redmine_agile/db/migrate/002_create_agile_colors.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/db/migrate/003_rename_issue_status_orders.rb b/plugins/redmine_agile/db/migrate/003_rename_issue_status_orders.rb index 751c90f..c21e129 100644 --- a/plugins/redmine_agile/db/migrate/003_rename_issue_status_orders.rb +++ b/plugins/redmine_agile/db/migrate/003_rename_issue_status_orders.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/db/migrate/004_rename_agile_ranks.rb b/plugins/redmine_agile/db/migrate/004_rename_agile_ranks.rb index 1b8cddf..de7b86f 100644 --- a/plugins/redmine_agile/db/migrate/004_rename_agile_ranks.rb +++ b/plugins/redmine_agile/db/migrate/004_rename_agile_ranks.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/db/migrate/005_add_story_points_to_agile_ranks.rb b/plugins/redmine_agile/db/migrate/005_add_story_points_to_agile_ranks.rb index cece10b..16ef93d 100644 --- a/plugins/redmine_agile/db/migrate/005_add_story_points_to_agile_ranks.rb +++ b/plugins/redmine_agile/db/migrate/005_add_story_points_to_agile_ranks.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/doc/CHANGELOG b/plugins/redmine_agile/doc/CHANGELOG old mode 100755 new mode 100644 index d769450..35be743 --- a/plugins/redmine_agile/doc/CHANGELOG +++ b/plugins/redmine_agile/doc/CHANGELOG @@ -1,9 +1,130 @@ == Redmine Agile plugin changelog Redmine Agile plugin - Agile board plugin for redmine -Copyright (C) 2011-2018 RedmineUP +Copyright (C) 2011-2021 RedmineUP http://www.redmineup.com/ +== 2021-05-04 v1.6.1 + +* Redmine 4.2 support +* Added assignee group board filter +* Added past Sprints +* Added reset sprint on project change +* Fixed missied table SQL error +* Fixed empty data error +* Fixed FCSV warning +* Dropped Redmine <3.0 support + +== 2021-02-15 v1.6.0 + +* Added Agile Sprint sharing +* Fixed sprint/story points copy +* Fixed sprint for on-board created sprints +* Fixed initial install error +* Fixed Sprint query save bug +* Fixed chart dates interval +* Fixed for all-projects query bug +* Fixed chart empty dates bug +* Fixed chart version bug +* Fixed Backlog load-more bug +* Fixed Agile board remarks +* Fixed selected sprint bug +* Fixed Agile board issues display bug +* Fixed missed filters for old Redmine versions +* Fixed empty watcher filter bug +* Fixed workflow issue creation bug +* Updated BR locale (Adriano J. Baptistella) +* Updated pt-BR locale (Adriano J. Baptistella) + +== 2020-07-24 v1.5.4 + +* Added None for Sprint in issue context menu +* Updated zh-tw locale (Hsiao Chung Chiang) +* Updated es locale (Manuel Alba Ortega) +* Fixed agile sprint remaining bug +* Fixed issue copy error +* Fixed submit error for issue search field +* Fixed future data in the charts +* Fixed error with all closed issues +* Fixed full screen board update bug +* Fixed first day for Agile chart +* Fixed chart query visibility bug +* Fixed no version column bug +* Fixed sub project sprint assignment +* Fixed backlog column saving +* Fixed "For all projects" chart bug + +== 2020-03-18 v1.5.3 + +* Added grouping for agine status changes view +* Added issue list sprint filter +* German translation updated (Peter Schossig) +* Added Italian translation (Marco Congia) +* Fixed bug with context menu story points +* Added validation for sprint open issues +* Backlog tab default menu + +== 2020-02-05 v1.5.2 + +* Agile Backlog as a different module +* Fixed between dates chart bug +* Fixed context menu edit bug +* Fixed sprint cards load bug + +== 2019-12-10 v1.5.1 + +* Added Sprint view permissions +* Redmine 4.1 styles support +* New Story points board setting +* Hide Sprint field without available sprints +* Modal edit styles cleanup +* Fixed sprint charts bug +* Fixed MySQL order by bug + +== 2019-10-22 v1.5.0 + +* Agile sprints +* New Backlog tab +* Board types: Scrum and Kanban +* New lead time chart +* Totals for board and swimlanes +* Added board units +* Added board default chart +* Fixed chart caclulation bug +* Fixed custom field js error + +== 2019-06-14 v1.4.12 + +* Fixed light version Redmine 4.0.3+ bug +* Fixed swilanes sorting +* Fixed version planing Load more bug + +== 2019-05-15 v1.4.11 + +* Fixed compatibility issues + +== 2019-04-25 v1.4.10 + +* Fixed light version compatibility bug + +== 2019-04-15 v1.4.9 + +* Redmine 4.0.3 support +* Agile chart units setting +* Charts end date fixes +* Fixed expand all board bug +* Fixed interval size for old user sessions +* Fixed charts with current interval + +== 2019-02-08 v1.4.8 + +* Added new Version planner +* Added saving for Agile charts +* Fixed "Related to" filter +* Fixed "Parent task" filter +* Fixed search sensitive bug +* Fixed empty current version bug + == 2018-09-25 v1.4.7 * Added missing filters: Issue, Description, Private, Watcher @@ -33,10 +154,10 @@ http://www.redmineup.com/ == 2017-07-06 v1.4.4 -* Redmine 3.4 support +* Redmine 3.4 support * Color attribute for users * Agile board <Current version> query filter -* Added initional state for status and assignee history +* Added initional state for status and assignee history * Chinese translation update * Fixed checklist items order diff --git a/plugins/redmine_agile/init.rb b/plugins/redmine_agile/init.rb old mode 100755 new mode 100644 index 53e8ad8..8cf0285 --- a/plugins/redmine_agile/init.rb +++ b/plugins/redmine_agile/init.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -17,12 +17,17 @@ # You should have received a copy of the GNU General Public License # along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. -requires_redmine_crm :version_or_higher => '0.0.32' rescue raise "\n\033[31mRedmine requires newer redmine_crm gem version.\nPlease update with 'bundle update redmine_crm'.\033[0m" +requires_redmine_crm version_or_higher: '0.0.43' rescue raise "\n\033[31mRedmine requires newer redmine_crm gem version.\nPlease update with 'bundle update redmine_crm'.\033[0m" require 'redmine' -AGILE_VERSION_NUMBER = '1.4.7' -AGILE_VERSION_TYPE = 'PRO version' +AGILE_VERSION_NUMBER = '1.6.1' +AGILE_VERSION_TYPE = "Light version" + +if ActiveRecord::VERSION::MAJOR >= 4 && !defined?(FCSV) + require 'csv' + FCSV = CSV +end Redmine::Plugin.register :redmine_agile do name "Redmine Agile plugin (#{AGILE_VERSION_TYPE})" @@ -32,28 +37,34 @@ Redmine::Plugin.register :redmine_agile do url 'http://redmineup.com/pages/plugins/agile' author_url 'mailto:support@redmineup.com' - requires_redmine :version_or_higher => '2.6' + requires_redmine version_or_higher: '3.0' - settings :default => { 'default_columns' => %w(tracker assigned_to) }, - :partial => 'settings/agile/general' + settings default: { 'default_columns' => %w(tracker assigned_to) }, + partial: 'settings/agile/general' menu :application_menu, :agile, - { :controller => 'agile_boards', :action => 'index' }, - :caption => :label_agile, - :if => Proc.new { User.current.allowed_to?(:view_agile_queries, nil, :global => true) } - menu :project_menu, :agile, {:controller => 'agile_boards', :action => 'index' }, - :caption => :label_agile, - :after => :gantt, - :param => :project_id + { controller: 'agile_boards', action: 'index' }, + caption: :label_agile, + if: Proc.new { User.current.allowed_to?(:view_agile_queries, nil, global: true) } + menu :project_menu, :agile, { controller: 'agile_boards', action: 'index' }, caption: :label_agile, + after: :gantt, + param: :project_id - menu :admin_menu, :agile, {:controller => 'settings', :action => 'plugin', :id => "redmine_agile"}, :caption => :label_agile, :html => {:class => 'icon'} + menu :admin_menu, :agile, { controller: 'settings', action: 'plugin', id: 'redmine_agile' }, caption: :label_agile, html: { class: 'icon' } project_module :agile do - permission :manage_public_agile_queries, {:agile_queries => [:new, :create, :edit, :update, :destroy]}, :require => :member - permission :manage_agile_verions, {:agile_versions => [:index, :update]} - permission :add_agile_queries, {:agile_queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin - permission :view_agile_queries, {:agile_boards => [:index, :create_issue], :agile_queries => :index}, :read => true - permission :view_agile_charts, {:agile_charts => [:show, :render_chart, :select_version_chart]}, :read => true + permission :manage_public_agile_queries, { agile_queries: [:new, :create, :edit, :update, :destroy] }, require: :member + permission :add_agile_queries, { agile_queries: [:new, :create, :edit, :update, :destroy] }, require: :loggedin + permission :view_agile_queries, { agile_boards: [:index, + :update, + :create_issue, + :issue_tooltip, + :inline_comment, + :agile_data, + :backlog_load_more, + :backlog_autocomplete], + agile_queries: :index }, read: true + permission :view_agile_charts, { agile_charts: [:show, :render_chart, :select_version_chart] }, read: true end end diff --git a/plugins/redmine_agile/lib/acts_as_colored/init.rb b/plugins/redmine_agile/lib/acts_as_colored/init.rb deleted file mode 100644 index 3ff28d4..0000000 --- a/plugins/redmine_agile/lib/acts_as_colored/init.rb +++ /dev/null @@ -1,21 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.dirname(__FILE__) + '/lib/acts_as_colored' -ActiveRecord::Base.send(:include, RedmineAgile::Acts::Colored) diff --git a/plugins/redmine_agile/lib/acts_as_colored/lib/acts_as_colored.rb b/plugins/redmine_agile/lib/acts_as_colored/lib/acts_as_colored.rb deleted file mode 100644 index f8701bb..0000000 --- a/plugins/redmine_agile/lib/acts_as_colored/lib/acts_as_colored.rb +++ /dev/null @@ -1,69 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Acts - module Colored - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def acts_as_colored(_options = {}) - return if included_modules.include?(RedmineAgile::Acts::Colored::InstanceMethods) - send :include, RedmineAgile::Acts::Colored::InstanceMethods - - class_eval do - has_one :agile_color, :as => :container, :dependent => :destroy - delegate :color, :to => :agile_color, :allow_nil => true - - accepts_nested_attributes_for :agile_color, :reject_if => :reject_color, :allow_destroy => true - - alias_method :agile_color_without_default, :agile_color - alias_method :agile_color, :agile_color_with_default - end - end - end - - module InstanceMethods - def self.included(base) - base.extend ClassMethods - end - - def reject_color(attributes) - exists = attributes['id'].present? - empty = attributes[:color].blank? - attributes[:_destroy] = 1 if exists && empty - !exists && empty - end - - def color=(value) - agile_color.color = value - end - - def agile_color_with_default - agile_color_without_default || build_agile_color - end - - module ClassMethods - end - end - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile.rb b/plugins/redmine_agile/lib/redmine_agile.rb old mode 100755 new mode 100644 index 300f6a0..9e292d4 --- a/plugins/redmine_agile/lib/redmine_agile.rb +++ b/plugins/redmine_agile/lib/redmine_agile.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -17,51 +17,31 @@ # You should have received a copy of the GNU General Public License # along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. -require 'redmine_agile/patches/compatibility/application_controller_patch' if Rails::VERSION::MAJOR < 4 -require 'acts_as_colored/init' + require 'redmine_agile/hooks/views_layouts_hook' require 'redmine_agile/hooks/views_issues_hook' require 'redmine_agile/hooks/views_versions_hook' require 'redmine_agile/hooks/controller_issue_hook' -require 'redmine_agile/hooks/helper_issues_hook' require 'redmine_agile/patches/issue_patch' -require 'redmine_agile/patches/compatibility_patch' - require 'redmine_agile/helpers/agile_helper' require 'redmine_agile/charts/agile_chart' require 'redmine_agile/charts/burndown_chart' require 'redmine_agile/charts/work_burndown_chart' -require 'redmine_agile/charts/velocity_chart' -require 'redmine_agile/charts/cumulative_flow_chart' -require 'redmine_agile/charts/trackers_cumulative_flow_chart' -require 'redmine_agile/charts/burnup_chart' -require 'redmine_agile/charts/work_burnup_chart' -require 'redmine_agile/charts/lead_time_chart' -require 'redmine_agile/charts/average_lead_time_chart' - -require 'redmine_agile/patches/issue_priority_patch' -require 'redmine_agile/patches/issue_query_patch' -require 'redmine_agile/patches/tracker_patch' -require 'redmine_agile/patches/project_patch' -require 'redmine_agile/hooks/views_context_menus_hook' -require 'redmine_agile/hooks/views_projects_form_hook' - -require 'redmine_agile/utils/header_tree' - -require 'redmine_agile/patches/user_patch' -require 'redmine_agile/hooks/views_users_form_hook' -require 'redmine_agile/patches/queries_controller_patch' if Redmine::VERSION.to_s >= '3.4' +require 'redmine_agile/charts/charts' +require 'redmine_agile/patches/issue_drop_patch' module RedmineAgile ISSUES_PER_COLUMN = 10 TIME_REPORTS_ITEMS = 1000 BOARD_ITEMS = 500 - ESTIMATE_UNITS = ['hours', 'story_points'] - COLOR_BASE = ['issue', 'tracker', 'priority', 'spent_time', 'user', 'project'] + + ESTIMATE_HOURS = 'hours'.freeze + ESTIMATE_STORY_POINTS = 'story_points'.freeze + ESTIMATE_UNITS = [ESTIMATE_HOURS, ESTIMATE_STORY_POINTS].freeze class << self def time_reports_items_limit @@ -84,7 +64,7 @@ module RedmineAgile end def default_chart - Setting.plugin_redmine_agile['default_chart'] || 'issues_burndown' + Setting.plugin_redmine_agile['default_chart'] || Charts::BURNDOWN_CHART end def estimate_units @@ -92,7 +72,11 @@ module RedmineAgile end def use_story_points? - estimate_units == 'story_points' + if Setting.plugin_redmine_agile.key?('story_points_on') + Setting.plugin_redmine_agile['story_points_on'] == '1' + else + estimate_units == ESTIMATE_STORY_POINTS + end end def trackers_for_sp @@ -100,18 +84,18 @@ module RedmineAgile end def use_story_points_for?(tracker) - return true if trackers_for_sp.blank? + return true if trackers_for_sp.blank? && use_story_points? tracker = tracker.is_a?(Tracker) ? tracker.id.to_s : tracker - trackers_for_sp == tracker + trackers_for_sp == tracker && use_story_points? end def use_colors? - COLOR_BASE.include?(color_base) - end + false + end def color_base - Setting.plugin_redmine_agile['color_on'] || 'none' - end + "none" + end def minimize_closed? Setting.plugin_redmine_agile['minimize_closed'].to_i > 0 @@ -124,19 +108,10 @@ module RedmineAgile def auto_assign_on_move? Setting.plugin_redmine_agile['auto_assign_on_move'].to_i > 0 end - def color_prefix - 'bk' - end - - COLOR_BASE.each do |cb| - define_method :"#{cb}_colors?" do - color_base == cb - end - end def status_colors? - Setting.plugin_redmine_agile['status_colors'].to_i > 0 - end + false + end def hide_closed_issues_data? Setting.plugin_redmine_agile['hide_closed_issues_data'].to_i > 0 @@ -147,15 +122,12 @@ module RedmineAgile end def allow_create_card? - Setting.plugin_redmine_agile['allow_create_card'].to_i > 0 - end + false + end def allow_inline_comments? Setting.plugin_redmine_agile['allow_inline_comments'].to_i > 0 end - def sp_values - Setting.plugin_redmine_agile['sp_values'].to_s.split(',').map{|x| x.strip.to_i}.uniq.delete_if{|x| x == 0} - end end end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/agile_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/agile_chart.rb index a8df261..f152dc3 100644 --- a/plugins/redmine_agile/lib/redmine_agile/charts/agile_chart.rb +++ b/plugins/redmine_agile/lib/redmine_agile/charts/agile_chart.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -22,22 +22,32 @@ module RedmineAgile include Redmine::I18n include Redmine::Utils::DateCalculation + DAY_INTERVAL = 'day'.freeze + WEEK_INTERVAL = 'week'.freeze + MONTH_INTERVAL = 'month'.freeze + QUARTER_INTERVAL = 'quarter'.freeze + YEAR_INTERVAL = 'year'.freeze + + TIME_INTERVALS = [DAY_INTERVAL, WEEK_INTERVAL, MONTH_INTERVAL, QUARTER_INTERVAL, YEAR_INTERVAL].freeze + attr_reader :line_colors - def initialize(data_scope, options={}) + def initialize(data_scope, options = {}) + @options = options @data_scope = data_scope @data_from ||= options[:data_from] @data_to ||= options[:data_to] - @period_count, @scale_division = chart_periods + @interval_size = options[:interval_size] || DAY_INTERVAL + initialize_chart_periods @step_x_labels = @period_count > 18 ? @period_count / 12 + 1 : 1 @fields = chart_fields_by_period @weekend_periods = weekend_periods - @estimated_unit = options[:estimated_unit] || 'hours' + @estimated_unit = options[:estimated_unit] || ESTIMATE_HOURS @line_colors = {} end def data - { :title => '', :y_title => '', :labels => [], :datasets => [] } + { title: '', y_title: '', labels: [], datasets: [] } end def self.data(data_scope, options = {}) @@ -47,17 +57,22 @@ module RedmineAgile protected def current_date_period - date_period = (@date_to <= Date.today ? @period_count : (@period_count - (@date_to - Date.today).to_i / @scale_division - 1) + 1).round + return @current_date_period if @current_date_period + + date_period = (@date_to <= Date.today || @options[:date_to].present? ? @period_count : (@period_count - (@date_to - Date.today).to_i / @scale_division) + 1).round @current_date_period ||= date_period > 0 ? date_period : 0 end def due_date_period + @date_from = @date_from.to_date + @date_to = @date_to.to_date due_date = (@due_date && @due_date > @date_from) ? @due_date : @date_from - @due_date_period ||= (@due_date ? @period_count - (@date_to - due_date).to_i / @scale_division - 1 : @period_count - 1) + 1 + @due_date_period ||= (@due_date ? @period_count - (@date_to - due_date.to_date).to_i : @period_count - 1) + 1 + @due_date_period = @due_date_period > 0 ? @due_date_period : 1 end def date_short_period? - (@date_to - @date_from).to_i <= 31 + (@date_to.to_date - @date_from.to_date).to_i <= 31 end def date_effort(issues, effort_date) @@ -127,25 +142,24 @@ module RedmineAgile color = options[:color] || [rand(255), rand(255), rand(255)].join(',') dataset_color = "rgba(#{color}, 1)" { - :type => (options[:type] || 'line'), - :data => dataset_data, - :label => label, - :fill => (options[:fill] || false), - :backgroundColor => "rgba(#{color}, 0.2)", - :borderColor => dataset_color, - :borderDash => (options[:dashed] ? [5, 5] : []), - :borderWidth => (options[:dashed] ? 1.5 : 2), - :pointRadius => (options[:nopoints] ? 0 : 3), - :pointBackgroundColor => dataset_color + type: (options[:type] || 'line'), + data: dataset_data, + label: label, + fill: (options[:fill] || false), + backgroundColor: "rgba(#{color}, 0.2)", + borderColor: dataset_color, + borderDash: (options[:dashed] ? [5, 5] : []), + borderWidth: (options[:dashed] ? 1.5 : 2), + pointRadius: (options[:nopoints] ? 0 : 3), + pointBackgroundColor: dataset_color, + tooltips: { enable: false } } end - def chart_periods - raise "Dates can't be blank" if [@date_to, @date_from].any?(&:blank?) - period_count = (@date_to.to_date + 1 - @date_from.to_date).to_i - scale_division = period_count > 31 ? period_count / 31.0 : 1 - - [(period_count / scale_division).round, scale_division] + def initialize_chart_periods + raise Exception "Dates can't be blank" if [@date_to, @date_from].any?(&:blank?) + period_count + scale_division end def issues_count_by_period(issues_scope) @@ -174,16 +188,17 @@ module RedmineAgile end def chart_fields_by_period - chart_dates_by_period.map do |d| - if @scale_division >= 365 - d.year - elsif @scale_division >= 13 - month_abbr_name(d.at_beginning_of_week.to_time.month) + ' ' + d.at_beginning_of_week.to_time.year.to_s - elsif @scale_division >= 7 - d.at_beginning_of_week.to_time.day.to_s + ' ' + month_name(d.at_beginning_of_week.to_time.month) - else - d.to_time.day.to_s + ' ' + month_name(d.to_time.month) - end + chart_dates_by_period.map { |d| chart_field_by_date(d) } + end + + def chart_field_by_date(date) + case @interval_size + when YEAR_INTERVAL + date.year + when QUARTER_INTERVAL, MONTH_INTERVAL + month_abbr_name(date.month) + ' ' + date.year.to_s + else + date.day.to_s + ' ' + month_name(date.month) end end @@ -206,13 +221,12 @@ module RedmineAgile end def chart_dates_by_period - @chart_dates_by_period ||= @period_count.times.inject([]) do |accum, m| + return @chart_dates_by_period if @chart_dates_by_period + + period = period_count > 1 ? period_count - 1 : period_count + @chart_dates_by_period ||= period.times.inject([]) do |accum, m| period_date = ((@date_to.to_date - 1 - m * @scale_division) + 1) - accum << if m == 0 || m == @period_count - 1 - period_date.to_date - elsif @scale_division >= 13 - period_date.at_beginning_of_week.to_date - elsif @scale_division >= 7 + accum << if @interval_size == WEEK_INTERVAL period_date.at_beginning_of_week.to_date else period_date.to_date @@ -248,5 +262,17 @@ module RedmineAgile def predict(x, slope, intercept) slope * x + intercept end + + def period_count + @period_count ||= ((@date_to.to_time - @date_from.to_time) / time_divider).round + 1 + end + + def scale_division + @scale_division ||= time_divider / 1.day + end + + def time_divider + @interval_size == QUARTER_INTERVAL ? 3.months : 1.send(@interval_size) + end end end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/average_lead_time_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/average_lead_time_chart.rb deleted file mode 100644 index a49e160..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/average_lead_time_chart.rb +++ /dev/null @@ -1,67 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class AverageLeadTimeChart < LeadTimeChart - def initialize(data_scope, options = {}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = options[:date_to] || Date.today - @average_lead_time = !!options[:average_lead_time] - super data_scope, options - @line_colors = { :closed => '247,175,125' } - end - - def data - chart_data = average_lead_time_data - datasets = [] - datasets << dataset(chart_data, l(:field_closed_on), :color => line_colors[:closed]) if chart_data.any? - - { - :title => l(:label_agile_charts_lead_time), - :y_title => l(:label_agile_charts_number_of_days), - :labels => @fields, - :datasets => datasets - } - end - - private - - def average_lead_time_data - lead_time_by_date = closed_issues.map { |c| { :closed_on => c.closed_on, :lead_time => (c.closed_on.to_time - c.created_on.to_time).to_f / (60 * 60 * 24) } } - lead_time_by_period = [0] * @period_count - lead_time_by_date.each do |c| - next if c[:closed_on].to_date > @date_to.to_date - period_num = (@date_to.to_date - c[:closed_on].to_date).to_i / @scale_division - if lead_time_by_period[period_num] - lead_time_by_period[period_num] = c[:lead_time] unless lead_time_by_period[period_num].to_i > 0 - lead_time_by_period[period_num] = ((lead_time_by_period[period_num].to_f + c[:lead_time]).to_f / 2).round(2) - end - end - lead_time_by_period.reverse! - - prev_lead_time = lead_time_by_period[0] - lead_time_by_period.each_with_index do |c, index| - lead_time_by_period[index] = c == 0 ? prev_lead_time : ((c + prev_lead_time).to_f / 2).round(2) - prev_lead_time = lead_time_by_period[index] - end - - lead_time_by_period - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/burndown_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/burndown_chart.rb index 5d23a67..6a21a57 100644 --- a/plugins/redmine_agile/lib/redmine_agile/charts/burndown_chart.rb +++ b/plugins/redmine_agile/lib/redmine_agile/charts/burndown_chart.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -21,13 +21,15 @@ module RedmineAgile class BurndownChart < AgileChart attr_accessor :burndown_data, :cumulative_burndown_data - def initialize(data_scope, options={}) + def initialize(data_scope, options = {}) @date_from = options[:date_from] && options[:date_from].to_date || [data_scope.minimum("#{Issue.table_name}.created_on"), data_scope.minimum("#{Issue.table_name}.start_date")].compact.map(&:to_date).min + @date_to = options[:date_to] && options[:date_to].to_date || [options[:due_date], data_scope.maximum("#{Issue.table_name}.updated_on")].compact.map(&:to_date).max + @due_date = options[:due_date].to_date if options[:due_date] @show_ideal_effort = options[:date_from] && options[:date_to] @@ -56,10 +58,21 @@ module RedmineAgile :title => @graph_title, :y_title => @y_title, :labels => @fields, - :datasets => datasets + :datasets => datasets, + :show_tooltips => [0, 2] } end + def self.data(data_scope, options = {}) + if options[:chart_unit] == Charts::UNIT_HOURS + WorkBurndownChart.new(data_scope, options.merge(estimated_unit: ESTIMATE_HOURS)).data + elsif options[:chart_unit] == Charts::UNIT_STORY_POINTS + WorkBurndownChart.new(data_scope, options.merge(estimated_unit: ESTIMATE_STORY_POINTS)).data + else + super + end + end + protected def ideal_effort(start_remaining) diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/burnup_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/burnup_chart.rb deleted file mode 100644 index 31a03b1..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/burnup_chart.rb +++ /dev/null @@ -1,81 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class BurnupChart < AgileChart - def initialize(data_scope, options = {}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = (options[:date_to] || Date.today).to_date - @due_date = options[:due_date].to_date if options[:due_date] - - super data_scope, options - - @fields = [''] + @fields - @y_title = l(:label_agile_charts_number_of_issues) - @graph_title = l(:label_agile_charts_burnup) - @line_colors = { :created => '102,102,102', :closed => '80,122,170', :ideal => '102,102,102' } - end - - def data - return false unless calculate_data.any? - - datasets = [ - dataset(@cumulative_data, l(:field_created_on),:fill => true, :color => line_colors[:created]), - dataset(@data, l(:field_closed_on), :fill => true, :color => line_colors[:closed]), - dataset(ideal_effort(@data.first, @cumulative_data.last), l(:label_agile_ideal_work_remaining), :color => line_colors[:ideal], :dashed => true, :nopoints => true) - ] - - { - :title => @graph_title, - :y_title => @y_title, - :labels => @fields, - :datasets => datasets - } - end - - protected - - def ideal_effort(start_data, end_data) - data = [0] * (due_date_period - 1) - active_periods = (RedmineAgile.exclude_weekends? && date_short_period?) ? due_date_period - @weekend_periods.select { |p| p < due_date_period }.count : due_date_period - avg_remaining_velocity = (end_data - start_data).to_f / active_periods.to_f - sum = start_data.to_f - data[0] = sum - (1..due_date_period - 1).each do |i| - sum += avg_remaining_velocity unless (RedmineAgile.exclude_weekends? && date_short_period?) && @weekend_periods.include?(i - 1) - data[i] = (sum * 100).round / 100.0 - end - data[due_date_period] = end_data - data - end - - def calculate_data - created_by_period = issues_count_by_period(scope_by_created_date) - closed_by_period = issues_count_by_period(scope_by_closed_date) - - total_issues = @data_scope.where("#{Issue.table_name}.created_on < ?", @date_from).count - total_closed = @data_scope.open(false).where("#{Issue.table_name}.closed_on < ?", @date_from).count - - sum = total_issues - @cumulative_data = [total_issues] + created_by_period.first(current_date_period).map { |x| sum += x } - sum = total_closed - @data = [total_closed] + closed_by_period.first(current_date_period).map { |x| sum += x } - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/cumulative_flow_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/cumulative_flow_chart.rb deleted file mode 100644 index 3029f46..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/cumulative_flow_chart.rb +++ /dev/null @@ -1,72 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class CumulativeFlowChart < AgileChart - def initialize(data_scope, options = {}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = options[:date_to] || Date.today - super data_scope, options - @line_colors = { 0 => '255,204,0', 1 => '0,153,0', 2 => '80,122,170', 3 => '102,102,102', 4 => '154,167,208', 5 => '224,112,235', - 6 => '235,17,142', 7 => '167,40,6', 8 => '108,97,58', 9 => '33,147,155', 10 => '43,237,59' } - end - - def data - datasets = [] - all_issues = @data_scope.eager_load(:journals => { :details => :journal }) - data = chart_dates_by_period.map do |date| - issues = all_issues.select { |issue| issue.created_on.localtime.to_date <= date } - issues.inject({}) do |accum, issue| - status_details = issue.journals.map(&:details).flatten.select { |detail| 'status_id' == detail.prop_key }.sort_by { |a| a.journal.created_on } - details_today_or_earlier = status_details.select { |a| a.journal.created_on.to_date <= date } - last_status_change = details_today_or_earlier.last - - status = if last_status_change - last_status_change.value.to_i - elsif status_details.size > 0 - status_details.first.old_value.to_i - else - issue.status_id - end - - accum[status] = accum[status].to_i + 1 - accum - end - end - - IssueStatus.where(:id => data.map(&:keys).flatten.uniq).sorted.reverse.each_with_index do |status, index| - datasets << dataset(data.map { |d| d[status.id].to_i }, status.name, :color => line_colors[index], :fill => true, :nopoints => true) unless data.empty? - end - - { - :title => l(:label_agile_charts_cumulative_flow), - :y_title => l(:label_agile_charts_number_of_issues), - :labels => @fields, - :stacked => true, - :datasets => datasets - } - end - - private - - def available_statuses - @available_statuses ||= IssueStatus.find(@data_scope.group("#{Issue.table_name}.status_id").count.keys).map - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/lead_time_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/lead_time_chart.rb deleted file mode 100644 index dc7bc72..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/lead_time_chart.rb +++ /dev/null @@ -1,77 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class LeadTimeChart < AgileChart - def initialize(data_scope, options = {}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = options[:date_to] || Date.today - @average_lead_time = !!options[:average_lead_time] - super data_scope, options - @line_colors = { :closed => '102,102,102' } - end - - def data - chart_data = lead_time_data - datasets = [] - if chart_data.any? - datasets << dataset(chart_data, l(:field_closed_on), :type => 'bar', :fill => true, :color => line_colors[:closed]) - datasets << dataset(trendline(chart_data), l(:field_closed_on_trendline), :nopoints => true, :dashed => true, :color => line_colors[:closed]) - end - { - :title => l(:label_agile_charts_lead_time), - :y_title => l(:label_agile_charts_number_of_days), - :labels => @fields, - :datasets => datasets - } - end - - private - - def lead_time_data - lead_time_by_date = closed_issues.map { |c| { :closed_on => c.closed_on.localtime, :lead_time => (c.closed_on.to_time.localtime - c.created_on.localtime.to_time).to_f / (60 * 60 * 24) } } - lead_time_arr_by_period = {} - lead_time_by_date.each do |c| - next if c[:closed_on].to_date > @date_to.to_date - period_num = ((@date_to.to_date - c[:closed_on].localtime.to_date).to_i / @scale_division).to_i - lead_time_arr_by_period[period_num] = [] if lead_time_arr_by_period[period_num].blank? - lead_time_arr_by_period[period_num] << c[:lead_time] - end - - lead_time_by_period = [0] * @period_count - (0..@period_count - 1).each do |period_num| - next if lead_time_arr_by_period[period_num].blank? - arr = lead_time_arr_by_period[period_num] - len = arr.length - half_len = len / 2 - sorted = arr.sort - median = len % 2 == 1 ? sorted[half_len] : (sorted[half_len - 1] + sorted[half_len]).to_f / 2 - lead_time_by_period[period_num] = median.round(2) - end - lead_time_by_period.reverse! - end - - def closed_issues - @closed_issues ||= @data_scope.open(false).where("#{Issue.table_name}.closed_on IS NOT NULL"). - where("#{Issue.table_name}.closed_on >= ?", @date_from). - where("#{Issue.table_name}.closed_on < ?", @date_to + 1). - where("#{Issue.table_name}.created_on IS NOT NULL") - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/trackers_cumulative_flow_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/trackers_cumulative_flow_chart.rb deleted file mode 100644 index 2b26388..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/trackers_cumulative_flow_chart.rb +++ /dev/null @@ -1,56 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class TrackersCumulativeFlowChart < AgileChart - def initialize(data_scope, options={}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = options[:date_to] || Date.today - super data_scope, options - @line_colors = { 0 => '0,153,0', 1 => '80,122,170', 2 => '102,102,102', 3 => '255,204,0', 4 => '154,167,208', 5 => '224,112,235', - 6 => '235,17,142', 7 => '167,40,6', 8 => '108,97,58', 9 => '33,147,155', 10 => '43,237,59' } - end - - def data - datasets = [] - Tracker.where(:id => @data_scope.group("#{Issue.table_name}.tracker_id").count.keys).sorted.reverse.each_with_index do |tracker, index| - created_by_date = @data_scope.where(:tracker_id => tracker.id). - where("#{Issue.table_name}.created_on >= ?", @date_from). - where("#{Issue.table_name}.created_on <= ?", @date_to). - where("#{Issue.table_name}.created_on IS NOT NULL"). - group("#{Issue.table_name}.created_on"). - count - created_by_period = issues_count_by_period(created_by_date) - total_issues = @data_scope.where(:tracker_id => tracker.id). - where("#{Issue.table_name}.created_on < ?", @date_from).count - cumulative_created_by_period = created_by_period.map { |x| total_issues += x } - - datasets << dataset(cumulative_created_by_period, tracker.name, :color => line_colors[index], :fill => true, :nopoints => true) if created_by_period.any? - end - - { - :title => l(:label_agile_charts_cumulative_flow), - :y_title => l(:label_agile_charts_number_of_issues), - :stacked => true, - :labels => @fields, - :datasets => datasets - } - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/velocity_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/velocity_chart.rb deleted file mode 100644 index f21688b..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/velocity_chart.rb +++ /dev/null @@ -1,60 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class VelocityChart < AgileChart - def initialize(data_scope, options = {}) - @date_from = (options[:date_from] || data_scope.minimum("#{Issue.table_name}.created_on")).to_date - @date_to = options[:date_to] || Date.today - super data_scope, options - @line_colors = { :closed => '102,102,102', :created => '80,122,170' } - end - - def data - created_by_period = issues_avg_count_by_period(scope_by_created_date) - closed_by_period = issues_avg_count_by_period(scope_by_closed_date) - - if @scale_division > 1 - y_title = l(:label_agile_charts_avarate_number_of_issues) - graph_title = l(:label_agile_charts_average_velocity) - else - y_title = l(:label_agile_charts_number_of_issues) - graph_title = l(:label_agile_charts_issues_velocity) - end - - datasets = [] - if closed_by_period.any? - datasets << dataset(closed_by_period, l(:field_closed_on), :type => 'bar', :fill => true, :color => line_colors[:closed]) - datasets << dataset(trendline(closed_by_period), l(:field_closed_on_trendline), :nopoints => true, :dashed => true, :color => line_colors[:closed]) - end - - if created_by_period.any? - datasets << dataset(created_by_period, l(:field_created_on), :type => 'bar', :fill => true, :color => line_colors[:created]) - datasets << dataset(trendline(created_by_period), l(:field_created_on_trendline), :nopoints => true, :dashed => true, :color => line_colors[:created]) - end - - { - :title => graph_title, - :y_title => y_title, - :labels => @fields, - :datasets => datasets - } - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/work_burndown_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/work_burndown_chart.rb index 27408d4..0c8019e 100644 --- a/plugins/redmine_agile/lib/redmine_agile/charts/work_burndown_chart.rb +++ b/plugins/redmine_agile/lib/redmine_agile/charts/work_burndown_chart.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -29,7 +29,7 @@ module RedmineAgile @graph_title = l(:label_agile_charts_work_burndown_sp) end - @line_colors = { :work => '0,153,0', :ideal => '102,102,102', :total => '0,153,0' } + @line_colors = { work: '0,153,0', ideal: '102,102,102', total: '0,153,0' } end protected @@ -40,11 +40,11 @@ module RedmineAgile if @estimated_unit == 'hours' all_issues = data_scope.where("#{Issue.table_name}.estimated_hours IS NOT NULL"). - eager_load([:journals, :status, { :journals => { :details => :journal } }]) + eager_load([:journals, :status, { journals: { details: :journal } }]) cumulative_total_hours = data_scope.sum("#{Issue.table_name}.estimated_hours").to_f else all_issues = data_scope.where("#{AgileData.table_name}.story_points IS NOT NULL"). - joins(:agile_data).eager_load([:journals, :status, { :journals => { :details => :journal } }]) + joins(:agile_data).eager_load([:journals, :status, { journals: { details: :journal } }]) cumulative_total_hours = data_scope.joins(:agile_data).sum("#{AgileData.table_name}.story_points").to_f end @@ -53,7 +53,8 @@ module RedmineAgile total_hours_left, cumulative_total_hours_left = date_effort(issues, date) [total_hours_left, cumulative_total_hours - cumulative_total_hours_left] end - data = first_period_effort(all_issues, chart_dates_by_period.first, cumulative_total_hours) + data + tail_values = data.last ? [data.last] * (current_date_period - data.size) : [] + data = first_period_effort(all_issues, chart_dates_by_period.first, cumulative_total_hours) + data + tail_values @burndown_data, @cumulative_burndown_data = data.transpose end diff --git a/plugins/redmine_agile/lib/redmine_agile/charts/work_burnup_chart.rb b/plugins/redmine_agile/lib/redmine_agile/charts/work_burnup_chart.rb deleted file mode 100644 index b72262f..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/charts/work_burnup_chart.rb +++ /dev/null @@ -1,112 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - class WorkBurnupChart < BurnupChart - def initialize(data_scope, options = {}) - super data_scope, options - if @estimated_unit == 'hours' - @y_title = l(:label_agile_charts_number_of_hours) - @graph_title = l(:label_agile_charts_work_burnup_hours) - else - @y_title = l(:label_agile_charts_number_of_story_points) - @graph_title = l(:label_agile_charts_work_burnup_sp) - end - @line_colors = { :created => '102,102,102', :closed => '0,153,0', :ideal => '102,102,102' } - end - - protected - - def calculate_data - data_scope = @data_scope - data_scope = data_scope.where("#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1") if use_subissue_done_ratio && @estimated_unit == 'hours' - if @estimated_unit == 'hours' - all_issues = data_scope.where("#{Issue.table_name}.estimated_hours IS NOT NULL"). - eager_load([:journals, :status, { :journals => { :details => :journal } }]) - @cumulative_data = cumulative_hours_by_period(data_scope) - else - all_issues = data_scope.where("#{AgileData.table_name}.story_points IS NOT NULL"). - joins(:agile_data).eager_load([:journals, :status, { :journals => { :details => :journal } }]) - @cumulative_data = cumulative_story_points_by_period(data_scope) - end - - data = chart_dates_by_period.select { |d| d <= Date.today }.map do |date| - issues = all_issues.select do |issue| - issue.created_on.localtime.to_date <= date - end - cumulative_total_hours_left, total_hours_done = date_effort(issues, date)[1..2] - total_hours_done - end - @data = [first_period_effort(all_issues, chart_dates_by_period.first)[0][2]] + data - @cumulative_data = @cumulative_data.first(@data.count) if @cumulative_data.count > @data.count - @data - end - - private - - def cumulative_hours_by_period(data_scope) - data = [0] * chart_dates_by_period.count - data_scope. - where("#{Issue.table_name}.created_on >= ?", @date_from). - where("#{Issue.table_name}.created_on < ?", @date_to.to_date + 1). - where("#{Issue.table_name}.created_on IS NOT NULL"). - group("#{Issue.table_name}.created_on"). - sum(:estimated_hours).each do |c| - next if c.first.localtime.to_date > @date_to.to_date - period_num = ((@date_to.to_date - c.first.localtime.to_date).to_i / @scale_division).to_i - data[period_num] += c.last unless data[period_num].blank? - end - - total_estimated_hours = data_scope.where("#{Issue.table_name}.created_on < ?", @date_from).sum(:estimated_hours) - first_date_estimated_hours = data_scope.where("#{Issue.table_name}.created_on < ?", @date_from).sum(:estimated_hours) - ([first_date_estimated_hours] + data.reverse.map { |x| total_estimated_hours += x }) - end - - def cumulative_story_points_by_period(data_scope) - data = [0] * @period_count - data_scope. - where("#{Issue.table_name}.created_on >= ?", @date_from). - where("#{Issue.table_name}.created_on < ?", @date_to.to_date + 1). - where("#{Issue.table_name}.created_on IS NOT NULL"). - group("#{Issue.table_name}.created_on"). - joins(:agile_data). - sum("#{AgileData.table_name}.story_points").each do |c| - next if c.first.localtime.to_date > @date_to.to_date - period_num = ((@date_to.to_date - c.first.localtime.to_date).to_i / @scale_division).to_i - data[period_num] += c.last.to_i unless data[period_num].blank? - end - - total_estimated_hours = data_scope.where("#{Issue.table_name}.created_on < ?", @date_from). - joins(:agile_data). - sum("#{AgileData.table_name}.story_points").to_i - first_date_estimated_hours = data_scope.where("#{Issue.table_name}.created_on < ?", @date_from). - joins(:agile_data). - sum("#{AgileData.table_name}.story_points").to_i - [first_date_estimated_hours] + data.reverse.map { |x| total_estimated_hours += x } - end - - def first_period_effort(issues_scope, start_date) - issues = issues_scope.select do |issue| - issue.created_on.localtime.to_date <= start_date - end - total_left, cumulative_left, total_done = date_effort(issues, start_date - 1) - [[total_left, cumulative_left, total_done]] - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/helpers/agile_helper.rb b/plugins/redmine_agile/lib/redmine_agile/helpers/agile_helper.rb index 9b77fbd..cdc4be9 100644 --- a/plugins/redmine_agile/lib/redmine_agile/helpers/agile_helper.rb +++ b/plugins/redmine_agile/lib/redmine_agile/helpers/agile_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -51,76 +51,27 @@ module RedmineAgile session[:agile_query] = {:id => @query.id, :project_id => @query.project_id} sort_clear elsif api_request? || params[:set_filter] || session[:agile_query].nil? || session[:agile_query][:project_id] != (@project ? @project.id : nil) - @query = AgileQuery.default_query(@project) || AgileQuery.default_query unless params[:set_filter] unless @query @query = AgileQuery.new(:name => "_", :project => @project) @query.build_from_params(params) else @query.project = @project if @project end - save_qeury_attribures_to_session(@query) + save_query_attribures_to_session(@query) else # retrieve from session @query = nil if session[:agile_query] && !session[:agile_query][:id] && !params[:project_id] @query = AgileQuery.new(get_query_attributes_from_session) end - @query ||= AgileQuery.default_query(@project) || AgileQuery.default_query @query ||= AgileQuery.find_by_id(session[:agile_query][:id]) if session[:agile_query][:id] @query ||= AgileQuery.new(get_query_attributes_from_session) @query.project = @project - save_qeury_attribures_to_session(@query) + save_query_attribures_to_session(@query) end end - def retrieve_versions_query - if api_request? || params[:set_filter] || session[:versions_query].nil? || session[:versions_query][:project_id] != (@project ? @project.id : nil) - @query = AgileVersionsQuery.new(:name => "_") - @query.project = @project if @project - @query.build_from_params(params) - - session[:versions_query] = {:project_id => @query.project_id, - :filters => @query.filters} - else - @query = AgileVersionsQuery.new(:name => "_", :filters => session[:versions_query][:filters]) - @query.project = @project if @project - end - end - def agile_query_links(title, queries) - return '' if queries.empty? - # links to #index on issues/show - url_params = {:controller => 'agile_boards', :action => 'index', :project_id => @project} - - content_tag('h3', title) + "\n" + - content_tag('ul', - queries.collect {|query| - css = 'query' - css << ' selected' if query == @query - content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css)) - }.join("\n").html_safe, - :class => 'queries' - ) + "\n" - end - - - def sidebar_agile_queries - unless @sidebar_agile_queries - @sidebar_agile_queries = AgileQuery.visible. - order("#{Query.table_name}.name ASC"). - # Project specific queries and global queries - where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]). - all - end - @sidebar_agile_queries - end - - def render_sidebar_agile_queries - out = ''.html_safe - out << agile_query_links(l(:label_agile_my_boards), sidebar_agile_queries.select {|q| !q.is_public?}) - out << agile_query_links(l(:label_agile_board_plural), sidebar_agile_queries.reject {|q| !q.is_public?}) - out - end def options_card_colors_for_select(selected, options={}) color_base = [[l(:label_agile_color_no_colors), "none"], [l(:label_issue), "issue"], @@ -128,56 +79,74 @@ module RedmineAgile [l(:field_priority), "priority"], [l(:label_spent_time), "spent_time"], [l(:field_assigned_to), "user"]] - if (@project && @project.children.any?) || !@project - color_base << [l(:field_project), 'project'] + color_base << [l(:field_project), 'project'] if (@project && @project.children.any?) || !@project + options_for_select(color_base.compact, selected) + end + + def options_charts_for_select(selected) + container = [] + RedmineAgile::Charts::AGILE_CHARTS.each { |k, v| container << [l(v[:name]), k] } + selected_chart = RedmineAgile::Charts.chart_by_alias(selected) || selected + options_for_select(container, selected_chart) + end + + def grouped_options_charts_for_select(selected) + grouped_options = {} + container = [] + + RedmineAgile::Charts::AGILE_CHARTS.each do |chart, value| + if RedmineAgile::Charts::CHARTS_WITH_UNITS.include?(chart) + group = l(value[:name]) + grouped_options[group] = [] + value[:aliases].each do |alias_name| + grouped_options[group] << ["#{group} (#{l(RedmineAgile::Charts.chart_unit_label_by(alias_name))})", alias_name] + end + else + container << [l(value[:name]), chart] + end end - options_for_select(color_base.compact, - selected) + + grouped_options_for_select(grouped_options, selected) + options_for_select(container, selected) end - def options_charts_for_select(selected, options={}) - options_for_select([[l(:label_agile_charts_issues_burndown), "issues_burndown"], - [l(:label_agile_charts_work_burndown_sp), "work_burndown_sp"], - [l(:label_agile_charts_work_burndown_hours), "work_burndown_hours"], - [l(:label_agile_charts_burnup), "burnup"], - [l(:label_agile_charts_work_burnup_sp), "work_burnup_sp"], - [l(:label_agile_charts_work_burnup_hours), "work_burnup_hours"], - [l(:label_agile_charts_cumulative_flow), "cumulative_flow"], - [l(:label_agile_charts_issues_velocity), "issues_velocity"], - [l(:label_agile_charts_lead_time), "lead_time"], - [l(:label_agile_charts_average_lead_time), "average_lead_time"], - [l(:label_agile_charts_trackers_cumulative_flow), "trackers_cumulative_flow"], - nil].compact, - selected) + def options_chart_units_for_select(selected = nil) + container = [] + RedmineAgile::Charts::CHART_UNITS.each { |k, v| container << [l(v), k] } + selected_unit = RedmineAgile::Charts::CHART_UNIT_BY_ALIAS[selected] || selected + options_for_select(container, selected_unit) end def render_agile_chart(chart_name, issues_scope) - render :partial => "agile_charts/chart", :locals => {:chart => chart_name, :issues_scope => issues_scope} + render partial: "agile_charts/chart", + locals: { chart: chart_name, issues_scope: issues_scope, chart_unit: params[:chart_unit] } + end + + def upgrade_to_pro_agile_chart_link(chart_name, query = @query, current_chart = @chart) + link_to l("label_agile_charts_#{chart_name}"), '#', + onclick: "showModal('upgrade-to-pro', '557px');", + class: ('selected' if query.is_a?(AgileChartsQuery) && query.new_record? && current_chart == chart_name) end private def get_query_attributes_from_session - attributes = { - :name => "_", - :filters => session[:agile_query][:filters], - :group_by => session[:agile_query][:group_by], - :column_names => session[:agile_query][:column_names], - :color_base => session[:agile_query][:color_base] - } + attributes = { name: '_', + filters: session[:agile_query][:filters], + group_by: session[:agile_query][:group_by], + column_names: session[:agile_query][:column_names], + color_base: session[:agile_query][:color_base] } (attributes[:options] = session[:agile_query][:options] || {}) if Redmine::VERSION.to_s > '2.4' attributes end - def save_qeury_attribures_to_session(query) - session[:agile_query] = {:project_id => query.project_id, - :filters => query.filters, - :group_by => query.group_by, - :color_base => (query.respond_to?(:color_base) && query.color_base), - :column_names => query.column_names} + def save_query_attribures_to_session(query) + session[:agile_query] = { project_id: query.project_id, + filters: query.filters, + group_by: query.group_by, + color_base: (query.respond_to?(:color_base) && query.color_base), + column_names: query.column_names } (session[:agile_query][:options] = query.options) if Redmine::VERSION.to_s > '2.4' end - end end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/controller_issue_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/controller_issue_hook.rb index 1905c47..06cc964 100644 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/controller_issue_hook.rb +++ b/plugins/redmine_agile/lib/redmine_agile/hooks/controller_issue_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -22,18 +22,19 @@ module RedmineAgile class ControllerIssueHook < Redmine::Hook::ViewListener def controller_issues_edit_before_save(context={}) + add_agile_journal_details(context) + end + + def controller_issues_bulk_edit_before_save(context={}) + add_agile_journal_details(context) + end + + private + + def add_agile_journal_details(context) return false unless context[:issue].project.module_enabled?(:agile) # return false unless context[:issue].color - old_value = Issue.find(context[:issue].id) - old_issue_color = old_value.color.to_s - new_issue_color = context[:issue].color.to_s - - if new_issue_color && !((new_issue_color == old_issue_color) || context[:issue].current_journal.blank?) - context[:issue].current_journal.details << JournalDetail.new(:property => 'attr', - :prop_key => 'color', - :old_value => old_issue_color, - :value => new_issue_color) - end + old_value = Issue.where(id: context[:issue].id).first || context[:issue] # save changes for story points to journal old_sp = old_value.story_points new_sp = context[:issue].story_points diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/helper_issues_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/helper_issues_hook.rb deleted file mode 100644 index 6057ba7..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/helper_issues_hook.rb +++ /dev/null @@ -1,34 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Hooks - class HelperIssuesHook < Redmine::Hook::ViewListener - - def helper_issues_show_detail_after_setting(context={}) - if context[:detail].prop_key == 'color' - detail = context[:detail] - context[:detail].value = detail.value.blank? ? nil : l(("label_agile_color_" + detail.value.to_s).to_sym) - context[:detail].old_value = detail.old_value.blank? ? nil : l(("label_agile_color_" + detail.old_value.to_s).to_sym) - end - end - - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_context_menus_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_context_menus_hook.rb deleted file mode 100644 index b701bd4..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_context_menus_hook.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Hooks - class ViewsContextMenuesHook < Redmine::Hook::ViewListener - render_on :view_issues_context_menu_end, :partial => "context_menus/agile_colors" - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_issues_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_issues_hook.rb old mode 100755 new mode 100644 index dab96ad..d4d7ab2 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_issues_hook.rb +++ b/plugins/redmine_agile/lib/redmine_agile/hooks/views_issues_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -20,16 +20,12 @@ module RedmineAgile module Hooks class ViewsIssuesHook < Redmine::Hook::ViewListener - def view_issues_sidebar_issues_bottom(context={}) - context[:controller].send(:render_to_string, { - :partial => 'agile_boards/issues_sidebar', - :locals => context }) + - context[:controller].send(:render_to_string, { - :partial => "agile_charts/agile_charts", - :locals => context }) + def view_issues_sidebar_issues_bottom(context = {}) + context[:controller].send(:render_to_string, partial: 'agile_charts/agile_charts', locals: context) end - render_on :view_issues_form_details_bottom, :partial => "issues/agile_data_fields" - render_on :view_issues_show_details_bottom, :partial => "issues/issue_story_points" + + render_on :view_issues_form_details_bottom, :partial => 'issues/agile_data_fields' + render_on :view_issues_show_details_bottom, :partial => 'issues/agile_data_labels' end end end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_layouts_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_layouts_hook.rb index 0ac141b..ef723bc 100644 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_layouts_hook.rb +++ b/plugins/redmine_agile/lib/redmine_agile/hooks/views_layouts_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -21,7 +21,7 @@ module RedmineAgile module Hooks class ViewsLayoutsHook < Redmine::Hook::ViewListener def view_layouts_base_html_head(context={}) - return stylesheet_link_tag(:redmine_agile, :plugin => 'redmine_agile') + return stylesheet_link_tag(:redmine_agile, :plugin => 'redmine_agile') end end end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_projects_form_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_projects_form_hook.rb deleted file mode 100644 index 94864ce..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_projects_form_hook.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Hooks - class ViewsProjectsForm < Redmine::Hook::ViewListener - render_on :view_projects_form, :partial => "projects/project_color_form" - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_users_form_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_users_form_hook.rb deleted file mode 100644 index 42d228c..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_users_form_hook.rb +++ /dev/null @@ -1,27 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Hooks - class ViewsUsersHook < Redmine::Hook::ViewListener - preferences_hook = Redmine::VERSION.to_s > '2.5' ? :view_users_form_preferences : :view_users_form - render_on preferences_hook, :partial => 'users/user_color_form' - end - end -end diff --git a/plugins/redmine_agile/lib/redmine_agile/hooks/views_versions_hook.rb b/plugins/redmine_agile/lib/redmine_agile/hooks/views_versions_hook.rb index deae5de..67af1e1 100644 --- a/plugins/redmine_agile/lib/redmine_agile/hooks/views_versions_hook.rb +++ b/plugins/redmine_agile/lib/redmine_agile/hooks/views_versions_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/compatibility/application_controller_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/compatibility/application_controller_patch.rb deleted file mode 100644 index 633e1a4..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/compatibility/application_controller_patch.rb +++ /dev/null @@ -1,41 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Patches - module ApplicationControllerPatch - def self.included(base) # :nodoc: - base.extend(ClassMethods) - base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development - end - end - - module ClassMethods - def before_action(*filters, &block) - before_filter(*filters, &block) - end - end - end - end -end - -unless ApplicationController.included_modules.include?(RedmineAgile::Patches::ApplicationControllerPatch) - ApplicationController.send(:include, RedmineAgile::Patches::ApplicationControllerPatch) -end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/compatibility_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/compatibility_patch.rb deleted file mode 100644 index e4b67d3..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/compatibility_patch.rb +++ /dev/null @@ -1,351 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -class AgileQuery < Query - unloadable - - VISIBILITY_PRIVATE = 0 - VISIBILITY_ROLES = 1 - VISIBILITY_PUBLIC = 2 - - attr_reader :truncated - - self.queried_class = Issue - - self.available_columns = [ - QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => :label_agile_issue_id), - QueryColumn.new(:project, :groupable => "#{Issue.table_name}.project_id"), - QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), - QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), - QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), - QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), - QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("users")}, :groupable => true), - QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => "#{Issue.table_name}.category_id"), - QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), - QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), - QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), - QueryColumn.new(:thumbnails, :caption => :label_agile_board_thumbnails), - QueryColumn.new(:description), - QueryColumn.new(:parent, :groupable => "#{Issue.table_name}.parent_id", :caption => :field_parent_issue), - QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => "#{Issue.table_name}.assigned_to_id") - ] - - scope :visible, lambda {|*args| - user = args.shift || User.current - base = Project.allowed_to_condition(user, :view_issues, *args) - user_id = user.logged? ? user.id : 0 - - eager_load(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id) - } - - def initialize(attributes=nil, *args) - attributes.delete(:color_base) if attributes - super attributes - self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } - @truncated = false - end - - def card_columns - self.inline_columns.select{|c| !%w(tracker thumbnails description assigned_to done_ratio spent_hours estimated_hours project id day_in_state last_comment story_points).include?(c.name.to_s)} - end - - def visible?(user=User.current) - (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) - end - - def is_private? - visibility == VISIBILITY_PRIVATE - end - - def is_public? - !is_private? - end - - def self.default_query(project=nil) - false - end - - def visibility=(value) - self.is_public = value == VISIBILITY_PUBLIC - end - - def visibility - self.is_public ? VISIBILITY_PUBLIC : VISIBILITY_PRIVATE - end - - def build_from_params(params) - if params[:fields] || params[:f] - self.filters = {} - add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) - else - available_filters.keys.each do |field| - add_short_filter(field, params[field]) if params[field] - end - end - self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) - self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) - self - end - - # Builds a new query from the given params and attributes - def self.build_from_params(params, attributes={}) - new(attributes).build_from_params(params) - end - - def initialize_available_filters - principals = [] - subprojects = [] - versions = [] - categories = [] - issue_custom_fields = [] - - if project - principals += project.principals.sort - unless project.leaf? - subprojects = project.descendants.visible.all - principals += Principal.member_of(subprojects) - end - versions = project.shared_versions.all - categories = project.issue_categories.all - issue_custom_fields = project.all_issue_custom_fields - else - if all_projects.any? - principals += Principal.member_of(all_projects) - end - versions = Version.visible.where(:sharing => 'system').all - issue_custom_fields = IssueCustomField.where(:is_for_all => true) - end - principals.uniq! - principals.sort! - users = principals.select {|p| p.is_a?(User)} - - add_available_filter "status_id", - :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } - - if project.nil? - project_values = [] - if User.current.logged? && User.current.memberships.any? - project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] - end - project_values += all_projects_values - add_available_filter("project_id", - :type => :list, :values => project_values - ) unless project_values.empty? - end - - add_available_filter "tracker_id", - :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } - add_available_filter "priority_id", - :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } - - author_values = [] - author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - author_values += users.collect{|s| [s.name, s.id.to_s] } - add_available_filter("author_id", - :type => :list, :values => author_values - ) unless author_values.empty? - - assigned_to_values = [] - assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - assigned_to_values += (Setting.issue_group_assignment? ? - principals : users).collect{|s| [s.name, s.id.to_s] } - add_available_filter("assigned_to_id", - :type => :list_optional, :values => assigned_to_values - ) unless assigned_to_values.empty? - - if versions.any? - add_available_filter "fixed_version_id", - :type => :list_optional, - :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } - end - - if categories.any? - add_available_filter "category_id", - :type => :list_optional, - :values => categories.collect{|s| [s.name, s.id.to_s] } - end - - add_available_filter "subject", :type => :text - add_available_filter "created_on", :type => :date_past - add_available_filter "updated_on", :type => :date_past - add_available_filter "closed_on", :type => :date_past - add_available_filter "start_date", :type => :date - add_available_filter "due_date", :type => :date - add_available_filter "estimated_hours", :type => :float - add_available_filter "done_ratio", :type => :integer - - if subprojects.any? - add_available_filter "subproject_id", - :type => :list_subprojects, - :values => subprojects.collect{|s| [s.name, s.id.to_s] } - end - - add_custom_fields_filters(issue_custom_fields) - - add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version - - Tracker.disabled_core_fields(trackers).each {|field| - delete_available_filter field - } - end - - def available_columns - return @available_columns if @available_columns - @available_columns = self.class.available_columns.dup - @available_columns += (project ? - project.all_issue_custom_fields : - IssueCustomField.all - ).collect {|cf| QueryCustomFieldColumn.new(cf) } - - if User.current.allowed_to?(:view_time_entries, project, :global => true) - index = nil - @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} - index = (index ? index + 1 : -1) - # insert the column after estimated_hours or at the end - @available_columns.insert index, QueryColumn.new(:spent_hours, - :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)", - :default_order => 'desc', - :caption => :label_spent_time - ) - end - - disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} - @available_columns.reject! {|column| - disabled_fields.include?(column.name.to_s) - } - - @available_columns.reject! {|column| column.name == :done_ratio} unless Issue.use_field_for_done_ratio? - - @available_columns - end - - def editable_by?(user) - return false unless user - # Admin can edit them all and regular users can edit their private queries - return true if user.admin? || (is_private? && self.user_id == user.id) - # Members can not edit public queries that are for all project (only admin is allowed to) - is_public? && !@is_for_all && user.allowed_to?(:manage_public_agile_queries, project, global: true) - end - - def default_columns_names - @default_columns_names = RedmineAgile.default_columns.map(&:to_sym) - end - - def has_column_name?(name) - columns.detect{|c| c.name == name} - end - - def groupable_columns - available_columns.select {|c| c.groupable && !c.is_a?(QueryCustomFieldColumn)} - end - - def issues(options={}) - order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) - - scope = issue_scope. - joins(:status, :project). - includes((options[:include] || []).uniq). - where(options[:conditions]). - order(order_option). - joins(joins_for_order_statement(order_option.join(','))). - limit(options[:limit]). - offset(options[:offset]) - - scope = scope.preload(:custom_values) - if has_column?(:author) - scope = scope.preload(:author) - end - - # if has_column?(:spent_hours) - # Issue.load_visible_spent_hours(issues) - # end - - scope - rescue ::ActiveRecord::StatementInvalid => e - raise StatementInvalid.new(e.message) - end - - # for compatibility with upper versions - def issue_last_comment(issue_id, options = {}) - nil - end - - def journals_for_state - nil - end - - def board_statuses - status_filter_operator = filters.fetch("status_id", {}).fetch(:operator, nil) - status_filter_values = filters.fetch("status_id", {}).fetch(:values, []) - statuses = IssueStatus.where(:id => Tracker.includes(:issues => [:status, :project, :fixed_version]).where(statement).map(&:issue_statuses).flatten.uniq.map(&:id)) - result_statuses = case status_filter_operator - when "o" - statuses.where(:is_closed => false).sorted - when "c" - statuses.where(:is_closed => true).sorted - when "=" - statuses.where(:id => status_filter_values).sorted - when "!" - statuses.where("#{IssueStatus.table_name}.id NOT IN (" + status_filter_values.map{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")").sorted - else - statuses.sorted - end - result_statuses.map do |s| - s.instance_variable_set "@issue_count", self.issue_count_by_status[s.id] - s - end - end - def swimlanes - return [] unless self.grouped? - lane_ids = issue_scope.group(self.group_by_column.groupable).count.keys - lanes = Issue.reflect_on_association(self.group_by_column.name).klass.where(:id => lane_ids).reorder(group_by_sort_order) - lanes << nil if lane_ids.include?(nil) - lanes - end - - def issue_count_by_swimlane - @issue_count_by_swimlane ||= issue_scope.group("#{Issue.table_name}.#{self.group_by_column.name}_id").count if self.grouped? - end - - def issue_count_by_status - @issue_count_by_status ||= issue_scope.group("#{Issue.table_name}.status_id").count - end - - def issue_board(options={}) - @truncated = RedmineAgile.board_items_limit <= issue_scope.count - all_issues = self.issues.limit(RedmineAgile.board_items_limit).sorted_by_rank - grouped_issues = grouped? ? all_issues.group_by{|i| [i.status_id, i.send("#{self.group_by_column.name}_id")]} : all_issues.group_by{|i| [i.status_id]} - grouped_issues.values.each{|x|x.sort!{|a,b| a.position.to_i <=> b.position.to_i}} if grouped? - grouped_issues - end - -private - def issue_scope - Issue.visible. - includes(:status, - :project, - :assigned_to, - :tracker, - :priority, - :category, - :fixed_version). - where(statement) - end - -end if Redmine::VERSION.to_s < '2.4' diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/issue_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/issue_patch.rb old mode 100755 new mode 100644 index 3161f86..8caca54 --- a/plugins/redmine_agile/lib/redmine_agile/patches/issue_patch.rb +++ b/plugins/redmine_agile/lib/redmine_agile/patches/issue_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -31,15 +31,8 @@ module RedmineAgile has_one :agile_data, :dependent => :destroy delegate :position, :to => :agile_data, :allow_nil => true scope :sorted_by_rank, lambda { eager_load(:agile_data). - order("COALESCE(#{AgileData.table_name}.position, 999999)") } - alias_method :css_classes_without_agile, :css_classes - alias_method :css_classes, :css_classes_with_agile - - acts_as_colored - - safe_attributes 'agile_color_attributes', - :if => lambda { |issue, user| user.allowed_to?(:edit_issues, issue.project) && user.allowed_to?(:view_agile_queries, issue.project) && RedmineAgile.issue_colors? } - safe_attributes 'agile_data_attributes', :if => lambda { |issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) && RedmineAgile.use_story_points? } + order(Arel.sql("COALESCE(#{AgileData.table_name}.position, 999999 )")) } + safe_attributes 'agile_data_attributes', :if => lambda { |issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } accepts_nested_attributes_for :agile_data, :allow_destroy => true alias_method :agile_data_without_default, :agile_data @@ -67,15 +60,6 @@ module RedmineAgile def story_points @story_points ||= agile_data.story_points end - def css_classes_with_agile(user = User.current) - s = if Redmine::VERSION.to_s < '2.4' - css_classes_without_agile - else - css_classes_without_agile(user) - end - s << " #{RedmineAgile.color_prefix}-#{color}" if RedmineAgile.issue_colors? && color - s - end def sub_issues descendants diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/issue_priority_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/issue_priority_patch.rb deleted file mode 100644 index 5ffcc76..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/issue_priority_patch.rb +++ /dev/null @@ -1,36 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Patches - - module IssuePriorityPatch - def self.included(base) - base.class_eval do - acts_as_colored - end - end - end - - end -end - -unless IssuePriority.included_modules.include?(RedmineAgile::Patches::IssuePriorityPatch) - IssuePriority.send(:include, RedmineAgile::Patches::IssuePriorityPatch) -end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/issue_query_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/issue_query_patch.rb deleted file mode 100644 index 6fe8c90..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/issue_query_patch.rb +++ /dev/null @@ -1,62 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'issue' - -module RedmineAgile - module Patches - - module IssueQueryPatch - def self.included(base) - base.send(:include, InstanceMethods) - base.class_eval do - alias_method :issues_without_agile, :issues - alias_method :issues, :issues_with_agile - alias_method :issue_ids_without_agile, :issue_ids - alias_method :issue_ids, :issue_ids_with_agile - - available_columns << QueryColumn.new(:story_points, :caption => :label_agile_story_points, :sortable => "#{AgileData.table_name}.story_points") - end - end - - module InstanceMethods - def issues_with_agile(options = {}) - options[:include] = (options[:include] || []) | [:agile_data] - issues = issues_without_agile(options) - if RedmineAgile.color_base == AgileColor::COLOR_GROUPS[:issue] - agile_colors = AgileColor.where(container_id: issues, container_type: 'Issue').group_by { |ac| ac[:container_id] } - issues.each { |issue| issue.color = agile_colors[issue.id].try(:first).try(:color) } - end - issues - end - - def issue_ids_with_agile(options = {}) - options[:include] = (options[:include] || []) | [:agile_data] - options[:include] = (options[:include] || []) | [:agile_color] if RedmineAgile.color_base == AgileColor::COLOR_GROUPS[:issue] - issue_ids_without_agile(options) - end - end - end - - end -end - -unless IssueQuery.included_modules.include?(RedmineAgile::Patches::IssueQueryPatch) - IssueQuery.send(:include, RedmineAgile::Patches::IssueQueryPatch) -end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/project_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/project_patch.rb index d03c328..2f4a628 100644 --- a/plugins/redmine_agile/lib/redmine_agile/patches/project_patch.rb +++ b/plugins/redmine_agile/lib/redmine_agile/patches/project_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -19,16 +19,18 @@ module RedmineAgile module Patches - module ProjectPatch + def self.included(base) base.class_eval do - unloadable - acts_as_colored + base.send(:include, InstanceMethods) safe_attributes 'agile_color_attributes', - :if => lambda { |project, user| user.allowed_to?(:edit_project, project) && user.allowed_to?(:view_agile_queries, project) && RedmineAgile.use_colors? } + if: lambda { |project, user| user.allowed_to?(:edit_project, project) && user.allowed_to?(:view_agile_queries, project) && RedmineAgile.use_colors? } end end + + module InstanceMethods + end end end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/queries_controller_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/queries_controller_patch.rb index d60e504..085b7d6 100644 --- a/plugins/redmine_agile/lib/redmine_agile/patches/queries_controller_patch.rb +++ b/plugins/redmine_agile/lib/redmine_agile/patches/queries_controller_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -31,6 +31,7 @@ module RedmineAgile module InstanceMethods def query_class_with_agile return AgileChartsQuery if params[:type] == 'AgileChartsQuery' + return AgileVersionsQuery if params[:type] == 'AgileVersionsQuery' query_class_without_agile end end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/tracker_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/tracker_patch.rb deleted file mode 100644 index 6e32677..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/tracker_patch.rb +++ /dev/null @@ -1,36 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Patches - - module TrackerPatch - def self.included(base) - base.class_eval do - acts_as_colored - end - end - end - - end -end - -unless Tracker.included_modules.include?(RedmineAgile::Patches::TrackerPatch) - Tracker.send(:include, RedmineAgile::Patches::TrackerPatch) -end diff --git a/plugins/redmine_agile/lib/redmine_agile/patches/user_patch.rb b/plugins/redmine_agile/lib/redmine_agile/patches/user_patch.rb deleted file mode 100644 index 8422507..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/patches/user_patch.rb +++ /dev/null @@ -1,39 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module Patches - - module UserPatch - def self.included(base) - base.class_eval do - unloadable - acts_as_colored - safe_attributes 'agile_color_attributes', - :if => lambda { |user, current_user| (current_user.admin? || (user.new_record? && current_user.anonymous? && Setting.self_registration?)) && RedmineAgile.use_colors? } - end - end - end - - end -end - -unless User.included_modules.include?(RedmineAgile::Patches::UserPatch) - User.send(:include, RedmineAgile::Patches::UserPatch) -end diff --git a/plugins/redmine_agile/lib/redmine_agile/utils/header_tree.rb b/plugins/redmine_agile/lib/redmine_agile/utils/header_tree.rb deleted file mode 100644 index eee599e..0000000 --- a/plugins/redmine_agile/lib/redmine_agile/utils/header_tree.rb +++ /dev/null @@ -1,118 +0,0 @@ -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -module RedmineAgile - module HeaderTree - class HeaderTree - class HeaderTreeNode - attr_accessor :leaf - - def initialize(name = nil, level = 0) - @name = name - @level = level - @children = ActiveSupport::OrderedHash.new - end - - def has_child?(name, has_leaf) - @children.keys.last == [name, has_leaf, @children.keys.size - 1] - end - - def get_child(name, has_leaf) - k = @children.keys.select{ |x| x.first(2) == [name, has_leaf] }.last - @children[k] - end - - def add_child(name, has_leaf) - @children[[name, has_leaf, @children.keys.size]] = HeaderTreeNode.new(name, @level + 1) - end - - def branch_width - if @leaf - 1 - else - @children.values.map(&:branch_width).sum - end - end - - def depth - if @leaf - 1 - else - 1 + @children.values.map(&:depth).max - end - end - - def render(levels) - height = @children.values.any? ? 1 : levels.size - @level - levels[@level] ||= [] - levels[@level] << [@name, height, branch_width, @leaf] - @children.values.each do |child| - child.render(levels) - end - levels - end - - def to_s - "#{"\t" * @level}#{@name || 'ROOT'} depth: #{depth}, width: #{branch_width}, level: #{@level}, leaf: #{!!@leaf}\n" + - @children.values.map(&:to_s).join - end - end - - def initialize - @root = HeaderTreeNode.new - end - - def put(path, leaf, node = nil) - node_to_put = node || @root - child_node_name = path.first - path_left = path[1..-1] - has_leaf = path_left.empty? - child = - if node_to_put.has_child? child_node_name, has_leaf - node_to_put.get_child(child_node_name, has_leaf) - else - node_to_put.add_child(child_node_name, has_leaf) - end - if has_leaf - child.leaf = leaf - else - put path_left, leaf, child - end - end - - def depth - @root.depth - 1 # Because root itself is not treat as node - end - - def render - maxdepth = depth - levels = Array.new maxdepth + 1 - @root.render(levels) - end - - def to_s - @root.to_s - end - end - end -end - -unless AgileBoardsHelper.included_modules.include?(RedmineAgile::HeaderTree) - AgileBoardsHelper.send(:include, RedmineAgile::HeaderTree) -end diff --git a/plugins/redmine_agile/test/functional/agile_board_controller_test.rb b/plugins/redmine_agile/test/functional/agile_board_controller_test.rb deleted file mode 100755 index 094e635..0000000 --- a/plugins/redmine_agile/test/functional/agile_board_controller_test.rb +++ /dev/null @@ -1,963 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -module RedmineAgile - module AgileBoardsControllerTest - module SpecificTestCase - def test_get_index_with_filter_issue_id - compatible_request :get, :index, { - project_id: 'ecookbook', - set_filter: '1', - f: %w(issue_id), - op: { 'issue_id' => '<=' }, - v: { 'issue_id' => ['2'] }, - c: %w(tracker assigned_to) - } - - assert_response :success - assert_equal [1, 2], agile_issues_in_list.map(&:id).sort - end - - def test_get_index_with_filter_is_private - compatible_request :get, :index, { - project_id: 'ecookbook', - set_filter: '1', - f: %w(is_private), - op: { 'is_private' => '=' }, - v: { 'is_private' => ['1'] }, - c: %w(tracker assigned_to) - } - - assert_response :success - assert_equal [14], agile_issues_in_list.map(&:id) - end - def test_get_index_with_filter_description - compatible_request :get, :index, { - project_id: 'ecookbook', - set_filter: '1', - f: %w(description), - op: { 'description' => '=' }, - v: { 'description' => ['Unable to print recipes'] }, - c: %w(tracker assigned_to) - } - - assert_response :success - assert_equal [1], agile_issues_in_list.map(&:id) - end - - def test_get_index_with_filter_watcher_id - issues(:issues_001).set_watcher(users(:users_002)) - - compatible_request :get, :index, { - project_id: 'ecookbook', - set_filter: '1', - f: %w(watcher_id), - op: { 'watcher_id' => '=' }, - v: { 'watcher_id' => ['2'] }, - c: %w(tracker assigned_to) - } - - assert_response :success - assert_equal [1], agile_issues_in_list.map(&:id) - end - end - end -end - - -class AgileBoardsControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :issue_relations, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - fixtures :email_addresses if Redmine::VERSION.to_s > '3.0' - - include(RedmineAgile::AgileBoardsControllerTest::SpecificTestCase) if Redmine::VERSION.to_s > '2.4' - - def setup - @project_1 = Project.find(1) - @project_2 = Project.find(5) - EnabledModule.create(:project => @project_1, :name => 'agile') - EnabledModule.create(:project => @project_2, :name => 'agile') - @request.session[:user_id] = 1 - end - - def test_get_index - # global board - compatible_request :get, :index - assert_response :success - assert_equal Issue.open.map(&:id).sort, agile_issues_in_list.map(&:id).sort - assert_select '.issue-card', Issue.open.count - end - - def test_get_index_with_project - compatible_request :get, :index, :project_id => @project_1 - issues = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a, - :status_id => IssueStatus.where(:is_closed => false)) - assert_equal issues.map(&:id).sort, agile_issues_in_list.map(&:id).sort - assert_select '.issue-card', issues.count - assert_select '.issue-card span.fields p.issue-id strong', issues.count - assert_select '.issue-card span.fields p.name a', issues.count - end - - def test_get_index_truncated - with_agile_settings 'board_items_limit' => 1 do - compatible_request :get, :index, agile_query_params - assert_response :success - assert_select 'div#content p.warning', 1 - assert_select 'td.issue-status-col .issue-card', 1 - end - end - - def test_limit_for_truncated - expected_issues = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a, - :status_id => IssueStatus.where(:is_closed => false)) - with_agile_settings 'board_items_limit' => (expected_issues.count + 1) do - compatible_request :get, :index, agile_query_params.merge(:f_status => IssueStatus.where(:is_closed => false).pluck(:id)) - assert_response :success - assert_select 'div#content p.warning', 0 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_filters - if Redmine::VERSION.to_s > '2.4' - compatible_request :get, :index, agile_query_params.merge(:f_status => IssueStatus.where('id != 1').pluck(:id)) - else - compatible_request :get, :index, agile_query_params.merge(:op => { :status_id => '!' }, :v => { :status_id => ['1'] }) - end - assert_response :success - expected_issues = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a, - :status_id => IssueStatus.where('id != 1')) - assert_equal expected_issues.map(&:id).sort, agile_issues_in_list.map(&:id).sort - end - - def test_get_index_with_filter_on_assignee_role - assignee_params = { :set_filter => '1', - :f => ['assigned_to_role', ''], - :op => { :assigned_to_role => '=' }, - :v => { 'assigned_to_role' => ['2'] }, - :c => ['assigned_to'], - :project_id => 'ecookbook' } - compatible_request :get, :index, assignee_params - assert_response :success - members = Member.joins(:roles).where(:project_id => [@project_1]).where('member_roles.role_id = 2').map(&:user_id).uniq - expected_issues = Issue.where(:project_id => [@project_1]).where(:assigned_to_id => members) - assert_equal expected_issues.map(&:id).sort, agile_issues_in_list.map(&:id).sort - end if Redmine::VERSION.to_s > '2.4' - - def create_subissue - @issue1 = Issue.find(1) - @subissue = Issue.create!( - :subject => 'Sub issue', - :project => @issue1.project, - :tracker => @issue1.tracker, - :author => @issue1.author, - :parent_issue_id => @issue1.id, - :fixed_version => Version.last - ) - end - - def test_get_index_with_filter_on_parent_tracker - create_subissue - compatible_request :get, :index, agile_query_params.merge( - :op => { :parent_issue_tracker_id => '=' }, - :v => { :parent_issue_tracker_id => [Tracker.find(1).name] }, - :f => [:parent_issue_tracker_id], - :project_id => Project.order(:id).first.id - ) - assert_response :success - assert_equal [@subissue.id], agile_issues_in_list.map(&:id) - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_filter_on_two_parent_id - create_subissue - issue2 = Issue.generate! - child2 = Issue.generate!(:parent_issue_id => issue2.id) - - compatible_request :get, :index, agile_query_params.merge( - :op => { :parent_issue_id => '=' }, - :v => { :parent_issue_id => ["#{@issue1.id}, #{issue2.id}"] }, - :f => [:parent_issue_id], - :project_id => Project.order(:id).first.id - ) - assert_response :success - assert_equal [@subissue.id, child2.id].sort, agile_issues_in_list.map(&:id).sort - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_filter_on_parent_tracker_inversed - create_subissue - compatible_request :get, :index, agile_query_params.merge( - :op => { :parent_issue_tracker_id => '!' }, - :v => { :parent_issue_tracker_id => [Tracker.find(1).name] }, - :f => [:parent_issue_tracker_id], - :project_id => Project.order(:id).first.id - ) - assert_response :success - assert_not_include @subissue.id, agile_issues_in_list.map(&:id) - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_filter_on_has_subissues - create_subissue - compatible_request :get, :index, agile_query_params.merge( - :op => { :has_sub_issues => '=' }, - :v => { :has_sub_issues => ['yes'] }, - :f => [:has_sub_issues], - :project_id => Project.order(:id).first.id - ) - assert_response :success - assert_equal [@issue1.id], agile_issues_in_list.map(&:id) - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_filter_and_field_time_in_state - create_subissue - columns_group_by = AgileQuery.new.groupable_columns - columns_group_by.each do |col| - compatible_request :get, :index, agile_query_params.merge( - :project_id => Project.order(:id).first.id, - :group_by => col.name.to_s - ) - assert_response :success, "Error with group by #{col.name}" - end - end if Redmine::VERSION.to_s > '2.4' - - def test_put_update_status - status_id = 1 - first_issue_id = 1 - second_issue_id = 3 - first_pos = 1 - second_pos = 2 - positions = { first_issue_id.to_s => { 'position' => first_pos }, second_issue_id.to_s => { 'position' => second_pos } } - compatible_xhr_request :put, :update, :id => first_issue_id, :issue => { :status_id => status_id }, :positions => positions - assert_response :success - assert_equal status_id, Issue.find(first_issue_id).status_id - assert_equal first_pos, Issue.find(first_issue_id).agile_data.position - assert_equal second_pos, Issue.find(second_issue_id).agile_data.position - # check js code for update board header - assert_match '$("table.issues-board thead").html(', @response.body - end - - def test_put_update_version - fixed_version_id = 3 - first_issue_id = 1 - second_issue_id = 3 - first_pos = 1 - second_pos = 2 - positions = { first_issue_id.to_s => { 'position' => first_pos }, second_issue_id.to_s => { 'position' => second_pos } } - compatible_xhr_request :put, :update, :id => first_issue_id, :issue => { :fixed_version_id => fixed_version_id }, :positions => positions - assert_response :success - assert_equal fixed_version_id, Issue.find(first_issue_id).fixed_version_id - assert_equal first_pos, Issue.find(first_issue_id).agile_data.position - assert_equal second_pos, Issue.find(second_issue_id).agile_data.position - end - - def test_put_update_assigned - assigned_to_id = 3 - issue_id = 1 - compatible_xhr_request :put, :update, :id => issue_id, :issue => { :assigned_to_id => assigned_to_id } - assert_response :success - assert_equal assigned_to_id, Issue.find(issue_id).assigned_to_id - end - - def test_get_index_with_all_fields - compatible_request :get, :index, agile_query_params.merge(:f => AgileQuery.available_columns.map(&:name)) - assert_response :success - end - def test_get_index_with_colors - with_agile_settings 'color_on' => 'issue' do - issue = Issue.find(1) - issue.agile_color.color = 'red' - issue.save - compatible_request :get, :index, agile_query_params.merge(:color_base => 'issue') - assert_response :success - assert_select 'td.issue-status-col .issue-card.bk-red', 1 - end - end - - def test_get_index_with_colors_for_spent_time - with_agile_settings 'color_on' => 'spent_time' do - issue = Issue.find(1) - est_time = issue.estimated_hours - spent_time = issue.spent_hours - compatible_request :get, :index, agile_query_params.merge(:color_base => 'spent_time') - assert_response :success - assert_select "td.issue-status-col .issue-card.bk-#{AgileColor.for_spent_time(est_time, spent_time)}" - end - end - - def test_get_index_with_colors_for_project - with_agile_settings 'color_on' => 'project' do - @project_1.color = AgileColor::AGILE_COLORS[:red] - @project_1.save - compatible_request :get, :index, :color_base => 'project' - assert_response :success - assert_select 'td.issue-status-col .issue-card.bk-red', @project_1.issues.open.count - end - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_colors_for_user - with_settings :gravatar_enabled => '1' do - with_agile_settings 'color_on' => 'user' do - issue = Issue.find(2) - user = issue.assigned_to - user.color = AgileColor::AGILE_COLORS[:red] - user.save - user_color = user.reload.color - compatible_request :get, :index, agile_query_params.merge(:color_base => 'user') - assert_response :success - assert_select 'td.issue-status-col .issue-card[data-id="2"]' - assert_select "img.gravatar[style='#{"border-left: 5px solid #{user_color}".html_safe}']" - assert response.body.include? user_color - end - end - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_colors_for_user_with_allow_issue_assignment_to_groups_settings - with_settings :gravatar_enabled => '1', :issue_group_assignment => '1' do - with_agile_settings 'color_on' => 'user' do - issue = Issue.find(2) - user = issue.assigned_to - user.color = AgileColor::AGILE_COLORS[:red] - user.save - user_color = user.reload.color - compatible_request :get, :index, agile_query_params.merge(:color_base => 'user') - assert_response :success - assert_select 'td.issue-status-col .issue-card[data-id="2"]' - assert_select "img.gravatar[style='#{"border-left: 5px solid #{user_color}".html_safe}']" - assert response.body.include? user_color - end - end - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_colors_by_user - with_settings :gravatar_enabled => '1' do - with_agile_settings 'color_on' => 'user' do - issue = Issue.find(2) - user_color = AgileColor.for_user(issue.assigned_to.login) - compatible_request :get, :index, agile_query_params.merge(:color_base => 'user') - assert_response :success - assert_select 'td.issue-status-col .issue-card[data-id="2"]' - assert_select "img.gravatar[style='#{"border-left: 5px solid #{user_color}".html_safe}']" - assert response.body.include? user_color - end - end - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_none_colors_by_user - with_settings :gravatar_enabled => '1' do - with_agile_settings 'color_on' => 'user' do - issue = Issue.find(2) - user_color = AgileColor.for_user(issue.assigned_to.login) - compatible_request :get, :index, agile_query_params.merge(:color_base => 'none') - assert_response :success - assert_select 'td.issue-status-col .issue-card[data-id="2"]' - assert_select "img.gravatar[style='#{"border-left: 5px solid #{user_color}".html_safe}']", false - assert !(response.body.include? user_color), 'Found color for user' - end - end - end - - def test_get_index_with_swimlanes - compatible_request :get, :index, agile_query_params.merge(:group_by => 'priority') - assert_response :success - assert_select 'tr.group.open.swimlane[data-id="4"] td', /Low/ - assert_select 'tr.group.open.swimlane[data-id="4"] td span.count', { :text => '5' } - - AgileQuery.available_columns.select(&:groupable).each do |column| - compatible_request :get, :index, agile_query_params.merge(:group_by => column.name.to_s) - assert_response :success - end - end - - def test_count_for_swimlanes - issues_count = Issue.where(:project_id => [@project_1.id] + @project_1.children.pluck(:id)).open.group('project_id').count - compatible_request :get, :index, agile_query_params.merge(:group_by => 'project') - assert_response :success - issues_count.each do |c| - assert_select "tr.group.open.swimlane[data-id='#{c[0]}'] td span.count", :text => c[1].to_s - end - end - - def test_get_index_with_sub_issues - issue1 = Issue.find(1) - issue2 = Issue.create!( - :subject => 'Sub issue', - :project => issue1.project, - :tracker => issue1.tracker, - :author => issue1.author, - :parent_issue_id => issue1.id, - :fixed_version => Version.last - ) - - compatible_request :get, :index, agile_query_params.merge(:c => ['sub_issues']) - assert_response :success - - assert_select 'div.sub-issues', 1 - end if Redmine::VERSION.to_s > '2.4' - - def test_proper_columns_for_default_board - # In Redmine 2.3 Query model doesn't have options attribute - # So it can't be default and so 'default' behaviour cannot be broken - # As it does not work at all - if AgileQuery.new.has_attribute?(:options) - AgileQuery.destroy_all - project1 = Project.create!(:name => 'project1', :identifier => 'project1') - board_params = { - :project => project1, - :name => 'board1', - :column_names => [:id, :author, :description], - :options => { :is_default => true }, - :visibility => 2 - } - - AgileQuery.create! board_params - EnabledModule.create(:project => project1, :name => 'agile') - compatible_request :get, :index, :project_id => 'project1' - assert_response :success - end - end - - def test_proper_columns_for_default_board_with_update - if AgileQuery.new.has_attribute?(:options) - board_params = { - :project => @project_1, - :name => 'board_for_project_1', - :column_names => [:id, :author, :description, :project, :tracker, :assignee], - :options => { :is_default => true }, - :visibility => 2 - } - - board = AgileQuery.create! board_params - issues = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a).open(true) - compatible_request :get, :index, :project_id => @project_1, :query_id => board.id - assert_select '.issue-card span.fields p.issue-id strong', issues.count - assert_select '.issue-card span.fields p.project', issues.count - assert_select '.issue-card span.fields p.name', issues.count - assert_select '.issue-card span.fields p.attributes a', issues.count - compatible_request :put, :update, :issue => { :status_id => (issues.first.status_id + 1) }, :id => issues.first.id - # update action return html for one card but with same fields - assert_select '.issue-card span.fields p.issue-id strong', 1 - assert_select '.issue-card span.fields p.project', 1 - assert_select '.issue-card span.fields p.name', 1 - assert_select '.issue-card span.fields p.attributes a', 1 - end - end - - def test_short_card_for_closed_issue - with_agile_settings 'hide_closed_issues_data' => '1' do - closed_status = IssueStatus.where(:is_closed => true).pluck(:id) - closed_issues = Issue.where(:status_id => closed_status) - project = closed_issues.first.project - - # get :index, agile_query_params.merge(:f_status => closed_status) - if Redmine::VERSION.to_s > '2.4' - compatible_request :get, :index, agile_query_params.merge(:f_status => closed_status) - else - compatible_request :get, :index, agile_query_params.merge('f' => ['']) - end - - assert_response :success - assert_select '.closed-issue', project.issues.where(:status_id => IssueStatus.where(:is_closed => true)).count - end - end - - def test_get_tooltip_for_issue - issue = Issue.where(:status_id => IssueStatus.where(:is_closed => true)).first - compatible_request :get, :issue_tooltip, :id => issue.id - assert_response :success - assert_select 'a.issue', 1 - assert_select 'strong', 6 - assert_match issue.status.name, @response.body - end - - def test_empty_node_for_tooltip - with_agile_settings 'hide_closed_issues_data' => '1' do - closed_status = IssueStatus.where(:is_closed => true).pluck(:id) - if Redmine::VERSION.to_s > '2.4' - compatible_request :get, :index, agile_query_params.merge(:f_status => closed_status) - else - compatible_request :get, :index, agile_query_params.merge('f' => ['']) - end - assert_select 'span.tip', { :text => '' } - end - end - - def test_setting_for_closed_issues - with_agile_settings 'hide_closed_issues_data' => '0' do - closed_issues = Issue.where(:status_id => IssueStatus.where(:is_closed => true)) - project = closed_issues.first.project - compatible_request :get, :index, agile_query_params.merge('f' => ['']) - assert_response :success - assert_select '.closed-issue', 0 - end - end - - def test_index_with_js_format - with_agile_settings 'hide_closed_issues_data' => '1' do - closed_issues = Issue.where(:status_id => IssueStatus.where(:is_closed => true)) - project = closed_issues.first.project - compatible_xhr_request :get, :index, agile_query_params.merge('f' => [''], :format => :js) - assert_response :success - assert_match "$('.tooltip').mouseenter(callGetToolTipInfo)", @response.body - end - end - - def test_get_index_with_day_in_state_and_parent_group - compatible_request :get, :index, agile_query_params.merge(:c => ['day_in_state'], :group_by => 'parent') - assert_response :success - end - - def test_assinged_to_and_in_state_in_index - issue1 = Issue.find(1) - issue2 = Issue.create!( - :subject => 'Test assigned_to with day in state', - :project => issue1.project, - :tracker => issue1.tracker, - :author => issue1.author, - :fixed_version => Version.last - ) - compatible_request :get, :index, agile_query_params.merge(:c => ['day_in_state'], :group_by => 'parent') - assigned_to_id = 3 - issue_id = issue2.id - compatible_xhr_request :put, :update, :id => issue_id, :issue => { :assigned_to_id => assigned_to_id } - assert_response :success - issue2.reload - assert_equal assigned_to_id, issue2.assigned_to_id - end - - def test_index_with_relations_relates - create_issue_relation - compatible_request :get, :index, agile_query_params.merge(:f_status => IssueStatus.pluck(:id), :op => { :status_id => '*', :relates => '*' }, :f => ['relates']) - assert_response :success - assert_equal [1, 7, 8], agile_issues_in_list.map(&:id).sort - end if Redmine::VERSION.to_s > '2.4' - - def test_index_with_relations_blocked - create_issue_relation - - compatible_request :get, :index, agile_query_params.merge(:f_status => IssueStatus.pluck(:id), :op => { :status_id => '*', :blocked => '*' }, :f => ['blocked']) - assert_response :success - assert_equal [2, 11], agile_issues_in_list.map(&:id).sort - end if Redmine::VERSION.to_s > '2.4' - - def test_list_of_attributes_on_update_status - params = agile_query_params.merge(:c => ['project', 'day_in_state'], :group_by => 'parent') - params.delete(:project_id) - compatible_request :get, :index, params - assert_response :success - assert_select 'p.project' - compatible_request :put, :update, :issue => { :status_id => 2 }, :id => 1 - assert_select 'p.project', { :text => @project_1.to_s } - end - - def test_day_in_state_when_change_status - @request.session[:agile_query] = {} - @request.session[:agile_query][:column_names] = ['project', 'day_in_state'] - issue = Issue.find(1) - compatible_request :put, :update, :issue => { :status_id => 2 }, :id => 1 - issue.reload - assert_response :success - assert_equal 2, issue.status_id - assert_select 'p.attributes', /0.hours/ - end if Redmine::VERSION.to_s > '2.4' - - def test_global_query_and_project_board - query = AgileQuery.create(:name => 'global', :column_names => ['project'], :visibility => 2, :options => { :is_default => true }) - compatible_request :get, :index, :project_id => 1 - assert_select 'p.project', { :count => 0, :text => Project.find(2).to_s } - end if Redmine::VERSION.to_s > '2.4' - - def test_total_estimated_hours_for_status - [1, 2, 3].each do |id| - issue = Issue.find(id) - issue.estimated_hours = 10 - issue.save - end - compatible_request :get, :index, agile_query_params.merge(:c => ['estimated_hours']) - assert_response :success - assert_select 'thead tr th span.hours', { :count => 1, :text => '20.00h' } # status_id == 1 - assert_select 'thead tr th span.hours', { :count => 1, :text => '10.00h' } # status_id == 2 - end if Redmine::VERSION.to_s > '2.4' - - def test_get_index_with_checklist - # global board - issue1 = issue_with_checklist - - compatible_request :get, :index, agile_query_params.merge(:c => ['checklists'], :project_id => nil) - assert_response :success - # check checklist nodes - assert_select "#checklist_#{issue1.id}" - issue1.checklists.each do |checklist| - assert_select "#checklist_item_#{checklist.id}", :text => checklist.subject - end - end if RedmineAgile.use_checklist? - - def test_get_index_without_checklist - issue1 = issue_with_checklist - compatible_request :get, :index, :c => [] - assert_response :success - # check checklist nodes - assert_select "#checklist_#{issue1.id}", :count => 0 - issue1.checklists.each do |checklist| - assert_select "#checklist_item_#{checklist.id}", :count => 0 - end - end if RedmineAgile.use_checklist? - - def test_get_index_with_project_with_checklist - issue1 = issue_with_checklist - compatible_request :get, :index, agile_query_params.merge(:c => ['checklists'], :project_id => issue1.project) - assert_response :success - # check checklist nodes - assert_select "#checklist_#{issue1.id}" - issue1.checklists.each do |checklist| - assert_select "#checklist_item_#{checklist.id}", :text => checklist.subject - end - end if RedmineAgile.use_checklist? - - def test_get_index_with_project_without_checklist - issue1 = issue_with_checklist - compatible_request :get, :index, agile_query_params.merge(:project_id => issue1.project, :c => []) - assert_response :success - # check checklist nodes - assert_select "#checklist_#{issue1.id}", :count => 0 - issue1.checklists.each do |checklist| - assert_select "#checklist_item_#{checklist.id}", :count => 0 - end - end if RedmineAgile.use_checklist? - - def test_get_index_check_quick_edit_without_permission - role = Role.find(2) - role.permissions << :veiw_issues - role.permissions << :view_agile_queries - role.permissions.delete(:edit_issues) - role.save - @request.session[:user_id] = 3 - - compatible_request :get, :index, :project_id => @project_1 - issues = Issue.where(:project_id => [@project_1], :status_id => IssueStatus.where(:is_closed => false)) - assert_response :success - assert_select '.issue-card', agile_issues_in_list.count - assert_select '.issue-card .quick-edit-card a', :count => 0 - end - - def test_last_comment_on_issue_cart - issue = @project_1.issues.open.first - text_comment = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus in blandit ex. Donec vitae urna quis tortor tempor mollis.' - comment = Journal.new(:notes => text_comment, :user_id => 1) - issue.journals << comment - params = agile_query_params.merge(:c => ['project', 'last_comment'], :group_by => 'parent') - compatible_request :get, :index, params - assert_response :success - assert_select 'span.last-comment', :text => text_comment.truncate(100) - compatible_request :put, :update, :issue => { :status_id => 2 }, :id => issue.id - assert_select 'span.last-comment', :text => text_comment.truncate(100) - end if Redmine::VERSION.to_s > '2.4' - - def test_show_sp_value_on_issue_cart - with_agile_settings 'estimate_units' => 'story_points' do - issues = @project_1.issues.open.first(3) - issues.each do |issue| - issue.agile_data.story_points = issue.id * 10 - issue.save - end - params = agile_query_params.merge(:c => ['story_points']) - compatible_request :get, :index, params - IssueStatus.where(:id => @project_1.issues.open.joins(:status).pluck(:status_id).uniq).each do |status| - sp_sum = @project_1.issues.eager_load(:agile_data).where(:status_id => status.id).sum("#{AgileData.table_name}.story_points") - next unless sp_sum.to_i > 0 - assert_select "th[data-column-id='#{status.id}'] span.hours", :text =>"#{sp_sum}sp" - end - issues.each do |issue| - assert_select ".issue-card[data-id='#{issue.id}'] span.fields p.issue-id span.hours", :text =>"(#{issue.story_points}sp)" - end - # check story_points in available_columns for query - assert_select 'input[value="story_points"]' - end - end if Redmine::VERSION.to_s > '2.4' - - def test_show_sp_with_estimated_hours_on_issue_cart - with_agile_settings 'estimate_units' => 'story_points' do - issues = @project_1.issues.open.first(3) - issues.each do |issue| - issue.agile_data.story_points = issue.id * 10 - issue.estimated_hours = issue.id * 2 - issue.save - end - params = agile_query_params.merge(:c => ['story_points', 'estimated_hours']) - compatible_request :get, :index, params - # in a header show only story_points! - IssueStatus.where(:id => @project_1.issues.open.joins(:status).pluck(:status_id).uniq).each do |status| - sp_sum = @project_1.issues.eager_load(:agile_data).where(:status_id => status.id).sum("#{AgileData.table_name}.story_points") - next unless sp_sum.to_i > 0 - assert_select "th[data-column-id='#{status.id}'] span.hours", :text =>"#{sp_sum}sp" - end - # in a card show and estimated hours and story points - issues.each do |issue| - assert_select ".issue-card[data-id='#{issue.id}'] span.fields p.issue-id span.hours", - :text => "(#{"%.2fh" % issue.estimated_hours.to_f}/#{issue.story_points}sp)" - end - end if Redmine::VERSION.to_s > '2.4' - end - - def test_quick_add_comment_button - with_agile_settings 'allow_inline_comments' => 1 do - compatible_request :get, :index, agile_query_params - assert_response :success - assert_select '.quick-edit-card img[alt="Edit"]' - end - end if Redmine::VERSION.to_s > '2.4' - - def test_quick_add_comment_form - compatible_request :get, :inline_comment, :id => @project_1.issues.open.first - assert_response :success - assert_select 'textarea' - assert_select 'button' - end if Redmine::VERSION.to_s > '2.4' - - def test_quick_add_comment_update - issue = @project_1.issues.open.first - compatible_request :put, :update, :issue => { :notes => 'new comment!!!' }, :id => issue - assert_response :success - assert_select '.last_comment', :text => 'new comment!!!' - end if Redmine::VERSION.to_s > '2.4' - def test_wp_max_count_and_style - statuses = IssueStatus.pluck(:id) - compatible_request :get, :index, 'set_filter' => '1', :project_id => @project_1, :f_status => statuses, :wp => statuses.inject({}){ |r,s| r.merge!(s.to_s => '5') } - issues_for_status = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a).group(:status_id).count - issues_for_status.each do |status_id, issues_count| - if issues_count > 5 - assert_select "th.over_wp_limit[data-column-id='#{status_id}']" - assert_select "th.over_wp_limit[data-column-id='#{status_id}'] span.count span.over_wp_limit", :text => issues_count.to_s - end - assert_select "th[data-column-id='#{status_id}'] span.count", :text => "#{issues_count}/5" - end - end if Redmine::VERSION.to_s > '2.4' - - def test_wp_min_count_and_style - statuses = IssueStatus.pluck(:id) - issues_for_status = Issue.where(:project_id => [@project_1] + Project.where(:parent_id => @project_1.id).to_a).group(:status_id).count - wp_limits = issues_for_status.inject({}) do |h, (k, v)| - h.merge!(k.to_s => "#{v.to_i + 1}-100") - end - - compatible_request :get, :index, 'set_filter' => '1', :project_id => @project_1, :f_status => statuses, :wp => wp_limits - issues_for_status.each do | status_id, issues_count| - if issues_count > 0 - assert_select "th.under_wp_limit[data-column-id='#{status_id}']" - assert_select "th.under_wp_limit[data-column-id='#{status_id}'] span.count span.under_wp_limit", :text => issues_count.to_s - end - end - end if Redmine::VERSION.to_s > '2.4' - - def test_card_for_new_issue - with_agile_settings 'allow_create_card' => 1 do - statuses = IssueStatus.all - compatible_request :get, :index, 'set_filter' => '1', :project_id => @project_1, :f_status => statuses.map(&:id) - assert_select '.add-issue input.new-card__input', IssueStatus.where(:is_closed => false).count - end - end if Redmine::VERSION.to_s > '2.4' - - def test_not_show_card_for_new_issue_without_permission - with_agile_settings 'allow_create_card' => 1 do - role = Role.find(2) - role.permissions << :veiw_issues - role.permissions << :view_agile_queries - role.permissions.delete(:add_issues) - role.save - @request.session[:user_id] = 3 - statuses = IssueStatus.all - compatible_request :get, :index, 'set_filter' => '1', :project_id => @project_1, :f_status => statuses.map(&:id) - assert_select '.add-issue input.new-card__input', 0 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_create_issue_from_board - with_agile_settings 'allow_create_card' => 1 do - assert_difference 'Issue.count' do - compatible_request :post, :create_issue, :project_id => @project_1, :subject => 'new issue from board', :status_id => 1 - end - assert_response :success - end - end if Redmine::VERSION.to_s > '2.4' - - def test_create_issue_from_board_with_empty_subject - with_agile_settings 'allow_create_card' => 1 do - assert_no_difference 'Issue.count' do - compatible_request :post, :create_issue, :project_id => @project_1, :subject => '', :status_id => 1 - end - assert_response 500 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_create_issue_from_board_when_subject_consists_of_only_spaces - with_agile_settings "allow_create_card" => 1 do - assert_no_difference 'Issue.count' do - compatible_request :post, :create_issue, :project_id => @project_1, :subject => ' ', :status_id => 1 - end - assert_response 500 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_create_issue_from_board_without_permission - with_agile_settings 'allow_create_card' => 1 do - role = Role.find(2) - role.permissions << :veiw_issues - role.permissions << :view_agile_queries - role.permissions.delete(:add_issues) - role.save - @request.session[:user_id] = 3 - assert_no_difference 'Issue.count' do - compatible_request :post, :create_issue, :project_id => @project_1, :subject => 'new issue from board', :status_id => 1 - end - assert_response 500 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_on_auto_assign_on_move - with_agile_settings 'auto_assign_on_move' => '1' do - @request.session[:user_id] = 2 - issue = Issue.find(1) - - user = issue.project.users.first - @request.session[:user_id] = user.id - - assert_nil issue.assigned_to - compatible_request :put, :update, :issue => { :status_id => 2 }, :id => 1 - issue.reload - assert_response :success - assert_equal 2, issue.status_id - assert_equal user, issue.assigned_to - end - end - - def test_off_auto_assign_on_move - with_agile_settings 'auto_assign_on_move' => '0' do - issue = Issue.find(1) - - user = issue.project.users.first - @request.session[:user_id] = user.id - - assert_nil issue.assigned_to - compatible_request :put, :update, :issue => { :status_id => 2 }, :id => 1 - issue.reload - assert_response :success - assert_equal 2, issue.status_id - assert_nil issue.assigned_to - end - end - - def test_off_auto_assign_on_move_by_sorting - with_agile_settings 'auto_assign_on_move' => '1' do - @request.session[:user_id] = 2 - issue = Issue.find(1) - - user = issue.project.users.first - @request.session[:user_id] = user.id - - assert_nil issue.assigned_to - compatible_request :put, :update, :issue => { :status_id => issue.status_id }, :id => 1 - issue.reload - assert_response :success - assert_nil issue.assigned_to - end - end - - def test_option_for_select_current_version - @request.session[:user_id] = 1 - compatible_request :get, :index, :project_id => @project_1 - assert_response :success - assert_match 'current_version', response.body - end if Redmine::VERSION.to_s > '2.4' - - def test_filter_by_current_version - @request.session[:user_id] = 1 - compatible_request :get, :index, :project_id => @project_1, 'v' => { 'fixed_version_id' => ['current_version'] }, - 'set_filter' => '1', 'f' => ['fixed_version_id'], - 'op' => { 'fixed_version_id' => '=' } - assert_response :success - current_version = @project_1.shared_versions.order(:effective_date).first - issue = @project_1.issues.first - issue.fixed_version = current_version - issue.save - assert agile_issues_in_list.all? { |i| i.fixed_version == current_version } - end if Redmine::VERSION.to_s > '2.4' - - def test_filter_by_current_version_closed - @request.session[:user_id] = 1 - current_version = @project_1.shared_versions.order(:effective_date).first - current_version.status = 'closed' - current_version.save - - compatible_request :get, :index, :project_id => @project_1, 'v' => { 'fixed_version_id' => ['current_version'] }, - 'set_filter' => '1', 'f' => ['fixed_version_id'], - 'op' => { 'fixed_version_id' => '=' } - assert_response :success - issue = @project_1.issues.first - issue.fixed_version = current_version - issue.save - assert agile_issues_in_list.all?{ |i| i.fixed_version != current_version } - # check set filter for current version - assert_match 'addFilter("fixed_version_id", "=", ["current_version"])', response.body - end if Redmine::VERSION.to_s > '2.4' - - private - - def agile_query_params - { :set_filter => '1', :f => ['status_id', ''], :op => { :status_id => 'o' }, :c => ['tracker', 'assigned_to'], :project_id => 'ecookbook' } - end - - def create_issue_relation - IssueRelation.delete_all - IssueRelation.create!(:relation_type => 'relates', :issue_from => Issue.find(1), :issue_to => Issue.find(7)) - IssueRelation.create!(:relation_type => 'relates', :issue_from => Issue.find(8), :issue_to => Issue.find(1)) - IssueRelation.create!(:relation_type => 'blocks', :issue_from => Issue.find(1), :issue_to => Issue.find(11)) - IssueRelation.create!(:relation_type => 'blocks', :issue_from => Issue.find(12), :issue_to => Issue.find(2)) - end - - def issue_with_checklist - issue1 = Issue.find(1) - chk1 = issue1.checklists.create(:subject => 'TEST1', :position => 1) - chk2 = issue1.checklists.create(:subject => 'TEST2', :position => 2) - issue1 - end if RedmineAgile.use_checklist? - -end diff --git a/plugins/redmine_agile/test/functional/agile_charts_controller_test.rb b/plugins/redmine_agile/test/functional/agile_charts_controller_test.rb index 02b51f7..0752633 100644 --- a/plugins/redmine_agile/test/functional/agile_charts_controller_test.rb +++ b/plugins/redmine_agile/test/functional/agile_charts_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -46,105 +46,150 @@ class AgileChartsControllerTest < ActionController::TestCase :journal_details, :queries - def test_get_show + def setup @request.session[:user_id] = 1 - compatible_request :get, :show - assert_response :success - assert_select 'canvas#agile-chart', 1 + @project = Project.find(1) + @issue = @project.issues.first + + EnabledModule.create(project: @project, name: 'agile') + + @charts = RedmineAgile::Charts::AGILE_CHARTS.keys + @charts_with_units = RedmineAgile::Charts::CHARTS_WITH_UNITS end - def test_get_render_chart_issues_burndown_with_version - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'issues_burndown', :version_id => 2 - assert_response :success - assert_equal 'application/json', @response.content_type + def test_get_show + should_get_show + should_get_show project_id: @project.identifier end - def test_get_render_chart_issues_burndown - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'issues_burndown' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_get_show_with_period + should_get_show({ f: ['issue_id', ''], op: { 'issue_id' => '*' } }) + should_get_show({ f: ['issue_id', ''], op: { 'issue_id' => '*' }, project_id: @project.identifier }) end - def test_get_render_chart_work_burndown_sp - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'work_burndown_sp' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_by_default_params + @charts.each { |chart| check_chart(chart: chart, project_id: @project.identifier) } end - def test_get_render_chart_work_burndown_hours - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'work_burndown_hours' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_with_chart_unit + @charts_with_units.each do |chart| + RedmineAgile::Charts::CHART_UNITS.each do |chart_unit, label| + check_chart chart: chart, project_id: @project.identifier, chart_unit: chart_unit + end + end end - def test_get_render_chart_burnup - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'burnup' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_by_different_time_intervals + @charts.each do |chart| + RedmineAgile::AgileChart::TIME_INTERVALS.each do |interval| + check_chart chart: chart, project_id: @project.identifier, interval_size: interval + end + end end - def test_get_render_chart_work_burnup_sp - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'work_burnup_sp' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_by_different_periods_and_time_intervals + @charts.each do |chart| + RedmineAgile::AgileChart::TIME_INTERVALS.each do |interval| + params = { + chart: chart, + project_id: @project.identifier, + interval_size: interval, + set_filter: 1, + f: ['chart_period'] + } + + check_chart params.merge(op: { chart_period: '=' }, v: { chart_period: ['2014-01-01'] }) + check_chart params.merge(op: { chart_period: '>=' }, v: { chart_period: ['2014-01-01'] }) + check_chart params.merge(op: { chart_period: '<=' }, v: { chart_period: ['2019-01-01'] }) + check_chart params.merge(op: { chart_period: '><' }, v: { chart_period: ['2014-01-01', '2018-12-31'] }) + check_chart params.merge(op: { chart_period: '>t-' }, v: { chart_period: [99] }) + check_chart params.merge(op: { chart_period: '<t-' }, v: { chart_period: [99] }) + check_chart params.merge(op: { chart_period: '><t-' }, v: { chart_period: [99] }) + check_chart params.merge(op: { chart_period: 't-' }, v: { chart_period: [99] }) + check_chart params.merge(op: { chart_period: 't' }) + check_chart params.merge(op: { chart_period: 'ld' }) + check_chart params.merge(op: { chart_period: 'w' }) + check_chart params.merge(op: { chart_period: 'lw' }) + check_chart params.merge(op: { chart_period: 'l2w' }) + check_chart params.merge(op: { chart_period: 'm' }) + check_chart params.merge(op: { chart_period: 'lm' }) + check_chart params.merge(op: { chart_period: 'y' }) + check_chart params.merge(op: { chart_period: '!*' }) + check_chart params.merge(op: { chart_period: '*' }) + end + end end - def test_get_render_chart_work_burnup_hours - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'work_burnup_hours' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_render_charts + @charts.each do |chart| + should_get_render_chart chart: chart, chart_unit: 'issues' + end end - def test_get_render_chart_trackers_cumulative_flow - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'trackers_cumulative_flow' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_with_version + @charts.each do |chart| + should_get_render_chart chart: chart, version_id: 2 + should_get_render_chart chart: chart, version_id: 2, project_id: @project.identifier + end end - def test_get_render_chart_cumulative_flow - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'cumulative_flow' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_charts_with_version_and_chart_unit + @charts_with_units.each do |chart| + RedmineAgile::Charts::CHART_UNITS.each do |chart_unit, label| + should_get_render_chart chart: chart, version_id: 2, chart_unit: chart_unit + end + end end - def test_get_render_chart_issues_velocity - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'issues_velocity' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_issues_burndown_chart_when_first_issue_later_then_due_date + new_version = Version.create!(name: 'Some new vesion', effective_date: (Date.today - 10.days), project_id: @project.id) + issue = Issue.create!( + project_id: @project.id, + tracker_id: 1, + subject: 'test_issues_burndown_chart_when_first_issue_later_then_due_date', + author_id: 2, + start_date: Date.today + ) + new_version.fixed_issues << issue.reload + + should_get_render_chart chart: RedmineAgile::Charts::BURNDOWN_CHART, project_id: @project.identifier, version_id: new_version.id end - def test_get_render_chart_lead_time - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'lead_time' - assert_response :success - assert_equal 'application/json', @response.content_type + def test_get_show_chart_with_open_target_version + current_version = @issue.fixed_version + @issue.update(fixed_version: Version.open.first) + + should_get_render_chart project_id: @project.identifier, chart: 'burndown_chart', + f: ['version_status'], + op: { 'version_status' => '=' }, + v: { 'version_status' => ['open'] } + ensure + @issue.update(fixed_version: current_version) end - def test_get_render_chart_average_lead_time - @request.session[:user_id] = 1 - compatible_request :get, :render_chart, :chart => 'average_lead_time' + private + + def should_get_show(parameters = {}) + compatible_request :get, :show, parameters assert_response :success - assert_equal 'application/json', @response.content_type + assert_select 'canvas#agile-chart', 1 end - def test_issues_burndown_chart_when_first_issue_later_then_due_date - @project_1 = Project.find(1) - EnabledModule.create(:project => @project_1, :name => 'agile') - @request.session[:user_id] = 1 - new_version = Version.create!(:name => 'Some new vesion', :effective_date => (Date.today - 10.days), :project_id => @project_1.id) - issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_issues_burndown_chart_when_first_issue_later_then_due_date', :author_id => 2, :start_date => Date.today) - new_version.fixed_issues << issue - compatible_request :get, :render_chart, :chart => 'issues_burndown', :project_id => 1, :version_id => new_version.id + def should_get_render_chart(parameters = {}) + compatible_xhr_request :get, :render_chart, parameters assert_response :success + assert_match 'application/json', response.content_type + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_equal parameters[:chart], json['chart'] + if parameters[:chart_unit] + assert_equal parameters[:chart_unit], json['chart_unit'] + end + end + + def check_chart(parameters = {}) + should_get_show parameters + should_get_render_chart parameters.slice(:chart, :project_id) end end diff --git a/plugins/redmine_agile/test/functional/agile_journal_details_controller_test.rb b/plugins/redmine_agile/test/functional/agile_journal_details_controller_test.rb index d63aa5c..5d3db81 100644 --- a/plugins/redmine_agile/test/functional/agile_journal_details_controller_test.rb +++ b/plugins/redmine_agile/test/functional/agile_journal_details_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -55,7 +55,7 @@ class AgileJournalDetailsControllerTest < ActionController::TestCase end def test_get_done_ratio - compatible_request :get, :done_ratio, :issue_id => 1 + compatible_request :get, :done_ratio, issue_id: 1 assert_response :success assert_match /% Done/, @response.body assert_match /Bug #1/, @response.body @@ -63,15 +63,30 @@ class AgileJournalDetailsControllerTest < ActionController::TestCase end def test_get_status - compatible_request :get, :status, :issue_id => 1 + compatible_request :get, :status, issue_id: 1 assert_response :success assert_match /Issue statuses/, @response.body assert_match /Bug #1/, @response.body assert_select '.list td.name', 2 end + def test_get_status_with_group + compatible_request :get, :status, issue_id: 1, group_by: 'status' + assert_response :success + assert_match /Issue statuses/, @response.body + assert_match /Bug #1/, @response.body + assert_select 'tr.group' + end + + def test_get_status_csv + compatible_request :get, :status, issue_id: 1, format: :csv + assert_response :success + assert_match 'text/csv', @response.content_type + assert_match /#,Created/, @response.body + end + def test_get_done_assignee - compatible_request :get, :assignee, :issue_id => 1 + compatible_request :get, :assignee, issue_id: 1 assert_response :success assert_match /Assignee/, @response.body assert_match /Bug #1/, @response.body diff --git a/plugins/redmine_agile/test/functional/agile_queries_controller_test.rb b/plugins/redmine_agile/test/functional/agile_queries_controller_test.rb deleted file mode 100644 index 37edd27..0000000 --- a/plugins/redmine_agile/test/functional/agile_queries_controller_test.rb +++ /dev/null @@ -1,134 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) -class AgileQueriesControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - RedmineAgile::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_agile).directory + '/test/fixtures/', [:queries]) - - def setup - RedmineAgile::TestCase.prepare - end - - def test_get_index - @request.session[:user_id] = 1 - compatible_request :get, :index - assert_response :success - assert_match /Agile boards/, @response.body - end - - def test_get_new - @request.session[:user_id] = 1 - compatible_request :get, :new - assert_response :success - assert_match /New agile board/, @response.body - end - - def test_get_edit - @request.session[:user_id] = 1 - compatible_request :get, :edit, :id => create_agile_query.id - assert_response :success - assert_match /Edit agile board/, @response.body - end - - def test_get_edit_for_public_board - @request.session[:user_id] = 2 - compatible_request :get, :edit, id: AgileQuery.find(102).id # Public board query - assert_response :success - assert_match /Edit agile board/, @response.body - end - - def test_post_create - @request.session[:user_id] = 1 - params = { :query => { :name => 'Test', :group_by => '' }, - :query_is_for_all => '1', :default_columns => '1', :f => ['status_id', ''], - :op => { 'status_id' => 'o' }, :c => ['tracker', 'assigned_to'] } - if Redmine::VERSION.to_s < '2.4' - params[:query][:is_public] = true - else - params[:query][:visibility] = '0' - end - assert_difference 'AgileQuery.count' do - compatible_request :post, :create, params - assert_response :redirect - end - end - - def test_put_update - @request.session[:user_id] = 1 - params = { :query => { :name => 'Test changed', :group_by => ''}, :id => create_agile_query.id} - Redmine::VERSION.to_s < '2.4' ? params[:query][:is_public] = true : params[:query][:visibility] = '0' - compatible_request :put, :update, params - assert_response :redirect - end - def test_save_wip_options - @request.session[:user_id] = 1 - wp_params = { '1' => '5', '2' => '5', '3' => '5', '4' => '5', '5' => '', '6' => '' } - params = { :query => { :name => 'Test', :group_by => '', :is_default => '1' }, - :query_is_for_all => '1', - :default_columns => '1', - :f_status => ['1', '2', '3', '4'], - :wp => wp_params, - :op => { 'status_id' => 'o' }, - :c => ['tracker', 'assigned_to'] } - compatible_request :post, :create, params - query = Query.last - assert_equal true, query.is_default? - assert_equal wp_params, query.options[:wp] - assert_equal ['1', '2', '3', '4'], query.options[:f_status] - end if Redmine::VERSION.to_s > '2.4' - -private - - def create_agile_query - query = AgileQuery.new(:name => 'Board for specific project', - :user_id => 1, - :project_id => 1, - :filters => { :tracker_id => { :values => ['3'], :operator => '=' } }) - Redmine::VERSION.to_s < '2.4' ? query.is_public = false : query.visibility = 2 - query.save - query - end - -end diff --git a/plugins/redmine_agile/test/functional/agile_versions_controller_test.rb b/plugins/redmine_agile/test/functional/agile_versions_controller_test.rb deleted file mode 100644 index 5c19385..0000000 --- a/plugins/redmine_agile/test/functional/agile_versions_controller_test.rb +++ /dev/null @@ -1,124 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class AgileVersionsControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - def setup - RedmineAgile.create_issues - - @project_1 = Project.find(1) - @project_2 = Project.find(2) - @project_3 = Project.find(5) - - EnabledModule.create(:project => @project_1, :name => 'agile') - EnabledModule.create(:project => @project_2, :name => 'agile') - EnabledModule.create(:project => @project_3, :name => 'agile') - - @request.session[:user_id] = 1 - end - - def test_get_index - compatible_request :get, :index, :project_id => @project_1 - assert_response :success - assert_match /Version planning/, @response.body - end - - def test_get_load - compatible_xhr_request :get, :load, :version_type => 'backlog', :version_id => '3', :project_id => 'ecookbook' - assert_response :success - end - - def test_get_autocomplete_id - compatible_xhr_request :get, :autocomplete, :project_id => 'ecookbook', :q => '#3' - assert_response :success - assert_match "Error 281", @response.body - end - - def test_get_autocomplete_text - compatible_xhr_request :get, :autocomplete, :project_id => 'ecookbook', :q => 'error' - assert_response :success - assert_match "Error 281", @response.body - end - def test_get_index_with_filter - compatible_request :get, :index, version_params - - assert_response :success - - assert_match /Issue 108/, @response.body - assert_match /Blaaa/, @response.body - assert_match /Issue 105/, @response.body - assert_match /Issue 106/, @response.body - - assert_no_match /(Issue 100)|(Issue 101)|(Issue 102)|(Issue 103)/, @response.body - end - - def test_get_index_with_filter_and_search_query - compatible_request :get, :index, version_params.merge({ :q => 'Blaaa' }) - - assert_no_match /Issue 108/, @response.body - assert_match /Blaaa/, @response.body - end - - def test_get_index_with_sp - with_agile_settings "estimate_units" => "story_points" do - compatible_request :get, :index, :project_id => @project_2 - assert_response :success - assert_select '.issue-card[data-id="100"] span.hours', :text => '10.00sp' - end - end - - private - - def version_params - { - :f =>['status_id'], - :op => { 'status_id' => '=' }, - :v => { 'status_id' => ['5'] }, - :project_id => @project_2 - } - end - -end diff --git a/plugins/redmine_agile/test/functional/issues_controller_test.rb b/plugins/redmine_agile/test/functional/issues_controller_test.rb index 5ea9766..f124644 100644 --- a/plugins/redmine_agile/test/functional/issues_controller_test.rb +++ b/plugins/redmine_agile/test/functional/issues_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -53,30 +53,9 @@ class IssuesControllerTest < ActionController::TestCase EnabledModule.create(:project => @project_2, :name => 'agile') @request.session[:user_id] = 1 end - def test_get_index_with_colors - with_agile_settings "color_on" => "issue" do - issue = Issue.find(1) - issue.color = AgileColor::AGILE_COLORS[:red] - issue.save - compatible_request :get, :index - assert_response :success - assert_select 'tr#issue-1.issue.bk-red', 1 - end - end - - def test_post_issue_journal_color - with_agile_settings 'color_on' => 'issue' do - compatible_request :put, :update, :id => 1, :issue => { :agile_color_attributes => { :color => AgileColor::AGILE_COLORS[:red] } } - issue = Issue.find(1) - details = issue.journals.order(:id).last.details.last - assert issue.color - assert_equal 'color', details.prop_key - assert_equal AgileColor::AGILE_COLORS[:red], details.value - end - end def test_new_issue_with_sp_value - with_agile_settings 'estimate_units' => 'story_points' do + with_agile_settings 'estimate_units' => 'story_points', 'story_points_on' => '1' do compatible_request :get, :new, :project_id => 1 assert_response :success assert_select 'input#issue_agile_data_attributes_story_points' @@ -92,7 +71,7 @@ class IssuesControllerTest < ActionController::TestCase end def test_create_issue_with_sp_value - with_agile_settings 'estimate_units' => 'story_points' do + with_agile_settings 'estimate_units' => 'story_points', 'story_points_on' => '1' do assert_difference 'Issue.count' do compatible_request :post, :create, :project_id => 1, :issue => { :subject => 'issue with sp', @@ -109,7 +88,7 @@ class IssuesControllerTest < ActionController::TestCase end def test_post_issue_journal_story_points - with_agile_settings 'estimate_units' => 'story_points' do + with_agile_settings 'estimate_units' => 'story_points', 'story_points_on' => '1' do compatible_request :put, :update, :id => 1, :issue => { :agile_data_attributes => { :story_points => 100 } } issue = Issue.find(1) assert_equal 100, issue.story_points @@ -120,10 +99,10 @@ class IssuesControllerTest < ActionController::TestCase end def test_show_issue_with_story_points - with_agile_settings 'estimate_units' => 'story_points' do + with_agile_settings 'estimate_units' => 'story_points', 'story_points_on' => '1' do compatible_request :get, :show, :id => 1 assert_response :success - assert_select '.attributes', :text => /Story points/, :count => 1 + assert_select '#issue-form .attributes', :text => /Story points/, :count => 1 end end @@ -135,29 +114,12 @@ class IssuesControllerTest < ActionController::TestCase :totalable_names => [], :sort => [['story_points', 'asc'], ['id', 'desc']] } - with_agile_settings 'estimate_units' => 'story_points' do + with_agile_settings 'estimate_units' => 'story_points', 'story_points_on' => '1' do compatible_request :get, :show, :id => 1 assert_response :success - assert_select '.attributes', :text => /Story points/, :count => 1 + assert_select '#issue-form .attributes', :text => /Story points/, :count => 1 end ensure session[:issue_query] = {} end - def test_show_issue_form_with_story_points_select - with_agile_settings('sp_values' => [1,2,3], - 'estimate_units' => 'story_points') do - compatible_request :get, :new, :project_id => 1 - assert_response :success - assert_select 'select#issue_agile_data_attributes_story_points' - end - end - - def test_dont_show_story_points_select_when_no_sp_values - with_agile_settings('sp_values' => [], - 'estimate_units' => 'story_points') do - compatible_request :get, :new, :project_id => 1 - assert_response :success - assert_select 'select#issue_agile_data_attributes_story_points', :count => 0 - end - end end diff --git a/plugins/redmine_agile/test/functional/projects_controller_test.rb b/plugins/redmine_agile/test/functional/projects_controller_test.rb index da7e3f2..67d3a32 100644 --- a/plugins/redmine_agile/test/functional/projects_controller_test.rb +++ b/plugins/redmine_agile/test/functional/projects_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -36,22 +36,4 @@ class ProjectsControllerTest < ActionController::TestCase EnabledModule.create(:project => @project_2, :name => 'agile') @request.session[:user_id] = 1 end - def test_get_index_with_colors - with_agile_settings 'color_on' => 'project' do - compatible_request :get, :settings, :id => @project_1 - assert_response :success - assert_select '#project_agile_color_attributes_color', 1 - end - end - - - def test_save_project_with_color - with_agile_settings 'color_on' => 'project' do - compatible_request :post, :update, :id => @project_1, :project => { :name => 'Test changed name', - :agile_color_attributes => { :color => AgileColor::AGILE_COLORS[:red] } } - @project_1.reload - assert_equal 'Test changed name', @project_1.name - assert_equal AgileColor::AGILE_COLORS[:red], @project_1.color - end - end end diff --git a/plugins/redmine_agile/test/functional/users_controller_test.rb b/plugins/redmine_agile/test/functional/users_controller_test.rb index 213f427..71aa90c 100644 --- a/plugins/redmine_agile/test/functional/users_controller_test.rb +++ b/plugins/redmine_agile/test/functional/users_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -33,19 +33,4 @@ class UsersControllerTest < ActionController::TestCase @user = User.find(1) @request.session[:user_id] = @user.id end - def test_get_index_with_colors - with_agile_settings 'color_on' => 'user' do - compatible_request :get, :edit, :id => @user.id - assert_response :success - assert_select '#user_agile_color_attributes_color', 1 - end - end if Redmine::VERSION.to_s > '2.4' - - def test_save_user_with_color - with_agile_settings 'color_on' => 'user' do - compatible_request :post, :update, :id => @user.id, - :user => { :agile_color_attributes => { :color => AgileColor::AGILE_COLORS[:red] } } - assert_equal AgileColor::AGILE_COLORS[:red], @user.reload.color - end - end if Redmine::VERSION.to_s > '2.4' end diff --git a/plugins/redmine_agile/test/integration/common_views_test.rb b/plugins/redmine_agile/test/integration/common_views_test.rb index c595266..eb3936c 100644 --- a/plugins/redmine_agile/test/integration/common_views_test.rb +++ b/plugins/redmine_agile/test/integration/common_views_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/test/test_helper.rb b/plugins/redmine_agile/test/test_helper.rb index efa1661..73e3b83 100644 --- a/plugins/redmine_agile/test/test_helper.rb +++ b/plugins/redmine_agile/test/test_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -73,6 +73,11 @@ module RedmineAgile send(type, action, :params => parameters, :xhr => true) end + def compatible_api_request(type, action, parameters = {}, headers = {}) + return send(type, action, :params => parameters, :headers => headers) if Rails.version >= '5.1' + send(type, action, parameters, headers) + end + def agile_issues_in_list ids = css_select('p.issue-id input').map { |tag| tag.to_s.to_s[/.*?value=\"(\d+)".*?/, 1] }.map(&:to_i) Issue.where(:id => ids).sort_by { |issue| ids.index(issue.id) } @@ -105,8 +110,8 @@ module RedmineAgile # journal = issue.init_journal(User.current) issue.done_ratio = value issue.save - issue.update_attributes(:updated_on => change_date) - Journal.last.update_attributes(:created_on => change_date) + issue.update(:updated_on => change_date) + Journal.last.update(:created_on => change_date) end def close_issue(issue, closed_on) @@ -116,11 +121,11 @@ module RedmineAgile days_count = (issue.due_date - issue.created_on).to_i change_date = issue.created_on change_date = change_date + rand((issue.due_date - days_count).to_i) - issue.update_attributes(:status_id => in_status.id, :updated_on => change_date) - journal.update_attributes(:created_on => change_date) + issue.update(:status_id => in_status.id, :updated_on => change_date) + journal.update(:created_on => change_date) change_date = change_date + rand((issue.due_date - days_count).to_i) - issue.update_attributes(:status_id => done_status.id, :updated_on => change_date) - journal.update_attributes(:created_on => change_date) + issue.update(:status_id => done_status.id, :updated_on => change_date) + journal.update(:created_on => change_date) issue end @@ -354,6 +359,7 @@ class RedmineAgile::TestCase r.permissions << :manage_public_agile_queries r.permissions << :add_agile_queries r.permissions << :view_agile_queries + r.permissions << :agile_versions r.save end end diff --git a/plugins/redmine_agile/test/ui/agile_board_ui_test.rb b/plugins/redmine_agile/test/ui/agile_board_ui_test.rb index 3c99997..b27389f 100644 --- a/plugins/redmine_agile/test/ui/agile_board_ui_test.rb +++ b/plugins/redmine_agile/test/ui/agile_board_ui_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/test/unit/agile_color_test.rb b/plugins/redmine_agile/test/unit/agile_color_test.rb deleted file mode 100644 index 3466977..0000000 --- a/plugins/redmine_agile/test/unit/agile_color_test.rb +++ /dev/null @@ -1,80 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class AgileColorTest < ActiveSupport::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - # Replace this with your real tests. - def test_save_color - issue = Issue.find(1) - assert_nil issue.color - issue.agile_color.color = AgileColor::AGILE_COLORS[:red] - assert issue.save - issue.reload - assert_equal AgileColor::AGILE_COLORS[:red], issue.color - end - - def test_delete_color - issue = Issue.find(1) - assert_nil issue.color - issue.agile_color.color = AgileColor::AGILE_COLORS[:red] - assert issue.save - issue.reload - color = issue.agile_color - assert issue.destroy - assert !AgileColor.exists?(color.id) - end - - def test_color_for_spent_time - assert_equal 'gray', AgileColor.for_spent_time(nil, nil) - assert_equal 'gray', AgileColor.for_spent_time(nil, 10) - assert_equal 'green', AgileColor.for_spent_time(20, 10) - assert_equal 'yellow', AgileColor.for_spent_time(20, 19) - assert_equal 'red', AgileColor.for_spent_time(20, 30) - assert_equal 'purple', AgileColor.for_spent_time(20, 42) - assert_equal 'gray', AgileColor.for_spent_time(0.0, 0.0) - assert_equal 'gray', AgileColor.for_spent_time(0.0, 8.8) - end -end diff --git a/plugins/redmine_agile/test/unit/agile_data_test.rb b/plugins/redmine_agile/test/unit/agile_data_test.rb index cbec736..13bf20a 100644 --- a/plugins/redmine_agile/test/unit/agile_data_test.rb +++ b/plugins/redmine_agile/test/unit/agile_data_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_agile/test/unit/agile_versions_query_test.rb b/plugins/redmine_agile/test/unit/agile_versions_query_test.rb deleted file mode 100644 index c473fcc..0000000 --- a/plugins/redmine_agile/test/unit/agile_versions_query_test.rb +++ /dev/null @@ -1,138 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmin Agile (redmine_agile) plugin, -# Agile board plugin for redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_agile is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_agile is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_agile. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class AgileVersionsQueryTest < ActiveSupport::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issue_statuses, - :issues, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - def filter_fields - ['assigned_to_id', 'tracker_id', 'status_id', 'author_id', 'category_id'] # estimated_hours - end - - def setup - super - RedmineAgile.create_issues - @query = AgileVersionsQuery.new(:name => '_') - @query.project = Project.find(2) - @backlog_version = Version.find(7) - @current_version = Version.find(5) - User.current = User.find(1) # because issues selected according permissions - end - - def test_backlog_version - assert_equal @backlog_version, @query.backlog_version - end - - def test_current_version - assert_equal @current_version, @query.current_version - end - - def test_backlog_issues - assert_equal [100, 101, 102, 103], @query.backlog_version_issues.map(&:id).sort - end - - def test_current_issues - assert_equal [104], @query.current_version_issues.map(&:id).sort - end - def test_current_version_issues - assert_equal 1, @query.current_version_issues.count - - @query.build_from_params(:f => ['status_id'], :o => ['status_id' => '*'], :v => ['status_id' => IssueStatus.all.map(&:id)]) - assert_equal [104, 105, 106], @query.current_version_issues.map(&:id).sort - end - - def test_filters_backlog_issues_in - filter_fields.each do |filter| - m = "Error in the #{filter} filter" - hash = { - :f =>[filter], - :op => { filter => '='}, - :v => { filter => ['1', '3'] } } - - @query.build_from_params(hash) - assert_equal [1, 3], @query.backlog_version_issues.collect { |issue| issue.send(filter.to_sym).to_i }.uniq.sort, m - end - end - - def test_filters_backlog_issues_not_in - filter_fields.each do |filter| - m = "Error in the #{filter} filter" - hash = { - :f => [filter], - :op => { filter => '!' }, - :v => { filter => ['1', '3'] } } - - @query.build_from_params(hash) - issues = @query.backlog_version_issues.collect { |issue| issue.send(filter.to_sym) }.uniq.sort - assert_equal [], [1, 3] & issues, m - end - end - - def test_with_few_filters - hash = { - :f => ['assigned_to_id', 'priority_id', 'tracker_id', 'estimated_hours'], - :op => { 'assigned_to_id' => '*', 'priority_id' => '!', 'tracker_id' => '=', 'estimated_hours' => '><' }, - :v => { 'priority_id' => ['3'], 'tracker_id' => ['1', '2', '3'], 'estimated_hours' => ['2', '7'] } } - @query.build_from_params(hash) - assert_equal [100], @query.backlog_version_issues.map(&:id) - assert_equal [105], @query.current_version_issues.map(&:id).sort - end - - def test_no_assigned_to - hash = { - :f => ['assigned_to_id'], :op => { 'assigned_to_id' => '!*' } - } - @query.build_from_params(hash) - assert_equal [104], @query.current_version_issues.map(&:id).sort - end - - def test_no_version_issues - hash = { - :f =>['tracker_id'], - :op => { 'tracker_id' => '=' }, - :v => { 'tracker_id' => ['1', '2', '3'] } } - @query.build_from_params(hash) - assert_equal [107, 109], @query.no_version_issues(:q => 'bla').map(&:id).sort - assert_equal [109], @query.no_version_issues(:q => '#109').map(&:id) - end -end diff --git a/plugins/redmine_agile/test/unit/helpers/agile_boards_helper_test.rb b/plugins/redmine_agile/test/unit/helpers/agile_boards_helper_test.rb index 3dc35ff..accb368 100644 --- a/plugins/redmine_agile/test/unit/helpers/agile_boards_helper_test.rb +++ b/plugins/redmine_agile/test/unit/helpers/agile_boards_helper_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmin Agile (redmine_agile) plugin, # Agile board plugin for redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_agile is free software: you can redistribute it and/or modify @@ -38,135 +38,6 @@ class AgileBoardsHelperTest < ActiveSupport::TestCase EnabledModule.create(:project => Project.find(1), :name => 'issue_tracking') if RedmineAgile.use_checklist? end - def test_render_board_headers_flat - columns = [ - status_1 = IssueStatus.create(:name => "ToDo"), - status_2 = IssueStatus.create(:name => "Doing"), - status_3 = IssueStatus.create(:name => "Done"), - ] - - html = %{ - <tr><th data-column-id="#{status_1.id}">ToDo (<span class="count">0</span>)</th><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_rowspan - # | One | Two | Three | - # | | Doing | Done | - columns = [ - status_1 = IssueStatus.create(:name => "One"), - status_2 = IssueStatus.create(:name => "Two:Doing"), - status_3 = IssueStatus.create(:name => "Three:Done"), - ] - - html = %{ - <tr><th data-column-id="#{status_1.id}" rowspan="2">One (<span class="count">0</span>)</th><th>Two</th><th>Three</th></tr><tr><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_colspan - # | One | Two | - # | | Doing | Done | - columns = [ - status_1 = IssueStatus.create(:name => "One"), - status_2 = IssueStatus.create(:name => "Two:Doing"), - status_3 = IssueStatus.create(:name => "Two:Done"), - ] - - html = %{ - <tr><th data-column-id="#{status_1.id}" rowspan="2">One (<span class="count">0</span>)</th><th colspan="2">Two</th></tr><tr><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_colspan_first - # | One | Two | - # | ToDo | Doing | Done | - columns = [ - status_1 = IssueStatus.create(:name => "One:ToDo"), - status_2 = IssueStatus.create(:name => "One:Doing"), - status_3 = IssueStatus.create(:name => "Two:Done"), - ] - - html = %{ - <tr><th colspan="2">One</th><th>Two</th></tr><tr><th data-column-id="#{status_1.id}">ToDo (<span class="count">0</span>)</th><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_three_levels - # | Mega One | | - # | One | Two | Mega Two | - # | ToDo | Doing | Done | | - columns = [ - status_1 = IssueStatus.create(:name => "Mega One:One:ToDo"), - status_2 = IssueStatus.create(:name => "Mega One:One:Doing"), - status_3 = IssueStatus.create(:name => "Mega One:Two:Done"), - status_4 = IssueStatus.create(:name => "Mega Two"), - ] - - html = %{ - <tr><th colspan="3">Mega One</th><th data-column-id="#{status_4.id}" rowspan="3">Mega Two (<span class="count">0</span>)</th></tr><tr><th colspan="2">One</th><th>Two</th></tr><tr><th data-column-id="#{status_1.id}">ToDo (<span class="count">0</span>)</th><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_same_name_levels - # | Mega One | | Mega Two | - # | One | Two | Mega Two | One | - # | ToDo | Doing | Done | | | - columns = [ - status_1 = IssueStatus.create(:name => "Mega One:One:ToDo"), - status_2 = IssueStatus.create(:name => "Mega One:One:Doing"), - status_3 = IssueStatus.create(:name => "Mega One:Two:Done"), - status_4 = IssueStatus.create(:name => "Mega Two"), - status_5 = IssueStatus.create(:name => "Mega Two:One"), - ] - - html = %{ - <tr><th colspan="3">Mega One</th><th data-column-id="#{status_4.id}" rowspan="3">Mega Two (<span class="count">0</span>)</th><th>Mega Two</th></tr><tr><th colspan="2">One</th><th>Two</th><th data-column-id="#{status_5.id}" rowspan="2">One (<span class="count">0</span>)</th></tr><tr><th data-column-id="#{status_1.id}">ToDo (<span class="count">0</span>)</th><th data-column-id="#{status_2.id}">Doing (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - - def test_render_board_headers_keep_order - # | Dev | Testing | Dev | - # | ToDo | | Done | - columns = [ - status_1 = IssueStatus.create(:name => "Dev:ToDo"), - status_2 = IssueStatus.create(:name => "Testing"), - status_3 = IssueStatus.create(:name => "Dev:Done") - ] - - html = %{ - <tr><th>Dev</th><th data-column-id="#{status_2.id}" rowspan="2">Testing (<span class="count">0</span>)</th><th>Dev</th></tr><tr><th data-column-id="#{status_1.id}">ToDo (<span class="count">0</span>)</th><th data-column-id="#{status_3.id}">Done (<span class="count">0</span>)</th></tr> - }.strip.gsub(/\r/, "") - - headers = render_board_headers(columns) - - assert_equal html, headers - end - def test_time_in_state hour10 = Time.now - 10.hours assert_equal "#{I18n.t('datetime.distance_in_words.x_hours', :count => 10)}", time_in_state(hour10) diff --git a/plugins/redmine_checklists/app/controllers/checklist_template_categories_controller.rb b/plugins/redmine_checklists/app/controllers/checklist_template_categories_controller.rb deleted file mode 100644 index 03893bc..0000000 --- a/plugins/redmine_checklists/app/controllers/checklist_template_categories_controller.rb +++ /dev/null @@ -1,72 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -class ChecklistTemplateCategoriesController < ApplicationController - unloadable - - before_action :find_category, :only => [:destroy, :update, :edit] - - def create - @category = ChecklistTemplateCategory.new - @category.safe_attributes = params[:category] - if @category.save - flash[:notice] = l(:notice_successful_create) - redirect_to_list - else - render :action => 'new' - end - end - - def destroy - @category.destroy - redirect_to_list - rescue - flash[:error] = l(:label_finance_can_not_delete_category) - redirect_to_list - end - - def update - @category.safe_attributes = params[:category] - @category.insert_at(@category.position) if @category.position_changed? - if @category.save - respond_to do |format| - format.html do - flash[:notice] = l(:notice_successful_update) - redirect_to_list - end - format.js { head 200 } - end - else - respond_to do |format| - format.html { render :action => 'edit' } - format.js { head 422 } - end - end - end - -private - - def find_category - @category = ChecklistTemplateCategory.find(params[:id]) - end - - def redirect_to_list - redirect_to :action =>"plugin", :id => "redmine_checklists", :controller => "settings", :tab => 'checklist_template_categories' - end -end diff --git a/plugins/redmine_checklists/app/controllers/checklist_templates_controller.rb b/plugins/redmine_checklists/app/controllers/checklist_templates_controller.rb deleted file mode 100644 index 651d907..0000000 --- a/plugins/redmine_checklists/app/controllers/checklist_templates_controller.rb +++ /dev/null @@ -1,94 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -class ChecklistTemplatesController < ApplicationController - unloadable - - before_action :find_checklist_template, :except => [:new, :create, :index] - before_action :find_optional_project, :only => [:new, :create, :add, :destroy, :edit, :update] - before_action :require_admin, :only => [:index] - - def new - @checklist_template = ChecklistTemplate.new - @checklist_template.user = User.current - @checklist_template.project = @project - @checklist_template.is_public = false unless User.current.allowed_to?(:manage_checklist_templates, @project) || User.current.admin? - end - - def create - @checklist_template = ChecklistTemplate.new - @checklist_template.safe_attributes = params[:checklist_template] - @checklist_template.user = User.current - @checklist_template.project = params[:checklist_template_is_for_all] && User.current.admin? ? nil : @project - @checklist_template.is_public = false unless User.current.allowed_to?(:manage_checklist_templates, @project) || User.current.admin? - - if @checklist_template.save - flash[:notice] = l(:notice_successful_create) - redirect_to_project_or_global - else - render :action => 'new', :layout => !request.xhr? - end - end - - def edit - end - - def update - @checklist_template.safe_attributes = params[:checklist_template] - @checklist_template.project = nil if params[:checklist_template_is_for_all] - @checklist_template.project = @project if params[:checklist_template][:is_public] == '1' && !User.current.admin? - @checklist_template.project = (params[:checklist_template_is_for_all] && User.current.admin?) ? nil : @project - @checklist_template.is_public = false unless User.current.allowed_to?(:manage_checklist_templates, @project) || User.current.admin? - - if @checklist_template.save - flash[:notice] = l(:notice_successful_update) - redirect_to_project_or_global - else - render :action => 'edit' - end - end - - def destroy - @checklist_template.destroy - redirect_to_project_or_global - end - -private - def redirect_to_project_or_global - redirect_to @project ? settings_project_path(@project, :tab => 'checklist_template') : {:action => "plugin", :id => "redmine_checklists", :controller => "settings", :tab => 'checklist_templates'} - end - - def find_issue - @issue = Issue.find(params[:issue_id]) - @project = @issue.project - rescue ActiveRecord::RecordNotFound - render_404 - end - - def find_checklist_template - template_scope = ChecklistTemplate.where(:id => params[:id].to_i) - template_scope = template_scope.where('user_id = ? OR is_public = ?', User.current.id, true) unless User.current.admin? - @checklist_template = template_scope.first - raise ActiveRecord::RecordNotFound unless @checklist_template.present? - @project = @checklist_template.project - rescue ActiveRecord::RecordNotFound - render_404 - end - -end diff --git a/plugins/redmine_checklists/app/controllers/checklists_controller.rb b/plugins/redmine_checklists/app/controllers/checklists_controller.rb index 5cf9545..e760c09 100644 --- a/plugins/redmine_checklists/app/controllers/checklists_controller.rb +++ b/plugins/redmine_checklists/app/controllers/checklists_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -54,6 +54,7 @@ class ChecklistsController < ApplicationController respond_to do |format| format.api { if @checklist_item.save + recalculate_issue_ratio(@checklist_item) render :action => 'show', :status => :created, :location => checklist_url(@checklist_item) else render_validation_errors(@checklist_item) @@ -66,7 +67,8 @@ class ChecklistsController < ApplicationController @checklist_item.safe_attributes = params[:checklist] respond_to do |format| format.api { - if @checklist_item.save + if with_issue_journal { @checklist_item.save } + recalculate_issue_ratio(@checklist_item) render_api_ok else render_validation_errors(@checklist_item) @@ -77,27 +79,12 @@ class ChecklistsController < ApplicationController def done (render_403; return false) unless User.current.allowed_to?(:done_checklists, @checklist_item.issue.project) - old_checklist_items = @checklist_item.issue.checklists.to_a - @checklist_item.is_done = params[:is_done] == 'true' - if @checklist_item.save - journal = Journal.new(:journalized => @checklist_item.issue, :user => User.current) - checklist_items = Checklist.where(:issue_id => @checklist_item.issue.id).to_a - detail = JournalDetail.new( :property => 'attr', - :prop_key => 'checklist', - :old_value => old_checklist_items.to_json.to_s, - :value => checklist_items.to_json.to_s, - :journal => journal - ) - if JournalChecklistHistory.can_fixup?(detail) - JournalChecklistHistory.fixup(detail) - else - journal.details << detail - end - journal.save unless journal.destroyed? - if (Setting.issue_done_ratio == "issue_field") && RedmineChecklists.settings["issue_done_ratio"].to_i > 0 - Checklist.recalc_issue_done_ratio(@checklist_item.issue.id) - @checklist_item.issue.reload + with_issue_journal do + @checklist_item.is_done = params[:is_done] == 'true' + if @checklist_item.save + recalculate_issue_ratio(@checklist_item) + true end end respond_to do |format| @@ -121,4 +108,16 @@ class ChecklistsController < ApplicationController rescue ActiveRecord::RecordNotFound render_404 end + + def with_issue_journal(&block) + return unless yield + true + end + + def recalculate_issue_ratio(checklist_item) + if (Setting.issue_done_ratio == 'issue_field') && RedmineChecklists.issue_done_ratio? + Checklist.recalc_issue_done_ratio(checklist_item.issue.id) + checklist_item.issue.reload + end + end end diff --git a/plugins/redmine_checklists/app/helpers/checklists_helper.rb b/plugins/redmine_checklists/app/helpers/checklists_helper.rb index e1459db..16c73cb 100644 --- a/plugins/redmine_checklists/app/helpers/checklists_helper.rb +++ b/plugins/redmine_checklists/app/helpers/checklists_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -54,20 +54,5 @@ module ChecklistsHelper "" end end - def template_options_for_select(project = nil, tracker = nil) - scoped = ChecklistTemplate.visible - scoped = scoped.in_project_and_global(project) if project.present? - scoped = scoped.for_tracker_and_global(tracker) if tracker.present? - templates = scoped.eager_load(:category).to_a - without_category = templates.select{ |x| x.category.nil? }.map{ |x| [x.name, x.id, {'data-template-items' => x.template_items}] } - with_category = templates.select{ |x| x.category } - options_for_select( - [[l(:label_select_template), '']] + without_category - ) + - grouped_options_for_select( - with_category.group_by{ |x| x.category.try(:name) }. - map{ |k,v| [ k, v.map{ |x| [ x.name, x.id, {'data-template-items' => x.template_items} ] } ] } - ) - end end diff --git a/plugins/redmine_checklists/app/models/checklist.rb b/plugins/redmine_checklists/app/models/checklist.rb index 763a2d2..97b964a 100644 --- a/plugins/redmine_checklists/app/models/checklist.rb +++ b/plugins/redmine_checklists/app/models/checklist.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -53,16 +53,16 @@ class Checklist < ActiveRecord::Base rcrm_acts_as_list validates_presence_of :subject - validates_length_of :subject, :maximum => 512 + validates_length_of :subject, maximum: 512 validates_presence_of :position validates_numericality_of :position def self.recalc_issue_done_ratio(issue_id) issue = Issue.find(issue_id) - return false if (Setting.issue_done_ratio != "issue_field") || RedmineChecklists.settings["issue_done_ratio"].to_i < 1 || issue.checklists.empty? - done_checklist = issue.checklists.map{|c| c.is_done ? 1 : 0} + return false if (Setting.issue_done_ratio != 'issue_field') || !RedmineChecklists.issue_done_ratio? || issue.checklists.reject(&:is_section).empty? + done_checklist = issue.checklists.reject(&:is_section).map { |c| c.is_done ? 1 : 0 } done_ratio = (done_checklist.count(1) * 10) / done_checklist.count * 10 - issue.update_attribute(:done_ratio, done_ratio) + issue.update(done_ratio: done_ratio) end def self.old_format?(detail) @@ -70,7 +70,7 @@ class Checklist < ActiveRecord::Base (detail.value.is_a?(String) && detail.value.match(/^\[[ |x]\] .+$/).present?) end - safe_attributes 'subject', 'position', 'issue_id', 'is_done' + safe_attributes 'subject', 'position', 'issue_id', 'is_done', 'is_section' def editable_by?(usr = User.current) usr && (usr.allowed_to?(:edit_checklists, project) || (author == usr && usr.allowed_to?(:edit_own_checklists, project))) diff --git a/plugins/redmine_checklists/app/models/checklist_template.rb b/plugins/redmine_checklists/app/models/checklist_template.rb deleted file mode 100644 index 873ab64..0000000 --- a/plugins/redmine_checklists/app/models/checklist_template.rb +++ /dev/null @@ -1,62 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -class ChecklistTemplate < ActiveRecord::Base - unloadable - include Redmine::SafeAttributes - belongs_to :project - belongs_to :tracker - belongs_to :user - belongs_to :category, :class_name => "ChecklistTemplateCategory", :foreign_key => "category_id" - - validates_presence_of :name, :template_items - validates_length_of :name, :maximum => 255 - - attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 - safe_attributes 'name', 'template_items', 'project', 'user', 'category_id', 'is_public', 'is_default', 'tracker_id' - - scope :visible, lambda {|*args| - user = args.shift || User.current - base = Project.allowed_to_condition(user, :manage_checklist_templates, *args) - user_id = user.logged? ? user.id : 0 - - eager_load(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id) - } - - scope :in_project_and_global, lambda {|project| - where("#{table_name}.project_id IS NULL OR #{table_name}.project_id = 0 OR #{table_name}.project_id = ?", project) - } - - scope :for_tracker_and_global, lambda { |tracker| - where("#{table_name}.tracker_id IS NULL OR #{table_name}.tracker_id = 0 OR #{table_name}.tracker_id = ?", tracker) - } - - scope :for_tracker_id, lambda { |tracker_id| where(:tracker_id => tracker_id) } - - scope :default, lambda { where(:is_default => true) } - - def to_s - name - end - - def checklists - template_items.split("\r\n").map { |subject| Checklist.new(:subject => subject) } - end - -end diff --git a/plugins/redmine_checklists/app/models/checklist_template_category.rb b/plugins/redmine_checklists/app/models/checklist_template_category.rb deleted file mode 100644 index 1059afa..0000000 --- a/plugins/redmine_checklists/app/models/checklist_template_category.rb +++ /dev/null @@ -1,33 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -class ChecklistTemplateCategory < ActiveRecord::Base - unloadable - include Redmine::SafeAttributes - - attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 - scope :ordered, lambda { order(:position) } - safe_attributes 'name', 'position' - - rcrm_acts_as_list - - def to_s - name - end -end diff --git a/plugins/redmine_checklists/app/models/journal_checklist_history.rb b/plugins/redmine_checklists/app/models/journal_checklist_history.rb index ca66b33..76278ff 100644 --- a/plugins/redmine_checklists/app/models/journal_checklist_history.rb +++ b/plugins/redmine_checklists/app/models/journal_checklist_history.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -19,27 +19,24 @@ class JournalChecklistHistory def self.can_fixup?(journal_details) - unless journal_details.journal - return false - end + return false if journal_details.journal.nil? + issue = journal_details.journal.journalized - unless issue.is_a?(Issue) - return false - end + return false unless issue.is_a?(Issue) + prev_journal_scope = issue.journals.order('id DESC') prev_journal_scope = prev_journal_scope.where('id <> ?', journal_details.journal_id) if journal_details.journal_id prev_journal = prev_journal_scope.first - unless prev_journal - return false - end + return false unless prev_journal + return false if prev_journal.user_id != journal_details.journal.user_id return false if Time.zone.now > prev_journal.created_on + 1.minute - prev_journal.details.all?{ |x| x.prop_key == 'checklist'} && - journal_details.journal.details.all?{ |x| x.prop_key == 'checklist'} && + prev_journal.details.all? { |x| x.prop_key == 'checklist' } && + journal_details.journal.details.all? { |x| x.prop_key == 'checklist' } && journal_details.journal.notes.blank? && prev_journal.notes.blank? && - prev_journal.details.select{ |x| x.prop_key == 'checklist' }.size == 1 + prev_journal.details.select { |x| x.prop_key == 'checklist' }.size == 1 end def self.fixup(journal_details) @@ -50,11 +47,9 @@ class JournalChecklistHistory checklist_details = prev_journal.details.find{ |x| x.prop_key == 'checklist'} if new(checklist_details.old_value, journal_details.value).empty_diff? prev_journal.destroy - journal_details.journal.send(:send_checklist_notification) else - checklist_details.update_attribute(:value, journal_details.value) + checklist_details.update(value: journal_details.value) journal_details.journal.destroy unless journal_details.journal.new_record? && journal_details.journal.details.any?{ |x| x.prop_key != 'checklist'} - prev_journal.send(:send_checklist_notification) end end @@ -63,16 +58,11 @@ class JournalChecklistHistory @become = force_object(become) @was_ids = @was.map(&:id) @become_ids = @become.map(&:id) - @added_ids = @become_ids - @was_ids - @removed_ids = @was_ids - @become_ids @both_ids = @become_ids & @was_ids end def diff { - :added => @become.select{ |x| @added_ids.include? x.id }, - :removed => @was.select{ |x| @removed_ids.include? x.id }, - :renamed => renamed, :undone => undone, :done => done } @@ -116,17 +106,6 @@ class JournalChecklistHistory end end.compact end - def renamed - Hash[@both_ids.map do |id| - was_subject = was_by_id(id).subject - become_subject = become_by_id(id).subject - if was_subject != become_subject - [was_subject, become_subject] - else - nil - end - end.compact] - end def was_by_id(id) @was.find{ |x| x.id == id } diff --git a/plugins/redmine_checklists/app/views/checklist_template_categories/_form.html.erb b/plugins/redmine_checklists/app/views/checklist_template_categories/_form.html.erb deleted file mode 100644 index 1ae5418..0000000 --- a/plugins/redmine_checklists/app/views/checklist_template_categories/_form.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= error_messages_for 'category' %> - -<div class="box tabular"> -<p><label for="category_name"><%=l(:field_name)%><span class="required"> *</span></label> -<%= text_field 'category', 'name' %></p> -</div> diff --git a/plugins/redmine_checklists/app/views/checklist_template_categories/edit.html.erb b/plugins/redmine_checklists/app/views/checklist_template_categories/edit.html.erb deleted file mode 100644 index d8b7f0f..0000000 --- a/plugins/redmine_checklists/app/views/checklist_template_categories/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%= link_to l(:label_checklist_template_category_plural), :action =>"plugin", :id => "redmine_checklists", :controller => "settings", :tab => 'checklist_template_categories' %> » <%= @category.name %></h2> - -<%= form_tag({:action => 'update', :id => @category}, :class => "tabular edit_checklist_template_category", :method => :put) do %> - <%= render :partial => 'form' %> - <%= submit_tag l(:button_save) %> -<% end %> diff --git a/plugins/redmine_checklists/app/views/checklist_template_categories/new.html.erb b/plugins/redmine_checklists/app/views/checklist_template_categories/new.html.erb deleted file mode 100644 index 0a30b9d..0000000 --- a/plugins/redmine_checklists/app/views/checklist_template_categories/new.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%= link_to l(:label_checklist_template_category_plural), :action =>"plugin", :id => "redmine_checklists", :controller => "settings", :tab => 'checklist_template_categories' %> » <%=l(:label_checklist_template_category_new)%></h2> - -<%= form_tag({:action => 'create'}, :class => "tabular new_checklist_template_category") do %> - <%= render :partial => 'form' %> - <%= submit_tag l(:button_create) %> -<% end %> diff --git a/plugins/redmine_checklists/app/views/checklist_templates/_form.html.erb b/plugins/redmine_checklists/app/views/checklist_templates/_form.html.erb deleted file mode 100644 index dc1e998..0000000 --- a/plugins/redmine_checklists/app/views/checklist_templates/_form.html.erb +++ /dev/null @@ -1,59 +0,0 @@ -<%= error_messages_for 'checklist_template' %> - -<div class="box tabular"> - <p><%= f.text_field :name, :size => 80, :required => true %></p> - <% if ChecklistTemplateCategory.any? %> - <p> - <%= f.select :category_id, options_for_select([['', nil]] + ChecklistTemplateCategory.all.map{ |x| [x.name, x.id]}, f.object.category_id) %> - </p> - <% end %> - - <% if User.current.admin? || User.current.allowed_to?(:manage_checklist_templates, @project) %> - <p><label><%=l(:field_visible)%></label> - <label class="block"> - <%= f.radio_button :is_public, 0, :checked => !@checklist_template.is_public?, - :onchange => (User.current.admin? ? nil : '$("#checklist_template_is_for_all").removeAttr("disabled");') %> - <%= l(:label_visibility_private) %> - </label> - <label class="block"> - <%= f.radio_button :is_public, 1, :checked => @checklist_template.is_public?, - :onchange => (User.current.admin? ? nil : '$("#checklist_template_is_for_all").removeAttr("checked"); $("#checklist_template_is_for_all").attr("disabled", true);') %> - <%= l(:label_visibility_public) %> - </label> - </p> - <% end %> - - <p><label for="checklist_template_is_for_all"><%=l(:field_is_for_all)%></label> - <%= check_box_tag 'checklist_template_is_for_all', 1, @checklist_template.project.nil?, - :disabled => (!@checklist_template.new_record? && (@checklist_template.project.nil? || (@checklist_template.is_public? && !User.current.admin?)) || @project.nil? ) %></p> - - <p id='is_default_block'><%= f.check_box :is_default, :label => l(:label_checklist_is_default) %></p> - - <p> - <%= f.select :tracker_id, options_from_collection_for_select(@project ? @project.trackers : Tracker.all, :id, :name, f.object.tracker_id), :include_blank => true, :label => l(:label_tracker) %> - </p> - - <p><%= f.text_area :template_items, :required => true, :rows => 5 %></p> - <p> - <em class='info'> - <%= l(:label_checklists_description) %> - </em> - </p> -</div> - -<%= javascript_tag do %> - function isDefaultToggle(element){ - if (element.prop('checked') == true) { - $('#is_default_block').hide(); - $('#checklist_template_is_default').prop('checked', false); - } else { - $('#is_default_block').show(); - } - } - - $('#checklist_template_is_for_all').change(function(){ - isDefaultToggle($(this)); - }); - - isDefaultToggle($('#checklist_template_is_for_all')); -<% end%> diff --git a/plugins/redmine_checklists/app/views/checklist_templates/edit.html.erb b/plugins/redmine_checklists/app/views/checklist_templates/edit.html.erb deleted file mode 100644 index a7e2e7a..0000000 --- a/plugins/redmine_checklists/app/views/checklist_templates/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%=l(:label_checklist_template)%></h2> - -<%= labelled_form_for :checklist_template, @checklist_template, :url => { :action => 'update', :project_id => @project } do |f| %> -<%= render :partial => 'checklist_templates/form', :locals => { :f => f } %> -<%= submit_tag l(:button_save) %> -<% end %> diff --git a/plugins/redmine_checklists/app/views/checklist_templates/new.html.erb b/plugins/redmine_checklists/app/views/checklist_templates/new.html.erb deleted file mode 100644 index 6c646f9..0000000 --- a/plugins/redmine_checklists/app/views/checklist_templates/new.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<h2><%=l(:label_checklist_new_checklist_template)%></h2> - -<%= labelled_form_for :checklist_template, @checklist_template, :url => { :action => 'create', :project_id => @project } do |f| %> -<%= render :partial => 'checklist_templates/form', :locals => { :f => f } %> -<%= submit_tag l(:button_create) %> -<% end %> diff --git a/plugins/redmine_checklists/app/views/checklists/_checklist_item.html.erb b/plugins/redmine_checklists/app/views/checklists/_checklist_item.html.erb index 19d094a..0855cdb 100644 --- a/plugins/redmine_checklists/app/views/checklists/_checklist_item.html.erb +++ b/plugins/redmine_checklists/app/views/checklists/_checklist_item.html.erb @@ -1,8 +1,11 @@ -<li id="checklist_item_<%= checklist_item.id %>" <%= "class=is-done-checklist-item" if checklist_item.is_done %> > - <%= check_box_tag 'checklist_item', "", checklist_item.is_done, - :disabled => !User.current.allowed_to?(:done_checklists, checklist_item.issue.project) && !User.current.allowed_to?(:edit_checklists, checklist_item.issue.project), - :data_url => url_for( {:controller => "checklists", :action => "done", :id => checklist_item.id} ), :class => 'checklist-checkbox' - %> +<li id="checklist_item_<%= checklist_item.id %>" class="<%= 'is-done-checklist-item' if checklist_item.is_done %> <%= 'checklist-section' if checklist_item.is_section %>"> + <% unless checklist_item.is_section %> + <%= check_box_tag 'checklist_item', '', checklist_item.is_done, + disabled: !User.current.allowed_to?(:done_checklists, checklist_item.issue.project) && !User.current.allowed_to?(:edit_checklists, checklist_item.issue.project), + data_url: url_for({ controller: 'checklists', action: 'done', id: checklist_item.id }), + class: 'checklist-checkbox', + id: "checklist-checkbox-#{checklist_item.id}" + %> + <% end %> <%= textilizable(checklist_item, :subject).gsub(/<\/?(p|h\d+|li|ul)>/, '').strip.html_safe %> - </li> diff --git a/plugins/redmine_checklists/app/views/checklists/done.js.erb b/plugins/redmine_checklists/app/views/checklists/done.js.erb index 9fc91bd..ac8b878 100644 --- a/plugins/redmine_checklists/app/views/checklists/done.js.erb +++ b/plugins/redmine_checklists/app/views/checklists/done.js.erb @@ -2,3 +2,16 @@ $("#checklist_item_<%= @checklist_item.id %>").toggleClass('is-done-checklist-it $('#checklist_form .checklist-item#<%= @checklist_item.id %> input[type=checkbox]').trigger('click'); $('.issue .attributes table.progress').parent().html('<%= j(progress_bar(@checklist_item.issue.done_ratio, :width => '80px', :legend => "#{@checklist_item.issue.done_ratio}%")) %>'); $('#issue_done_ratio').val('<%= @checklist_item.issue.done_ratio %>'); + +var checkedCheckboxes = $('#checklist_items .checklist-checkbox:checkbox:checked'); + +if(localStorage.getItem("hide_closed_checklists") === '0' && checkedCheckboxes.length > 0){ + $("#checklist_item_<%= @checklist_item.id %>").fadeOut(1000).hide(15); + $('#switch_link').text('<%= l("label_checklist_show_closed") %>' + '(' + checkedCheckboxes.length + ')'); +} +if(checkedCheckboxes.length < 1 && localStorage.getItem("hide_closed_checklists") === '1'){ + $('#switch_link').hide(); +} +if(checkedCheckboxes.length > 0){ + $('#switch_link').show(); +} diff --git a/plugins/redmine_checklists/app/views/issues/_checklist.html.erb b/plugins/redmine_checklists/app/views/issues/_checklist.html.erb index edf30ee..9ef810f 100644 --- a/plugins/redmine_checklists/app/views/issues/_checklist.html.erb +++ b/plugins/redmine_checklists/app/views/issues/_checklist.html.erb @@ -1,7 +1,9 @@ <% if !@issue.blank? && @issue.checklists.any? && User.current.allowed_to?(:view_checklists, @project) %> <hr /> <div id="checklist"> - + <div class="contextual"> + <%= link_to l("label_checklist_hide_closed"), '#', id: 'switch_link' %> + </div> <p><strong><%=l(:label_checklist_plural)%></strong></p> <ul id="checklist_items"> @@ -11,6 +13,8 @@ <% end %> </ul> </div> - - + <%= javascript_tag do %> + new Redmine.ChecklistToggle('<%= l("label_checklist_show_closed") %>', '<%= l("label_checklist_hide_closed") %>'); + $("#checklist_items").checklist(); + <% end %> <% end %> diff --git a/plugins/redmine_checklists/app/views/issues/_checklist_fields.html.erb b/plugins/redmine_checklists/app/views/issues/_checklist_fields.html.erb index 92dd220..f0daba9 100644 --- a/plugins/redmine_checklists/app/views/issues/_checklist_fields.html.erb +++ b/plugins/redmine_checklists/app/views/issues/_checklist_fields.html.erb @@ -1,21 +1,26 @@ -<span class="checklist-item <%= new_or_show(f) %>" id = "<%=f.object.id%>"> - <span class = "checklist-show-only checklist-checkbox"><%= f.check_box :is_done %></span> - <span class = "checklist-show checklist-subject <%= done_css(f) %>"> - <%= f.object.subject %> +<span class="checklist-item <%= new_or_show(f) %> <%= 'checklist-section' if f.object.is_section %> existing" id="<%= f.object.id %>"> + <% unless f.object.is_section %> + <span class="checklist-show-only checklist-checkbox"><%= f.check_box :is_done %></span> + <% end %> + + <span class="checklist-show checklist-subject <%= done_css(f) %>"> + <%= textilizable(f.object, :subject).gsub(/<\/?(p|h\d+|li|ul)>/, '').strip.html_safe %> </span> - <span class = "checklist-edit checklist-new checklist-edit-box"> - <%= text_field_tag nil, f.object.subject, :class => 'edit-box'%> - <%= f.hidden_field :subject, :class => 'checklist-subject-hidden' %> + + <span class="checklist-edit checklist-new checklist-edit-box"> + <%= text_field_tag nil, f.object.subject, class: 'edit-box' %> + <%= f.hidden_field :subject, class: 'checklist-subject-hidden' %> </span> - <span class= "checklist-edit-only checklist-edit-save-button"><%= submit_tag l(:button_save), :type => "button", :class => "item item-save small"%> </span> - <span class= "checklist-edit-only checklist-edit-reset-button"><% concat l(:button_cancel) - %> </span> - <span class = "checklist-show-only checklist-remove"><%= link_to_remove_checklist_fields "", f, - :class => "icon icon-del" %></span> - <%= f.hidden_field :position, :class => 'checklist-item-position' %> - <%= f.hidden_field :id, :class => 'checklist-item-id' %> - <span class = "icon icon-add checklist-new-only save-new-by-button"></span> + + <span class="checklist-edit-only checklist-edit-save-button"><%= submit_tag l(:button_save), type: 'button', class: 'item item-save small' %> </span> + <span class="checklist-edit-only checklist-edit-reset-button"><% concat l(:button_cancel) %> </span> + <span class="checklist-show-only checklist-remove"><%= link_to_remove_checklist_fields "", f, class: 'icon icon-del' %></span> + + <%= f.hidden_field :position, class: 'checklist-item-position' %> + <%= f.hidden_field :is_section, class: 'checklist-item-is_section' %> + <%= f.hidden_field :id, class: 'checklist-item-id' %> + + <span class="icon icon-add checklist-new-only save-new-by-button"></span> <br> </span> - diff --git a/plugins/redmine_checklists/app/views/issues/_checklist_form.html.erb b/plugins/redmine_checklists/app/views/issues/_checklist_form.html.erb index e7f8546..0b5ab22 100644 --- a/plugins/redmine_checklists/app/views/issues/_checklist_form.html.erb +++ b/plugins/redmine_checklists/app/views/issues/_checklist_form.html.erb @@ -4,12 +4,10 @@ <label><%=l(:label_checklist_plural)%></label> <% @issue.checklists.build if @issue.checklists.blank? || @issue.checklists.last.subject.present? %> <%= fields_for :issue, issue do |f| -%> - <%= hidden_field_tag 'issue[checklist_template_id]', params[:issue][:checklist_template_id] if params[:issue] %> <span id="checklist_form_items" data-checklist-fields='<%= fields(f, :checklists) %>'> <%= f.fields_for :checklists do |builder| %> <%= render :partial => 'checklist_fields', :locals => {:f => builder, :checklist => @checklist} %> <% end %> - <%= render 'checklist_templates' if ChecklistTemplate.visible.in_project_and_global(@project).for_tracker_and_global(@issue.tracker).any? %> </span> <% end %> </p> @@ -22,5 +20,4 @@ <% end %> $("span#checklist_form_items").checklist(); - $("#checklist_items").checklist(); <% end %> diff --git a/plugins/redmine_checklists/app/views/issues/_checklist_templates.html.erb b/plugins/redmine_checklists/app/views/issues/_checklist_templates.html.erb deleted file mode 100644 index fe35508..0000000 --- a/plugins/redmine_checklists/app/views/issues/_checklist_templates.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<span class='search_for_watchers template-wrapper'> - <%= link_to l(:label_add_checklists_from_template), 'javascript:void(0)', :id => 'template-link' %> -</span> -<%= select :checklist, :template, template_options_for_select(@project, @issue.tracker), {}, :style => 'display: none' %> diff --git a/plugins/redmine_checklists/app/views/projects/settings/_checklist_templates.html.erb b/plugins/redmine_checklists/app/views/projects/settings/_checklist_templates.html.erb deleted file mode 100644 index 19ba0c0..0000000 --- a/plugins/redmine_checklists/app/views/projects/settings/_checklist_templates.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<% @checklist_templates = ChecklistTemplate.visible.in_project_and_global(@project).order("#{ChecklistTemplate.table_name}.name") %> -<% if @checklist_templates %> - <table class="list"> - <thead> - <tr> - <th><%= l(:field_name) %></th> - <th><%= "#{l(:field_visible)} #{l(:label_visibility_public)}" %></th> - <th><%= l(:field_is_for_all) %></th> - <th><%= l(:field_is_for_tracker) %></th> - <th></th> - </tr> - </thead> - <tbody> - <% @checklist_templates.each do |checklist_template| %> - <tr class="checklist-template <%= cycle 'odd', 'even' %>"> - <td class="name"><%= checklist_template.name %></td> - <td class="tick"><%= checked_image checklist_template.is_public? %></td> - <td class="tick"><%= checked_image checklist_template.project.blank? %></td> - <td><%= checklist_template.tracker %></td> - <td class="buttons"> - <% if User.current.admin? || User.current == checklist_template.user || User.current.allowed_to?(:manage_checklist_templates, checklist_template.project) %> - <%= link_to l(:button_edit), edit_project_checklist_template_path(@project, checklist_template), :class => 'icon icon-edit' %> - <%= delete_link checklist_template_path(checklist_template, :project_id => @project) %> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> -<% else %> - <p class="nodata"><%= l(:label_no_data) %></p> -<% end %> - -<p><%= link_to l(:label_checklist_new_checklist_template), new_project_checklist_template_path(@project), :class => 'icon icon-add' if User.current.allowed_to?(:manage_checklist_templates, @project) %></p> diff --git a/plugins/redmine_checklists/app/views/settings/checklists/_checklists.html.erb b/plugins/redmine_checklists/app/views/settings/checklists/_checklists.html.erb index f59936b..d4307f4 100644 --- a/plugins/redmine_checklists/app/views/settings/checklists/_checklists.html.erb +++ b/plugins/redmine_checklists/app/views/settings/checklists/_checklists.html.erb @@ -1,11 +1,5 @@ <% checklist_tabs = [ {:name => 'general', :partial => 'settings/checklists/general', :label => :label_general}] - checklist_tabs.push({:name => 'checklist_templates', - :partial => 'settings/checklists/templates', - :label => :label_checklist_templates}) - checklist_tabs.push({:name => 'checklist_template_categories', - :partial => 'settings/checklists/template_categories', - :label => :label_checklist_template_category_plural}) %> diff --git a/plugins/redmine_checklists/app/views/settings/checklists/_template_categories.html.erb b/plugins/redmine_checklists/app/views/settings/checklists/_template_categories.html.erb deleted file mode 100644 index 5853c1c..0000000 --- a/plugins/redmine_checklists/app/views/settings/checklists/_template_categories.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<div class="contextual"> -<%= link_to l(:label_checklist_template_category_new), {:controller => "checklist_template_categories", :action => 'new'}, :class => 'icon icon-add' %> -</div> - -<h3><%=l(:label_checklist_template_category_plural)%></h3> - -<table class="list"> - <thead><tr> - <th><%=l(:field_checklist_template_category)%></th> - <th><%=l(:button_sort)%></th> - <th></th> - </tr></thead> - <tbody> - <% for category in ChecklistTemplateCategory.ordered %> - <tr class="<%= cycle("odd", "even") %>"> - <td class="name"><%= category.name %></td> - <td align="center" style="width:15%;"><%= stocked_reorder_link(category, 'category', {:controller => "checklist_template_categories", :action => 'update', :id => category}, :put) %></td> - <td class="buttons"> - <%= link_to l(:button_edit), edit_checklist_template_category_path(category), :class => 'icon icon-edit' %> - <%= delete_link checklist_template_category_path(category) %> - </td> - </tr> - <% end %> - </tbody> -</table> - -<%= javascript_tag do %> - $(function() { $("table.list tbody").positionedItems(); }); -<% end %> diff --git a/plugins/redmine_checklists/app/views/settings/checklists/_templates.html.erb b/plugins/redmine_checklists/app/views/settings/checklists/_templates.html.erb deleted file mode 100644 index 8a1b487..0000000 --- a/plugins/redmine_checklists/app/views/settings/checklists/_templates.html.erb +++ /dev/null @@ -1,44 +0,0 @@ -<div class='contextual'> - <%= link_to l(:label_checklist_new_checklist_template), new_checklist_template_path, :class => 'icon icon-add' if User.current.allowed_to?(:manage_checklist_templates, nil, { :global => true }) %> -</div> - -<h3><%=l(:label_checklist_templates)%></h3> - -<% if ChecklistTemplate.any? %> - <table class="list"> - <thead><tr> - <th><%= l(:field_name) %></th> - <th><%= "#{l(:field_visible)} #{l(:label_visibility_public)}" %></th> - <th><%= l(:field_project) %></th> - <th></th> - </tr></thead> - <tbody> - <% previous_group = false %> - <% ChecklistTemplate.eager_load(:category).each do |checklist_template| %> - <% if checklist_template.category != previous_group %> - <% reset_cycle %> - <% unless checklist_template.category.blank? %> - <tr class="group open"> - <td colspan="4"> - <span class="expander" onclick="toggleRowGroup(this);"> </span> - <%= checklist_template.category.name %> - </td> - </tr> - <% end %> - <% previous_group = checklist_template.category %> - <% end %> - <tr class="checklist-template <%= cycle 'odd', 'even' %>"> - <td class="name"><%= checklist_template.name %></td> - <td class="tick"><%= checked_image checklist_template.is_public? %></td> - <td class="project"><%= checklist_template.project ? checklist_template.project.name : l(:field_is_for_all) %></td> - <td class="buttons"> - <%= link_to l(:button_edit), edit_checklist_template_path(checklist_template), :class => 'icon icon-edit' %> - <%= delete_link checklist_template_path(checklist_template, :project_id => checklist_template.project) %> - </td> - </tr> - <% end %> - </tbody> - </table> -<% else %> - <p class="nodata"><%= l(:label_no_data) %></p> -<% end %> diff --git a/plugins/redmine_checklists/assets/javascripts/checklists.js b/plugins/redmine_checklists/assets/javascripts/checklists.js index 1bdb224..828ac52 100644 --- a/plugins/redmine_checklists/assets/javascripts/checklists.js +++ b/plugins/redmine_checklists/assets/javascripts/checklists.js @@ -99,6 +99,12 @@ if(typeof(String.prototype.trim) === "undefined") })( jQuery ); +var updateChecklistPositions = function() { + $(".checklist-item.existing").each(function(index, element){ + $(element).children('.checklist-item-position').val(index); + }); +} + var Redmine = Redmine || {}; Redmine.Checklist = $.klass({ @@ -110,15 +116,12 @@ Redmine.Checklist = $.klass({ event.returnValue = false }, - addChecklistFields: function(templateDiv) { + addChecklistFields: function() { var new_id = new Date().getTime(); var regexp = new RegExp("new_checklist", "g"); - if (templateDiv) { - appended = $(this.content.replace(regexp, new_id)).insertBefore(templateDiv) - } else { - appended = $(this.content.replace(regexp, new_id)).appendTo(this.root) - } - appended.find('.edit-box').focus() + appended = $(this.content.replace(regexp, new_id)).appendTo(this.root); + updateChecklistPositions(); + appended.find('.edit-box').focus(); }, findSpan: function(event) { @@ -129,13 +132,16 @@ Redmine.Checklist = $.klass({ return elem.prevAll('span.checklist-item.new') }, - transformItem: function(event, elem, valueToSet) { + transformItem: function(event, elem, valueToSet, isSection) { var checklistItem; if (event) { checklistItem = this.findSpan(event) - } else { + } else if (elem) { checklistItem = this.findSpanBefore(elem) + } else { + checklistItem = this.root.find('span.checklist-item.new') } + var val; if (valueToSet) { val = valueToSet @@ -143,11 +149,17 @@ Redmine.Checklist = $.klass({ } else { val = checklistItem.find('input.edit-box').val() } + checklistItem.find('.checklist-subject').text(val) checklistItem.find('.checklist-subject-hidden').val(val) checklistItem.removeClass('edit') checklistItem.removeClass('new') checklistItem.addClass('show') + + if (isSection) { + checklistItem.addClass('checklist-section'); + checklistItem.children('.checklist-item-is_section').val(true); + } }, resetItem: function(item) { @@ -157,12 +169,9 @@ Redmine.Checklist = $.klass({ }, addChecklistItem: function(event) { - this.preventEvent(event) - this.transformItem(event) - if ($('.template-wrapper').length) - this.addChecklistFields($('.template-wrapper')) - else - this.addChecklistFields() + this.preventEvent(event); + this.transformItem(event); + this.addChecklistFields(); }, canSave: function(span) { @@ -193,6 +202,57 @@ Redmine.Checklist = $.klass({ }, this)) }, + onClickAddChecklistItemMenuButton: function() { + $('#checklist-menu .add-checklist-item').on('click', $.proxy(function(event) { + this.preventEvent(event); + var span = $('#checklist_form_items > span.checklist-item.new'); + if (this.canSave(span)) { + this.transformItem(); + this.addChecklistFields(); + this.$plusButtonMenu.hide(); + } + }, this)) + }, + + onClickNewSectionMenuButton: function() { + $('#checklist-menu .add-checklist-section').on('click', $.proxy(function(event) { + this.preventEvent(event); + var span = $('#checklist_form_items > span.checklist-item.new'); + if (this.canSave(span)) { + this.transformItem(null, null, null, true); + this.addChecklistFields(); + this.$plusButtonMenu.hide(); + } + }, this)) + }, + + onMouseEnterLeavePlusButton: function() { + var hideMenuTimer; + var $menu = this.$plusButtonMenu; + + this.root.on('mouseenter', '.save-new-by-button', function() { + var $plusButton = $(this); + var position = $plusButton.position(); + $menu.css('left', (position.left + 'px')); + $menu.css('top', (position.top + $plusButton.height() + 'px')); + $menu.show(); + }); + + this.root.on('mouseleave', '.save-new-by-button', function() { + hideMenuTimer = setTimeout(function() { + $menu.hide(); + }, 500); + }); + + $('#checklist-menu').on('mouseenter', function() { + clearTimeout(hideMenuTimer); + }); + + $('#checklist-menu').on('mouseleave', function() { + $menu.hide(); + }); + }, + onIssueFormSubmitRemoveEmptyChecklistItems: function() { $('body').on('submit', '#issue-form', function(){ $('.checklist-subject-hidden').each(function(i, elem) { @@ -212,14 +272,16 @@ Redmine.Checklist = $.klass({ if (checkbox.val() === "false") { checkbox.val("1"); + itemToRemove.removeClass('existing') itemToRemove.fadeOut(200); } + + updateChecklistPositions(); }, this)); }, makeChecklistsSortable: function() { $('#checklist_form_items').sortable({ - revert: true, items: '.checklist-item.show', helper: "clone", stop: function (event, ui) { @@ -229,9 +291,7 @@ Redmine.Checklist = $.klass({ if (ui.item.hasClass("edit-active")) { $( this ).sortable( "cancel" ); } - $(".checklist-item").each(function(index, element){ - $(element).children('.checklist-item-position').val(index); - }); + updateChecklistPositions(); } }); }, @@ -269,13 +329,33 @@ Redmine.Checklist = $.klass({ }, onChangeCheckbox: function(){ - this.root.on('change', 'input.checklist-checkbox', $.proxy(function(event){ + this.root.on('change', 'input.checklist-checkbox', $.proxy(function(event) { + this.darkenCompletedSections(); checkbox = $(event.target) url = checkbox.attr('data_url') $.ajax({type: "PUT", url: url, data: { is_done: checkbox.prop('checked') }, dataType: 'script'}) }, this)) }, + darkenCompletedSections: function() { + var isCompletedSection = true; + var reversedChecklistItems = $('#checklist_items li').get().reverse(); + + $(reversedChecklistItems).each(function(index, element) { + var $element = $(element); + if ($element.hasClass('checklist-section')) { + if (isCompletedSection) { + $element.addClass('completed-section') + } else { + $element.removeClass('completed-section') + } + isCompletedSection = true; + } else { + isCompletedSection = isCompletedSection && $element.children('.checklist-checkbox').is(':checked') + } + }) + }, + enableUniquenessValidation: function() { this.root.on('keyup', 'input.edit-box', $.proxy(function(event) { value = $(event.target).val() @@ -307,39 +387,39 @@ Redmine.Checklist = $.klass({ }, assignTemplateSelectedEvent: function() { - var item; - this.root.on('change', '#checklist_template', $.proxy(function(){ - value = $('#checklist_template').val() - selected = $('#checklist_template option[value='+value+']').data('template-items') - items = selected.split(/\n/) - for(i = 0; i<items.length; i++) - { - item = items[i] - if (!this.hasAlreadyChecklistWithName(item)) - { - this.transformItem(null, $('#checklist_template'), item) - this.addChecklistFields($('#template-link').closest('span')) + this.$plusButtonMenu.on('click', 'li a.checklist-template', $.proxy(function(event) { + this.preventEvent(event); + items = $(event.target).data('template-items').split(/\n/); + for(var i = 0; i < items.length; i++) { + var item = items[i]; + var isSection = item.slice(0, 2) === '--'; + if (isSection) { item = item.slice(2) } + if (!this.hasAlreadyChecklistWithName(item)) { + this.transformItem(null, null, item, isSection); + this.addChecklistFields(); } } - $('#checklist_template').val('') - $('#template-link').show() - $('#checklist_template').hide() - }, this)) }, - clickSelectTemplateLink: function() { - this.root.on('click', '#template-link', function(){ - $('#template-link').hide() - $('#checklist_template').show() - }) - }, - init: function(element) { this.root = element this.content = element.data('checklist-fields') this.onEnterInNewChecklistItemForm() this.onClickPlusInNewChecklistItem() + + if (this.content) { + this.$plusButtonMenu = $('#checklist-menu').menu(); + if (this.$plusButtonMenu.length > 0) { + this.onMouseEnterLeavePlusButton(); + this.onClickAddChecklistItemMenuButton(); + this.assignTemplateSelectedEvent(); + this.onClickNewSectionMenuButton(); + } + } else { + this.darkenCompletedSections() + } + this.onIssueFormSubmitRemoveEmptyChecklistItems() this.onChecklistRemove() this.makeChecklistsSortable() @@ -347,12 +427,59 @@ Redmine.Checklist = $.klass({ this.onCheckboxChanged() this.onChangeCheckbox() this.enableUniquenessValidation() - this.assignTemplateSelectedEvent() - this.clickSelectTemplateLink() } }) $.fn.checklist = function(element){ new Redmine.Checklist(this); -} +}; + +Redmine.ChecklistToggle = $.klass({ + manageToggling: function (t_val) { + var checkedCheckboxes = $('#checklist_items .checklist-checkbox:checkbox:checked'); + + if(localStorage.getItem("hide_closed_checklists") === t_val){ + $($(checkedCheckboxes).closest('li')).hide(); + $(this.switch_link).text(this.show_text + '(' + checkedCheckboxes.length + ')'); + } else { + $($(checkedCheckboxes).closest('li')).show(); + $(this.switch_link).text(this.hide_text); + } + }, + switch_link_click: function(){ + var th = $(this)[0]; + this.switch_link.click(function (e) { + e.preventDefault(); + th.manageToggling("1"); + var setVal = (localStorage.getItem("hide_closed_checklists") === "1") ? "0" : "1"; + localStorage.setItem("hide_closed_checklists", setVal); + }); + }, + hide_switch_link: function(){ + if($('.checklist-checkbox:checkbox:checked').length < 1){ + this.switch_link.hide(); + } + }, + init: function(show_text, hide_text) { + this.show_text = show_text; + this.hide_text = hide_text; + this.switch_link = $('#switch_link'); + this.manageToggling("0"); + this.switch_link_click(); + this.hide_switch_link(); + } +}); + + +$(document).ready(function () { + if (typeof(contextMenuCheckSelectionBox) === 'function') { + var originContextMenuCheckSelectionBox = contextMenuCheckSelectionBox; + contextMenuCheckSelectionBox = function (tr, checked) { + var $td = tr.find('td.checklist_relations'); + var $checklist = $td.find('.checklist').detach(); + originContextMenuCheckSelectionBox(tr, checked); + $checklist.appendTo($td); + }; + } +}); diff --git a/plugins/redmine_checklists/assets/stylesheets/checklists.css b/plugins/redmine_checklists/assets/stylesheets/checklists.css index 030318f..a254bda 100644 --- a/plugins/redmine_checklists/assets/stylesheets/checklists.css +++ b/plugins/redmine_checklists/assets/stylesheets/checklists.css @@ -1,3 +1,7 @@ +#checklist_form_items input[type="checkbox"], +#checklist_items input[type="checkbox"] { + height: initial; +} div#checklist ul { list-style: none; @@ -10,6 +14,8 @@ div#checklist li { margin-left: 10px; } +.checklist-checkbox {height: inherit} + #checklist li:hover a.delete {opacity: 1;} #checklist a.delete {opacity: 0.4;} @@ -74,4 +80,65 @@ span.checklist-item.edit .checklist-show-only { div#checklist ol { display: inline-block; padding-left: 0; -} \ No newline at end of file +} + +.checklist-section { + padding-top: 10px; + font-weight: bold; + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: #eee; +} + +.checklist-item.checklist-section > .checklist-checkbox { display: none; } + +#checklist_items li.checklist-section { + padding-bottom: 5px; + margin-bottom: 5px; +} + +.completed-section { color: #999; } + +.save-new-by-button { cursor: pointer; } + +table.list td.checklist_relations { text-align: left } + +/* ========================================================================= */ +/* Checklist context menu */ +/* ========================================================================= */ + +#checklist-menu ul, #checklist-menu li, #checklist-menu a { + display:block; + margin:0; + padding:0; + border:0; +} + +#checklist-menu { + display: none; + position: absolute; + font-size: 0.9em; +} + +#checklist-menu, #checklist-menu ul { + width: 150px; + border: 1px solid #ccc; + background: white; + list-style: none; + padding: 2px; + border-radius: 2px; +} + +#checklist-menu li { + position: relative; + padding: 1px; + border: 1px solid white; +} + +#checklist-menu a { + text-decoration: none !important; + padding: 2px 0px 2px 20px; +} + +#checklist-menu a:hover { color:#2A5685; } +#checklist-menu li:hover { border:1px solid #628db6; background-color:#eef5fd; border-radius:3px; } diff --git a/plugins/redmine_checklists/config/locales/de.yml b/plugins/redmine_checklists/config/locales/de.yml index 4a90806..ac85201 100644 --- a/plugins/redmine_checklists/config/locales/de.yml +++ b/plugins/redmine_checklists/config/locales/de.yml @@ -25,5 +25,15 @@ de: label_checklist_changed_to: zu label_checklist_added: hinzugefĂĽgt label_checklist_done: als Erledigt markiert - label_checklist_undone: als Nicht Erledigt markiert - label_checklist_updated: Checklisten-Eintrag editiert \ No newline at end of file + label_checklist_undone: als Nicht erledigt markiert + label_checklist_updated: Checklisten-Eintrag editiert + label_checklist_status: Checklisten-Status + label_checklist_status_done: Erledigt + label_checklist_status_undone: Nicht erledigt + label_checklist_is_default: Standard + field_is_for_tracker: Tracker + label_checklists_must_be_completed: "Alle Checklisten-Einträge eines Tickets mĂĽssen vor dem SchlieĂźen erledigt werden." + label_checklist_block_issue_closing: "SchlieĂźen des Tickets blockieren" + label_checklist_show_closed: Zeige alle + label_checklist_hide_closed: Verberge geschlossene + label_checklist_new_section: Neue Unterteilung diff --git a/plugins/redmine_checklists/config/locales/en.yml b/plugins/redmine_checklists/config/locales/en.yml index 4052f03..b60cd28 100644 --- a/plugins/redmine_checklists/config/locales/en.yml +++ b/plugins/redmine_checklists/config/locales/en.yml @@ -1,5 +1,9 @@ # English strings go here for Rails i18n en: + activerecord: + attributes: + checklists: + subject: Checklist subject label_checklist_plural: Checklist field_checklist: Checklist label_checklist_save_log: Save changes to issue log @@ -15,10 +19,12 @@ en: field_template_items: Template items label_checklist_template: Checklist template label_add_checklists_from_template: Add from template + label_checklists_from_template: From template label_select_template: "-- Select template --" label_checklist_category_not_specified: "-- Not specified --" label_checklists_description: 'Multiple values allowed (one line for each value)' label_checklist_item: Checklist item + label_checklist_section: Checklist section label_checklist_deleted: deleted label_checklist_changed_from: changed from label_checklist_changed_to: to @@ -29,8 +35,10 @@ en: label_checklist_status: Checklist status label_checklist_status_done: Done label_checklist_status_undone: Undone - label_checklist_status: Checklist status label_checklist_is_default: Default field_is_for_tracker: Tracker label_checklists_must_be_completed: All checklists of an issue must be done before closing - label_checklist_block_issue_closing: Block issue closing \ No newline at end of file + label_checklist_block_issue_closing: Block issue closing + label_checklist_show_closed: Show closed + label_checklist_hide_closed: Hide closed + label_checklist_new_section: New section diff --git a/plugins/redmine_checklists/config/locales/fr.yml b/plugins/redmine_checklists/config/locales/fr.yml old mode 100755 new mode 100644 index 6a1ed72..5c1fe44 --- a/plugins/redmine_checklists/config/locales/fr.yml +++ b/plugins/redmine_checklists/config/locales/fr.yml @@ -2,3 +2,8 @@ fr: label_checklist_plural: Liste de Tâches field_checklist: Tâche + label_checklist_templates: Template de checklists + label_checklist_new_checklist_template: Ajouter un template + field_is_for_tracker: + label_checklist_is_default: Checklist par d faut + field_template_items: ElĂ©ments diff --git a/plugins/redmine_checklists/config/locales/pt-BR.yml b/plugins/redmine_checklists/config/locales/pt-BR.yml index 0991c43..b185ee7 100644 --- a/plugins/redmine_checklists/config/locales/pt-BR.yml +++ b/plugins/redmine_checklists/config/locales/pt-BR.yml @@ -1,9 +1,44 @@ #Portuguese Brazilian strings go here for Rails i18n pt-BR: + activerecord: + attributes: + checklists: + subject: Checklist conteĂşdo label_checklist_plural: Checklist field_checklist: Checklist - label_checklist_save_log: Salvar alterações nos logs das tarefas - label_checklist_done_ratio: Definir % terminando quando desmarcar o item + label_checklist_save_log: Salvar alterações nas notas + label_checklist_done_ratio: Atribuir % conclusĂŁo permission_view_checklists: Ver checklist - permission_done_checklists: Remover itens do checklist - permission_edit_checklists: Alterar itens do checklist + permission_done_checklists: Prontos do checklist + permission_edit_checklists: Editar checklist + label_checklist_template_category_plural: Categoria de templates + label_checklist_template_category_new: Nova categoria + field_checklist_template_category: Categoria + label_checklist_templates: Checklist templates + label_checklist_new_checklist_template: Novo checklist template + field_template_items: Template items + label_checklist_template: Checklist template + label_add_checklists_from_template: Adicionar do template + label_checklists_from_template: Do template + label_select_template: "-- Selecione o template --" + label_checklist_category_not_specified: "-- NĂŁo especificado --" + label_checklists_description: 'Permitir multiplos valores' + label_checklist_item: Checklist item + label_checklist_section: Checklist sessĂŁo + label_checklist_deleted: deletado + label_checklist_changed_from: alterado por + label_checklist_changed_to: para + label_checklist_added: adicionado + label_checklist_done: alterar para concluĂdo + label_checklist_undone: alterar para nĂŁo concluĂdo + label_checklist_updated: Checklist item editado + label_checklist_status: Checklist status + label_checklist_status_done: Pronto + label_checklist_status_undone: NĂŁo pronto + label_checklist_is_default: PadrĂŁo + field_is_for_tracker: Tipo + label_checklists_must_be_completed: Para concluir a tarefa, todos os checklists devem estar prontos + label_checklist_block_issue_closing: Bloquear tarefa ao concluir + label_checklist_show_closed: Mostrar concluĂdos + label_checklist_hide_closed: Esconder concluĂdos + label_checklist_new_section: Nova sessĂŁo diff --git a/plugins/redmine_checklists/config/locales/ru.yml b/plugins/redmine_checklists/config/locales/ru.yml index 7e3d5d6..8c7463f 100644 --- a/plugins/redmine_checklists/config/locales/ru.yml +++ b/plugins/redmine_checklists/config/locales/ru.yml @@ -1,5 +1,9 @@ # encoding: utf-8 ru: + activerecord: + attributes: + checklists: + subject: Заголовок чеклиŃŃ‚Đ° label_checklist_plural: ЧеклиŃŃ‚ field_checklist: ЧеклиŃŃ‚ label_checklist_save_log: Сохранять изменения в иŃтории @@ -15,10 +19,12 @@ ru: field_template_items: Đлементы Ńаблона label_checklist_template: Шаблон чеклиŃтов label_add_checklists_from_template: Добавить из Ńаблона + label_checklists_from_template: ĐĐ· Ńаблона label_select_template: "-- Выберите Ńаблон --" label_checklist_category_not_specified: "-- Без категории --" label_checklists_description: 'Для ввода неŃкольких значений вводите по ĐľĐ´Đ˝ĐľĐĽŃ Đ˝Đ° ŃтрокŃ' label_checklist_item: ĐźŃнкт чеклиŃŃ‚Đ° + label_checklist_section: Раздел чеклиŃŃ‚Đ° label_checklist_deleted: Ńдалён label_checklist_changed_from: изменён Ń label_checklist_changed_to: на @@ -31,3 +37,10 @@ ru: label_checklist_status_undone: Не выполнен label_checklist_is_default: По Ńмолчанию field_is_for_tracker: Трекер + label_checklist_show_closed: Показать закрытые + label_checklist_hide_closed: Скрыть закрытые + label_checklist_new_section: Новая Ńекция + label_checklist_block_issue_closing: Запретить закрытие заявки + label_checklists: ЧеклиŃŃ‚ + label_checklists_must_be_completed: Чтобы заявка закрылаŃŃŚ, должны быть выполнены вŃе ĐżŃнкты чеклиŃŃ‚Đ° + field_checklists: ЧеклиŃŃ‚ diff --git a/plugins/redmine_checklists/config/routes.rb b/plugins/redmine_checklists/config/routes.rb index 98d51b4..dab2482 100644 --- a/plugins/redmine_checklists/config/routes.rb +++ b/plugins/redmine_checklists/config/routes.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -26,10 +26,3 @@ resources :checklists, :only => [:destroy, :update, :show] do put :done end end -resources :projects do - resources :checklist_templates, :only => [:new, :create, :update, :edit] -end - -resources :checklist_templates - -resources :checklist_template_categories, :only => [:edit, :update, :new, :create, :destroy] diff --git a/plugins/redmine_checklists/db/migrate/001_create_checklists.rb b/plugins/redmine_checklists/db/migrate/001_create_checklists.rb index f5a56c3..2408934 100644 --- a/plugins/redmine_checklists/db/migrate/001_create_checklists.rb +++ b/plugins/redmine_checklists/db/migrate/001_create_checklists.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/db/migrate/002_add_time_stamps_to_checklists.rb b/plugins/redmine_checklists/db/migrate/002_add_time_stamps_to_checklists.rb index db3f1a4..03e3c99 100644 --- a/plugins/redmine_checklists/db/migrate/002_add_time_stamps_to_checklists.rb +++ b/plugins/redmine_checklists/db/migrate/002_add_time_stamps_to_checklists.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/db/migrate/003_create_checklist_template_category.rb b/plugins/redmine_checklists/db/migrate/003_create_checklist_template_category.rb index 700930e..ebb7987 100644 --- a/plugins/redmine_checklists/db/migrate/003_create_checklist_template_category.rb +++ b/plugins/redmine_checklists/db/migrate/003_create_checklist_template_category.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/db/migrate/004_create_checklist_templates.rb b/plugins/redmine_checklists/db/migrate/004_create_checklist_templates.rb index 89b7a07..8de0692 100644 --- a/plugins/redmine_checklists/db/migrate/004_create_checklist_templates.rb +++ b/plugins/redmine_checklists/db/migrate/004_create_checklist_templates.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/db/migrate/005_modify_checklist_subject_length.rb b/plugins/redmine_checklists/db/migrate/005_modify_checklist_subject_length.rb index 7707d7c..5538b2b 100644 --- a/plugins/redmine_checklists/db/migrate/005_modify_checklist_subject_length.rb +++ b/plugins/redmine_checklists/db/migrate/005_modify_checklist_subject_length.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/db/migrate/006_add_fields_to_checklist_template.rb b/plugins/redmine_checklists/db/migrate/006_add_fields_to_checklist_template.rb index f5a75fa..ce38cb1 100644 --- a/plugins/redmine_checklists/db/migrate/006_add_fields_to_checklist_template.rb +++ b/plugins/redmine_checklists/db/migrate/006_add_fields_to_checklist_template.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/doc/CHANGELOG b/plugins/redmine_checklists/doc/CHANGELOG index c86364d..dec076c 100644 --- a/plugins/redmine_checklists/doc/CHANGELOG +++ b/plugins/redmine_checklists/doc/CHANGELOG @@ -1,12 +1,64 @@ == Redmine Checklists plugin changelog Redmine Checklists plugin - managing issue checklists plugin for Redmine -Copyright (C) 2011-2018 RedmineUP +Copyright (C) 2011-2021 RedmineUP http://www.redmineup.com/ +== 2021-05-21 v3.1.19 + +* Added Redmine 4.2 compatibility +* Fixed empty project errors +* Fixed notification on journal fixup +* Fixed checklist positions bug + +== 2020-08-17 v3.1.18 + +* Added italian locale +* Updated zh-tw locale +* Fixed size() method error +* Fixed initial install error +* Fixed checklist ration recalculate +* Fixed display checklist element with Markdown syntax +* Fixed done ratio recalculate on checklist API update +* Fixed API call journalizing + +== 2020-01-31 v3.1.17 + +* Redmine 4.1 compatibility fixes +* Fixed view permission bug +* Fixed template permissions bug +* Fixed context menu conflicts +* Fixed Agile support +* Fixed checklist copy bug +* Fixed locale bug +* Fixed project copy bug + +== 2019-04-29 v3.1.16 + +* Checklists sections + +== 2019-04-15 v3.1.15 + +* Redmine 4.0.3 support +* Added Hide link for closed items + +== 2018-12-20 v3.1.14 + +* Hotfix for Redmine 4 + +== 2018-12-18 v3.1.13 + +* Redmine 4 saving issue fixes + +== 2018-11-26 v3.1.12 + +* German translation update from Tobias Fischer +* Fixed diferent authors changes bug +* Fixed sortable animation bug + == 2018-03-23 v3.1.11 -* Rails 4 support +* Redmine 4 support * Setting for block issues with undone checklists * Fixed bug with default template * Fixed email notification bug @@ -36,10 +88,10 @@ http://www.redmineup.com/ * Redmine 3.4 support * New checklists filters for issues table -* Save log by default +* Save log by default * Chinese translation update * Polish translation update -* Fixed bug with template editing +* Fixed bug with template editing == 2016-08-15 v3.1.5 diff --git a/plugins/redmine_checklists/init.rb b/plugins/redmine_checklists/init.rb index 174d9d7..3250729 100644 --- a/plugins/redmine_checklists/init.rb +++ b/plugins/redmine_checklists/init.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -20,9 +20,8 @@ require 'redmine' require 'redmine_checklists/redmine_checklists' -CHECKLISTS_VERSION_NUMBER = '3.1.11'.freeze -CHECKLISTS_VERSION_TYPE = 'PRO version'.freeze - +CHECKLISTS_VERSION_NUMBER = '3.1.19'.freeze +CHECKLISTS_VERSION_TYPE = "Light version" Redmine::Plugin.register :redmine_checklists do name "Redmine Checklists plugin (#{CHECKLISTS_VERSION_TYPE})" @@ -32,7 +31,7 @@ Redmine::Plugin.register :redmine_checklists do url 'https://www.redmineup.com/pages/plugins/checklists' author_url 'mailto:support@redmineup.com' - requires_redmine :version_or_higher => '2.3' + requires_redmine :version_or_higher => '3.0' settings :default => { :save_log => true, @@ -44,7 +43,6 @@ Redmine::Plugin.register :redmine_checklists do map.permission :view_checklists, { :checklists => [:show, :index] } map.permission :done_checklists, { :checklists => :done } map.permission :edit_checklists, { :checklists => [:done, :create, :destroy, :update] } - map.permission :manage_checklist_templates, { :checklist_templates => [:new, :create, :destroy, :edit, :update] } end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/hooks/controller_issues_hook.rb b/plugins/redmine_checklists/lib/redmine_checklists/hooks/controller_issues_hook.rb index ef97797..2a8cde0 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/hooks/controller_issues_hook.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/hooks/controller_issues_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -21,20 +21,8 @@ module RedmineChecklists module Hooks class ControllerIssuesHook < Redmine::Hook::ViewListener def controller_issues_edit_after_save(context = {}) - old_checklists = context[:issue].old_checklists - new_checklists = context[:issue].checklists.to_json - journal = context[:journal] - details = JournalChecklistHistory.new(old_checklists, new_checklists).journal_details - if JournalChecklistHistory.can_fixup?(details) - JournalChecklistHistory.fixup(details) - elsif details.old_value != details.value - journal.details << details - journal.save - else - journal.save - end - if (Setting.issue_done_ratio == "issue_field") && RedmineChecklists.settings["issue_done_ratio"].to_i > 0 + if (Setting.issue_done_ratio == 'issue_field') && RedmineChecklists.issue_done_ratio? Checklist.recalc_issue_done_ratio(context[:issue].id) end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_issues_hook.rb b/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_issues_hook.rb index 3923d25..79c2294 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_issues_hook.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_issues_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_layouts_hook.rb b/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_layouts_hook.rb index 7631101..852f30a 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_layouts_hook.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/hooks/views_layouts_hook.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/add_helpers_for_checklists_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/add_helpers_for_checklists_patch.rb index 750e2d0..660da28 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/add_helpers_for_checklists_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/add_helpers_for_checklists_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/2.1/redmine_api_test_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/2.1/redmine_api_test_patch.rb index d7ad48c..d15bcfd 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/2.1/redmine_api_test_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/2.1/redmine_api_test_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_controller_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_controller_patch.rb deleted file mode 100644 index 22a5595..0000000 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_controller_patch.rb +++ /dev/null @@ -1,41 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -module RedmineChecklists - module Patches - module ApplicationControllerPatch - def self.included(base) # :nodoc: - base.extend(ClassMethods) - base.class_eval do - unloadable # Send unloadable so it will not be unloaded in development - end - end - - module ClassMethods - def before_action(*filters, &block) - before_filter(*filters, &block) - end - end - end - end -end - -unless ApplicationController.included_modules.include?(RedmineChecklists::Patches::ApplicationControllerPatch) - ApplicationController.send(:include, RedmineChecklists::Patches::ApplicationControllerPatch) -end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_helper_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_helper_patch.rb index a627386..47ed55e 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_helper_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/application_helper_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/journal_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/journal_patch.rb index 4e48af9..9892dd1 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/journal_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/journal_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -17,28 +17,18 @@ # You should have received a copy of the GNU General Public License # along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - require_dependency 'journal' module RedmineChecklists module Patches module JournalPatch - def self.included(base) # :nodoc: base.send(:include, InstanceMethods) base.class_eval do - after_create :send_checklist_notification end end module InstanceMethods - def send_checklist_notification - detail = detail_for_attribute('checklist') - checklist_email_nootification(self).deliver if !Setting.notified_events.include?('issue_updated') && - Setting.notified_events.include?('checklist_updated') && - detail.present? && - !JournalChecklistHistory.new(detail.old_value, detail.value).empty_diff? - end if Redmine::VERSION.to_s < '2.6' def send_notification @@ -48,7 +38,7 @@ module RedmineChecklists (Setting.notified_events.include?('issue_status_updated') && new_status.present?) || (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) ) - checklist_email_nootification(self).deliver + deliver_checklist_notification end end @@ -57,7 +47,17 @@ module RedmineChecklists end end - def checklist_email_nootification(journal) + def deliver_checklist_notification + if Redmine::VERSION.to_s >= '4.0' + (notified_watchers | notified_users).each do |user| + Mailer.issue_edit(user, self).deliver + end + else + checklist_email_notification(self).deliver + end + end + + def checklist_email_notification(journal) if Redmine::VERSION.to_s < '2.4' Mailer.issue_edit(journal) else @@ -65,7 +65,6 @@ module RedmineChecklists end end end - end end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/open_struct_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/open_struct_patch.rb index d0d27e1..36c0395 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/open_struct_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility/open_struct_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility_patch.rb index 3e1460e..c347ab2 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/compatibility_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_patch.rb index ab57df5..8cfc0c6 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -46,23 +46,21 @@ module RedmineChecklists safe_attributes 'checklists_attributes', :if => lambda { |issue, user| (user.allowed_to?(:done_checklists, issue.project) || user.allowed_to?(:edit_checklists, issue.project)) } + end + end - def copy_checklists(arg) - issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) - issue.checklists.each{ |checklist| Checklist.create(checklist.attributes.except('id', 'issue_id').merge(:issue => self)) } if issue - end - - def block_issue_closing_if_checklists_unclosed - if RedmineChecklistSetting.block_issue_closing? && checklists.any? && status.is_closed? - errors.add(:checklists, l(:label_checklists_must_be_completed)) unless (checklists - checklists.where(:id => removed_checklist_ids)).all?(&:is_done) + module InstanceMethods + def copy_checklists(arg) + issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) + if issue + issue.checklists.each do |checklist| + Checklist.create(checklist.attributes.except('id', 'issue_id').merge(issue: self)) end end end - end - module InstanceMethods def copy_subtask_checklists - return if !copy? || parent_id.nil? || checklists.any? + return if !copy? || parent_id.nil? || checklists.reload.any? copy_checklists(@copied_from) end @@ -71,6 +69,23 @@ module RedmineChecklists copy.copy_checklists(self) copy end + + def all_checklist_items_is_done? + (checklists - checklists.where(id: removed_checklist_ids)).reject(&:is_section).all?(&:is_done) + end + + def need_to_block_issue_closing? + RedmineChecklists.block_issue_closing? && + checklists.reject(&:is_section).any? && + status.is_closed? && + !all_checklist_items_is_done? + end + + def block_issue_closing_if_checklists_unclosed + if need_to_block_issue_closing? + errors.add(:checklists, l(:label_checklists_must_be_completed)) + end + end end end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_query_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_query_patch.rb index d6b2de3..4e613f9 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_query_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/issue_query_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -24,67 +24,15 @@ module RedmineChecklists module IssueQueryPatch def self.included(base) base.send(:include, InstanceMethods) - base.class_eval do - unloadable - alias_method :available_filters_without_checklists, :available_filters - alias_method :available_filters, :available_filters_with_checklists - end end module InstanceMethods - - def available_filters_with_checklists - if @available_filters.blank? - add_available_filter('checklists_status', :type => :list, :name => l(:label_checklist_status), - :values => [[l(:label_checklist_status_done), '1'], [l(:label_checklist_status_undone), '0']]) unless available_filters_without_checklists.key?('checklists_status') && !User.current.allowed_to?(:view_checklists, project, :global => true) - - add_available_filter('checklists_item', :type => :string, :name => l(:label_checklist_item)) unless available_filters_without_checklists.key?('checklists_item') && !User.current.allowed_to?(:view_checklists, project, :global => true) - else - available_filters_without_checklists - end - @available_filters - end - - def sql_for_checklists_status_field(_field, operator, value) - case operator - when '=' - compare = '=' - when '!' - compare = '!=' - end - condition = - if value.size > 1 - '1=1' - else - "is_done #{compare} #{value.join == '1' ? self.class.connection.quoted_true : self.class.connection.quoted_false}" - end - issue_ids = "SELECT DISTINCT(#{Checklist.table_name}.issue_id) FROM #{Checklist.table_name} WHERE #{condition}" - "(#{Issue.table_name}.id IN (#{issue_ids}))" - end - - def sql_for_checklists_item_field(_field, operator, value) - case operator - when '=', '!' - condition = "#{Checklist.table_name}.subject = ?" - when '~', '!~' - condition = "LOWER(#{Checklist.table_name}.subject) LIKE LOWER(?)" - value = "%#{value.join}%" - when '*', '!*' - condition = '1=1' - end - issue_ids = Checklist.where(condition, value).pluck(:issue_id).uniq - if ['!', '!~'].include?(operator) - all_issue_ids = Checklist.pluck(:issue_id).uniq - issue_ids = all_issue_ids - issue_ids - end - return '1=0' if issue_ids.empty? - "(#{Issue.table_name}.id #{'NOT' if operator == '!*'} IN (#{issue_ids.join(',')}))" - end end end end end -unless IssueQuery.included_modules.include?(RedmineChecklists::Patches::IssueQueryPatch) +if (ActiveRecord::Base.connection.tables.include?('queries') rescue false) && + IssueQuery.included_modules.exclude?(RedmineChecklists::Patches::IssueQueryPatch) IssueQuery.send(:include, RedmineChecklists::Patches::IssueQueryPatch) end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_controller_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_controller_patch.rb index af244e6..f927fd4 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_controller_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_controller_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -35,10 +35,14 @@ module RedmineChecklists module InstanceMethods def build_new_issue_from_params_with_checklist if params[:id].blank? - if params[:copy_from].blank? - fill_default_checklist - else - fill_checklist_attributes + begin + if params[:copy_from].blank? + else + fill_checklist_attributes + end + rescue ActiveRecord::RecordNotFound + render_404 + return end end build_new_issue_from_params_without_checklist @@ -58,51 +62,22 @@ module RedmineChecklists def fill_checklist_attributes return unless params[:issue].blank? - begin - @copy_from = Issue.visible.find(params[:copy_from]) - add_checklists_to_params(@copy_from.checklists) - rescue ActiveRecord::RecordNotFound - render_404 - return - end - end - def fill_default_checklist - return if custom_checklists_included?(params[:issue]) - params[:issue] ||= {} - tracker_id = params[:issue].try(:[], :tracker_id) || issue_project.trackers.first.try(:id) - default_template = issue_project.default_checklist_template(tracker_id) - return params[:issue][:checklists_attributes] = {} if default_template.nil? - params[:issue][:checklist_template_id] = default_template.id - add_checklists_to_params(default_template.checklists) + + @copy_from = Issue.visible.find(params[:copy_from]) + add_checklists_to_params(@copy_from.checklists) end def add_checklists_to_params(checklists) params[:issue].blank? ? params[:issue] = { :checklists_attributes => {} } : params[:issue][:checklists_attributes] = {} checklists.each_with_index do |checklist_item, index| - params[:issue][:checklists_attributes][index.to_s] = { :is_done => checklist_item.is_done, - :subject => checklist_item.subject, - :position => checklist_item.position } - end - end - def custom_checklists_included?(issue_attr) - return if issue_attr.blank? || issue_attr[:checklists_attributes].blank? || checklist_subjects(issue_attr[:checklists_attributes]).empty? - default_template = issue_project.checklist_templates.visible.where(:id => issue_attr[:checklist_template_id]).first - return true unless default_template - checklist_subjects(issue_attr[:checklists_attributes]) != default_template.checklists.map(&:subject) - end - - def checklist_subjects(attrs) - if attrs.is_a?(Array) # JSON/XML - attrs.map { |checklist_attr| checklist_attr[:subject] }.compact - else - attrs = attrs.permit!.to_h if Rails::VERSION::MAJOR >= 5 - attrs.map { |_k, v| v[:subject] if v[:subject].present? }.compact + params[:issue][:checklists_attributes][index.to_s] = { + is_done: checklist_item.is_done, + subject: checklist_item.subject, + position: checklist_item.position, + is_section: checklist_item.is_section + } end end - - def issue_project - @project || Project.where(:id => Issue.new.allowed_target_projects.map(&:id)).order(:id).first - end end end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_helper_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_helper_patch.rb index f010f4e..07333cf 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_helper_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/issues_helper_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -28,28 +28,18 @@ module RedmineChecklists alias_method :details_to_strings_without_checklists, :details_to_strings alias_method :details_to_strings, :details_to_strings_with_checklists - if Redmine::VERSION.to_s >= '2.2' && Redmine::VERSION.to_s <= '2.4' - alias_method :render_email_issue_attributes_without_checklists, :render_email_issue_attributes - alias_method :render_email_issue_attributes, :render_email_issue_attributes_with_checklists - end end end module InstanceMethods - def render_email_issue_attributes_with_checklists(issue, html = false) - journal = issue.journals.order(:id).last - return render_email_issue_attributes_without_checklists(issue, html) unless journal - details = journal.details - return render_email_issue_attributes_without_checklists(issue, html) unless details - checklist_details = details.select{ |x| x.prop_key == 'checklist'} - return render_email_issue_attributes_without_checklists(issue, html) unless checklist_details.any? - return render_email_issue_attributes_without_checklists(issue, html) + details_to_strings_with_checklists(checklist_details, !html).join(html ? "<br/>".html_safe : "\n") - end - def details_to_strings_with_checklists(details, no_html = false, options = {}) details_checklist, details_other = details.partition{ |x| x.prop_key == 'checklist' } + if @issue.nil? || !User.current.allowed_to?(:view_checklists, @issue.try(:project), global: @issue.present?) + return details_to_strings_without_checklists(details_other, no_html, options) + end + details_checklist.map do |detail| result = [] diff = Hash.new([]) @@ -59,33 +49,20 @@ module RedmineChecklists else diff = JournalChecklistHistory.new(detail.old_value, detail.value).diff end - if diff[:removed].any? - diff[:removed].each do |item| - result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> #{ERB::Util.h l(:label_checklist_deleted)} (<strike><i>#{ERB::Util.h item[:subject]}</i></strike>)" - end - end - if diff[:renamed].any? - diff[:renamed].each do |was, became| - result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> #{ERB::Util.h l(:label_checklist_changed_from)} <i>#{ERB::Util.h was}</i> #{ERB::Util.h l(:label_checklist_changed_to)} <i>#{ERB::Util.h became}</i>" - end - end - - if diff[:added].any? - diff[:added].each do |item| - result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> <input type='checkbox' #{item.is_done ? 'checked' : '' } disabled> <i>#{ERB::Util.h item[:subject]}</i> #{ERB::Util.h l(:label_checklist_added)}" - end + checklist_item_label = lambda do |item| + item[:is_section] ? l(:label_checklist_section) : l(:label_checklist_item) end if diff[:done].any? diff[:done].each do |item| - result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> <input type='checkbox' #{item.is_done ? 'checked' : '' } disabled> <i>#{ERB::Util.h item[:subject]}</i> #{ERB::Util.h l(:label_checklist_done)}" + result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> <input type='checkbox' class='checklist-checkbox' #{item.is_done ? 'checked' : '' } disabled> <i>#{ERB::Util.h item[:subject]}</i> #{ERB::Util.h l(:label_checklist_done)}" end end if diff[:undone].any? diff[:undone].each do |item| - result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> <input type='checkbox' #{item.is_done ? 'checked' : '' } disabled> <i>#{ERB::Util.h item[:subject]}</i> #{ERB::Util.h l(:label_checklist_undone)}" + result << "<b>#{ERB::Util.h l(:label_checklist_item)}</b> <input type='checkbox' class='checklist-checkbox' #{item.is_done ? 'checked' : '' } disabled> <i>#{ERB::Util.h item[:subject]}</i> #{ERB::Util.h l(:label_checklist_undone)}" end end @@ -93,8 +70,8 @@ module RedmineChecklists result = nil if result.blank? if result && no_html result = result.gsub /<\/li><li>/, "\n" - result = result.gsub /<input type='checkbox'[^c^>]*checked[^>]*>/, '[x]' - result = result.gsub /<input type='checkbox'[^c^>]*>/, '[ ]' + result = result.gsub /<input type='checkbox' class='checklist-checkbox'[^c^>]*checked[^>]*>/, '[x]' + result = result.gsub /<input type='checkbox' class='checklist-checkbox'[^c^>]*>/, '[ ]' result = result.gsub /<[^>]*>/, '' result = CGI.unescapeHTML(result) end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/notifiable_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/notifiable_patch.rb index 911a3fe..c0fa95b 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/notifiable_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/notifiable_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -18,32 +18,3 @@ # along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. -module RedmineChecklists - module Patches - module NotifiablePatch - def self.included(base) - base.extend(ClassMethods) - base.class_eval do - unloadable - class << self - alias_method :all_without_checklists, :all - alias_method :all, :all_with_checklists - end - end - end - - module ClassMethods - def all_with_checklists - notifications = all_without_checklists - last_issue_child_index = notifications.find_index(notifications.select{ |element| element.parent == 'issue_updated' }.last) - notifications.insert(last_issue_child_index + 1, Redmine::Notifiable.new('checklist_updated', 'issue_updated')) - notifications - end - end - end - end -end - -unless Redmine::Notifiable.included_modules.include?(RedmineChecklists::Patches::NotifiablePatch) - Redmine::Notifiable.send(:include, RedmineChecklists::Patches::NotifiablePatch) -end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/project_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/project_patch.rb index 0c374f3..d2985db 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/project_patch.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/patches/project_patch.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -28,20 +28,14 @@ module RedmineChecklists unloadable # Send unloadable so it will not be unloaded in development alias_method :copy_issues_without_checklist, :copy_issues alias_method :copy_issues, :copy_issues_with_checklist - has_many :checklist_templates end end module InstanceMethods - def default_checklist_template(tracker_id = nil) - default_templates = checklist_templates.visible.default - default_by_tracker = default_templates.for_tracker_id(tracker_id).first - default_by_tracker || default_templates.for_tracker_id(nil).first - end def copy_issues_with_checklist(project) copy_issues_without_checklist(project) - issues.each{ |issue| issue.copy_checklists(issue.copied_from)} + issues.each{ |issue| issue.copy_checklists(issue.copied_from) if issue.reload.checklists.empty? } end end end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/patches/projects_helper_patch.rb b/plugins/redmine_checklists/lib/redmine_checklists/patches/projects_helper_patch.rb deleted file mode 100644 index 51b6fe2..0000000 --- a/plugins/redmine_checklists/lib/redmine_checklists/patches/projects_helper_patch.rb +++ /dev/null @@ -1,52 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - - -module RedmineChecklists - module Patches - module ProjectsHelperPatch - def self.included(base) - base.send(:include, InstanceMethods) - - base.class_eval do - unloadable - - alias_method :project_settings_tabs_without_checklists, :project_settings_tabs - alias_method :project_settings_tabs, :project_settings_tabs_with_checklists - end - end - - module InstanceMethods - def project_settings_tabs_with_checklists - tabs = project_settings_tabs_without_checklists - tab = { :name => 'checklist_template', - :action => :manage_checklist_templates, - :partial => 'projects/settings/checklist_templates', - :label => :label_checklist_templates } - tabs << tab if User.current.allowed_to?(:edit_issues, @project) && User.current.allowed_to?(tab[:action], @project) - tabs - end - end - end - end -end - -unless ProjectsHelper.included_modules.include?(RedmineChecklists::Patches::ProjectsHelperPatch) - ProjectsHelper.send(:include, RedmineChecklists::Patches::ProjectsHelperPatch) -end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklist_setting.rb b/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklist_setting.rb deleted file mode 100644 index a876100..0000000 --- a/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklist_setting.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -module RedmineChecklists - class RedmineChecklistSetting - def self.block_issue_closing? - Setting.plugin_redmine_checklists['block_issue_closing'].to_i > 0 - end - end -end diff --git a/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklists.rb b/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklists.rb index 966a59b..6db0db4 100644 --- a/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklists.rb +++ b/plugins/redmine_checklists/lib/redmine_checklists/redmine_checklists.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -19,7 +19,6 @@ Rails.configuration.to_prepare do require 'redmine_checklists/patches/compatibility/application_helper_patch' - require 'redmine_checklists/patches/compatibility/application_controller_patch' if Rails::VERSION::MAJOR < 4 require 'redmine_checklists/hooks/views_issues_hook' require 'redmine_checklists/hooks/views_layouts_hook' @@ -30,14 +29,19 @@ Rails.configuration.to_prepare do require 'redmine_checklists/patches/issues_controller_patch' require 'redmine_checklists/patches/add_helpers_for_checklists_patch' require 'redmine_checklists/patches/compatibility_patch' - require 'redmine_checklists/patches/projects_helper_patch' - require 'redmine_checklists/patches/notifiable_patch' - require 'redmine_checklists/patches/issue_query_patch' require 'redmine_checklists/patches/issues_helper_patch' require 'redmine_checklists/patches/compatibility/open_struct_patch' require 'redmine_checklists/patches/compatibility/journal_patch' end module RedmineChecklists - def self.settings() Setting[:plugin_redmine_checklists].blank? ? {} : Setting[:plugin_redmine_checklists] end + def self.settings() Setting.plugin_redmine_checklists.blank? ? {} : Setting.plugin_redmine_checklists end + + def self.block_issue_closing? + settings['block_issue_closing'].to_i > 0 + end + + def self.issue_done_ratio? + settings['issue_done_ratio'].to_i > 0 + end end diff --git a/plugins/redmine_checklists/scripts/run_local.sh b/plugins/redmine_checklists/scripts/run_local.sh deleted file mode 100755 index dc77740..0000000 --- a/plugins/redmine_checklists/scripts/run_local.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -RUBY=$1 -DB=$2 -REDMINE=$3 - -docker run -t -i -v `pwd`:/var/www/$RUBY/$DB/$REDMINE/plugins/redmine_checklists \ - --env RUBY=$1 \ - --env DB=$2 \ - --env REDMINE=$3 \ - --env PLUGIN=redmine_checklists \ - redmineup/redmine_checklists \ - /root/run_local.sh diff --git a/plugins/redmine_checklists/test/fixtures/checklists.yml b/plugins/redmine_checklists/test/fixtures/checklists.yml index 573b24a..399383b 100644 --- a/plugins/redmine_checklists/test/fixtures/checklists.yml +++ b/plugins/redmine_checklists/test/fixtures/checklists.yml @@ -1,14 +1,25 @@ -# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +--- +# === Checklist for Issue(1) === one: id: 1 is_done: false subject: First todo issue_id: 1 + two: id: 2 is_done: true subject: Second todo issue_id: 1 + +# === Checklist for Issue(2) === +section_one: + id: 4 + is_done: false + subject: New section + is_section: true + issue_id: 2 + three: id: 3 is_done: true diff --git a/plugins/redmine_checklists/test/functional/checklist_template_categories_controller_test.rb b/plugins/redmine_checklists/test/functional/checklist_template_categories_controller_test.rb deleted file mode 100644 index 6c242b2..0000000 --- a/plugins/redmine_checklists/test/functional/checklist_template_categories_controller_test.rb +++ /dev/null @@ -1,92 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) -class ChecklistTemplateCategoriesControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - RedmineChecklists::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_checklists).directory + '/test/fixtures/', [:checklists]) - - def setup - RedmineChecklists::TestCase.prepare - Setting.default_language = 'en' - Project.find(1).enable_module!(:checklists) - Project.find(1).enable_module!(:issue_tracking) - User.current = nil - @project_1 = Project.find(1) - @issue_1 = Issue.find(1) - @checklist_1 = Checklist.find(1) - end - - test 'should show new form' do - @request.session[:user_id] = 1 - compatible_request :get, :new - assert_select 'form.new_checklist_template_category div.box.tabular' - end - - test 'creates new checklist template category' do - @request.session[:user_id] = 1 - compatible_request :post, :create, :checklist_template_category => { :name => 'test1' } - assert_equal 1, ChecklistTemplateCategory.count - end - - test 'should show edit form' do - @request.session[:user_id] = 1 - @template = ChecklistTemplateCategory.create!(:name => 'category1') - compatible_request :get, :edit, :id => @template.to_param - assert_select 'form.edit_checklist_template_category div.box.tabular' - end - - test 'should update checklist template category' do - @request.session[:user_id] = 1 - @template = ChecklistTemplateCategory.create!(:name => 'category1') - compatible_request :put, :update, :id => @template.to_param, :category => { :name => 'category2' } - assert_equal 'category2', ChecklistTemplateCategory.last.name - end - - test 'should delete checklist template' do - @request.session[:user_id] = 1 - @template = ChecklistTemplateCategory.create!(:name => 'category1') - compatible_request :delete, :destroy, :id => @template.to_param - assert_equal 0, ChecklistTemplateCategory.count - end -end diff --git a/plugins/redmine_checklists/test/functional/checklist_templates_controller_test.rb b/plugins/redmine_checklists/test/functional/checklist_templates_controller_test.rb deleted file mode 100644 index 73a6374..0000000 --- a/plugins/redmine_checklists/test/functional/checklist_templates_controller_test.rb +++ /dev/null @@ -1,135 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class ChecklistTemplatesControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - RedmineChecklists::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_checklists).directory + '/test/fixtures/', [:checklists]) - - def setup - RedmineChecklists::TestCase.prepare - Setting.default_language = 'en' - Project.find(1).enable_module!(:checklists) - Project.find(1).enable_module!(:issue_tracking) - User.current = nil - @second_user = User.find(2) - @project_1 = Project.find(1) - @issue_1 = Issue.find(1) - @checklist_1 = Checklist.find(1) - MemberRole.create(:member_id => 1, :role_id => 2) - end - - test 'should show new form' do - @request.session[:user_id] = 1 - compatible_request :get, :new - assert_select 'form.new_checklist_template div.box.tabular' - end - - test 'creates new checklist template' do - @request.session[:user_id] = 1 - compatible_request :post, :create, :checklist_template => { :name => 'test1', :template_items => 'item1 item2' }, :checklist_template_is_for_all => true - assert_equal 'test1', ChecklistTemplate.last.name - assert_equal 1, ChecklistTemplate.last.user_id - end - - test 'user with right can create template' do - @request.session[:user_id] = @second_user.id - compatible_request :post, :create, :checklist_template => { :name => 'user_test', :template_items => 'item1 item2' }, :project_id => @project_1 - assert_equal 'user_test', ChecklistTemplate.last.name - assert_equal 2, ChecklistTemplate.last.user_id - assert_equal @project_1, ChecklistTemplate.last.project - end - - test 'user cant create public template for all projects' do - @request.session[:user_id] = @second_user.id - compatible_request :post, :create, :checklist_template => { :name => 'public_template', :template_items => 'item1 item2', :is_public => '1' }, - :checklist_template_is_for_all => '1', - :project_id => @project_1 - assert_equal 'public_template', ChecklistTemplate.last.name - assert_equal 2, ChecklistTemplate.last.user_id - assert_equal true, ChecklistTemplate.last.is_public - assert_equal @project_1, ChecklistTemplate.last.project - end - - test 'nobody cant edit user template if it not public, except admins' do - @request.session[:user_id] = @second_user.id - compatible_request :post, :create, :checklist_template => { :name => 'not_public_template', :template_items => 'item1 item2', :is_public => '0' }, - :checklist_template_is_for_all => '1', - :project_id => @project_1 - assert_equal 'not_public_template', ChecklistTemplate.last.name - @request.session[:user_id] = 3 - compatible_request :get, :edit, :project_id => @project_1, :id => ChecklistTemplate.last.id - assert_response :missing - end - - test 'should show edit form' do - @request.session[:user_id] = 1 - @template = ChecklistTemplate.create!(:name => 'template1', :template_items => 'item1 item2', :is_public => true) - compatible_request :get, :edit, :id => @template.to_param - assert_select 'form.edit_checklist_template div.box.tabular' - end - - test 'should update checklist template' do - @request.session[:user_id] = 1 - @template = ChecklistTemplate.create!(:name => 'template1', :template_items => 'item1 item2', :is_public => true) - compatible_request :put, :update, :id => @template.to_param, :checklist_template => { :name => 'test2' } - assert_equal 'test2', ChecklistTemplate.last.name - end - - test 'should delete checklist template' do - @request.session[:user_id] = 1 - @template = ChecklistTemplate.create!(:name => 'template1', :template_items => 'item1 item2', :is_public => true) - compatible_request :delete, :destroy, :id => @template.to_param - assert_equal 0, ChecklistTemplate.count - end - - def test_should_created_default_template_for_tracker - @request.session[:user_id] = 1 - compatible_request :post, :create, :checklist_template => { :name => 'default', :template_items => 'default1 default2', :is_default => 1, :tracker_id => 1 } - assert_equal 'default', ChecklistTemplate.last.name - assert_equal 1, ChecklistTemplate.last.user_id - assert_equal true, ChecklistTemplate.last.is_default? - assert_equal Tracker.find(1), ChecklistTemplate.last.tracker - end -end diff --git a/plugins/redmine_checklists/test/functional/checklists_controller_test.rb b/plugins/redmine_checklists/test/functional/checklists_controller_test.rb index 3ad4aaf..29d7a59 100644 --- a/plugins/redmine_checklists/test/functional/checklists_controller_test.rb +++ b/plugins/redmine_checklists/test/functional/checklists_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -64,19 +64,6 @@ class ChecklistsControllerTest < ActionController::TestCase assert_response :success, 'Post done not working' assert_equal true, Checklist.find(1).is_done, 'Post done not working' end - test 'sends email about checklists' do - @request.session[:user_id] = 1 - Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } - EmailAddress.create!(:user_id => 2, :address => 'test@example.com') if Redmine::VERSION.to_s >= '3.0' - compatible_xhr_request :put, :done, :is_done => 'true', :id => '1' - assert ActionMailer::Base.deliveries.last - email = ActionMailer::Base.deliveries.last - assert_include 'Checklist item [x] First todo set to Done', email.text_part.body.to_s - # Test changes fixup - compatible_xhr_request :put, :done, :is_done => 'false', :id => '2' - email = ActionMailer::Base.deliveries.last - assert_include 'Checklist item [x] First todo set to Done', email.text_part.body.to_s - end test "should not post done by deny user" do # log_user('admin', 'admin') diff --git a/plugins/redmine_checklists/test/functional/issues_controller_test.rb b/plugins/redmine_checklists/test/functional/issues_controller_test.rb index 3cf565a..ad83748 100644 --- a/plugins/redmine_checklists/test/functional/issues_controller_test.rb +++ b/plugins/redmine_checklists/test/functional/issues_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -53,28 +53,7 @@ class IssuesControllerTest < ActionController::TestCase def setup @request.session[:user_id] = 1 - end - def test_show_index_with_checklists_filter - compatible_request :get, :index, :project_id => 1, :set_filter => '1', :f => ['checklists_status', ''], :op => { :checklists_status => '=' }, :v => { :checklists_status => ['1'] } - assert_response :success - assert_equal issues_in_list.map(&:id), [2, 1] - - compatible_request :get, :index, :project_id => 1, :set_filter => '1', :f => ['checklists_status', ''], :op => { :checklists_status => '=' }, :v => { :checklists_status => ['0'] } - assert_response :success - assert_equal issues_in_list.map(&:id), [1] - - compatible_request :get, :index, :project_id => 1, :set_filter => '1', :f => ['checklists_item', ''], :op => { :checklists_item => '~' }, :v => { :checklists_item => ['todo'] } - assert_response :success - assert_equal issues_in_list.map(&:id), [2, 1] - - compatible_request :get, :index, :project_id => 1, :set_filter => '1', :f => ['checklists_item', ''], :op => { :checklists_item => '~' }, :v => { :checklists_item => ['Third'] } - assert_response :success - assert_equal issues_in_list.map(&:id), [2] - - compatible_request :get, :index, :project_id => 1, :set_filter => '1', :f => ['checklists_item', ''], :op => { :checklists_item => '!*' } - assert_response :success - assert_equal issues_in_list.map(&:id).include?(1), false - assert_equal issues_in_list.map(&:id).include?(2), false + RedmineChecklists::TestCase.prepare end def test_new_issue_without_project @@ -118,64 +97,9 @@ class IssuesControllerTest < ActionController::TestCase compatible_xhr_request :put, :new, :issue => parameters, :project_id => issue.project end assert_response :success - assert_equal 'text/javascript', response.content_type + assert_match 'text/javascript', response.content_type assert_match 'FirstChecklist', response.body end - def test_update_sends_email - Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } - parameters = { :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => 'Third' }, - '1' => { 'is_done' => '1', 'subject' => 'Fourth' }, - '2' => { 'id' => 2, '_destroy' => '1', 'subject' => 'Second todo' } - } } - - @request.session[:user_id] = 1 - issue = Issue.find(1) - EmailAddress.create!(:user_id => 2, :address => 'test@example.com') if Redmine::VERSION.to_s >= '3.0' - - compatible_xhr_request :put, :update, :issue => parameters, :project_id => issue.project, :id => issue.to_param - assert ActionMailer::Base.deliveries.last - email = ActionMailer::Base.deliveries.last - assert_include 'Checklist item [ ] Third added', email.text_part.body.to_s - assert_include 'Checklist item [x] Fourth added', email.text_part.body.to_s - assert_include 'Checklist item deleted (Second todo)', email.text_part.body.to_s - end - - def test_update_send_notification_email - old_events = Setting.notified_events - Setting.notified_events = ['checklist_updated'] - Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } - parameters = { 'checklists_attributes' => { '0' => { 'is_done' => '0', 'subject' => 'Third' }, - '1' => { 'is_done' => '1', 'subject' => 'Fourth' }, - '2' => { 'id' => '2', '_destroy' => '1', 'subject' => 'Second todo' } } } - @request.session[:user_id] = 1 - issue = Issue.find(1) - EmailAddress.create!(:user_id => 2, :address => 'test@example.com', :is_default => true) if Redmine::VERSION.to_s >= '3.0' - - compatible_xhr_request :put, :update, :issue => parameters, :project_id => issue.project, :id => issue.to_param - assert ActionMailer::Base.deliveries.last - email = ActionMailer::Base.deliveries.last - assert_include 'Checklist item [ ] Third added', email.text_part.body.to_s - ensure - Setting.notified_events = old_events - end - - def test_update_status_with_checklist_destroy - issue = Issue.find(1) - initial_status = issue.status_id - closed_status = IssueStatus.where(:is_closed => true).first - with_checklists_settings('block_issue_closing' => '1') do - parameters = { :status_id => closed_status.id, - :checklists_attributes => { '0' => { 'id' => 1, 'is_done' => '1', 'subject' => 'First todo' }, - '1' => { 'id' => 2, '_destroy' => '1', 'subject' => 'Second todo' } } } - - compatible_xhr_request :put, :update, :issue => parameters, :project_id => issue.project, :id => issue.to_param - issue.reload - assert_equal issue.status, closed_status - assert_equal issue.checklists.count, 1 - end - ensure - issue.update_attributes(:status_id => initial_status) - end def test_added_attachment_shows_in_log_once Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } @@ -193,31 +117,6 @@ class IssuesControllerTest < ActionController::TestCase assert_response :redirect assert_equal 1, Journal.last.details.where(:property => 'attachment').count end - def test_update_with_delete_write_to_journal - Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } - @request.session[:user_id] = 1 - issue = Issue.find(1) - EmailAddress.create!(:user_id => 2, :address => 'test@example.com') if Redmine::VERSION.to_s >= '3.0' - - # Create new checklist - compatible_xhr_request :put, :update, - :issue => { :notes => 'fix me', - :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => 'Five' } } }, - :project_id => issue.project, - :id => issue.to_param - assert_response :redirect - issue.reload - # Delete new checklist - compatible_xhr_request :put, :update, - :issue => { :checklists_attributes => { '0' => { 'id' => issue.checklists.max.id, '_destroy' => '1', 'subject' => 'First todo' } } }, - :project_id => issue.project, - :id => issue.to_param - assert_response :redirect - - compatible_request :get, :show, :id => issue.id - assert_response :success - assert_select "#change-#{issue.journals.last.id} .details li", 'Checklist item deleted (Five)' - end def test_history_dont_show_old_format_checklists Setting[:plugin_redmine_checklists] = { :save_log => 1, :issue_done_ratio => 0 } @@ -261,6 +160,24 @@ class IssuesControllerTest < ActionController::TestCase assert_not_nil issue end + def test_create_issue_with_checklists + @request.session[:user_id] = 1 + assert_difference 'Issue.count' do + compatible_request :post, :create, :project_id => 1, :issue => { :tracker_id => 3, + :status_id => 2, + :subject => 'NEW issue with checklists', + :description => 'This is the description', + :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => 'item 001', 'position' => '1' } } + } + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('NEW issue with checklists') + assert_equal 1, issue.checklists.count + assert_equal 'item 001', issue.checklists.last.subject + assert_not_nil issue + end + def test_create_issue_using_json old_value = Setting.rest_api_enabled Setting.rest_api_enabled = '1' @@ -282,89 +199,28 @@ class IssuesControllerTest < ActionController::TestCase ensure Setting.rest_api_enabled = old_value end - def test_save_order_position - parameters = { :checklists_attributes => { '0' => { 'id' => 1, 'is_done' => '0', 'subject' => 'First todo', 'position' => '0' }, - '1' => { 'is_done' => '1', 'subject' => 'New checklist', 'position' => '1' }, - '2' => { 'id' => 2, 'is_done' => '0', 'subject' => 'Second todo', 'position' => '2' } } } - issue = Issue.find(1) + def test_history_displaying_for_checklist @request.session[:user_id] = 1 - compatible_xhr_request :put, :update, :issue => parameters, :project_id => issue.project, :id => issue.to_param - assert_equal 0, Checklist.find(1).position - assert_equal 2, Checklist.find(2).position - assert_equal 1, Checklist.last.position - end + Setting[:plugin_redmine_checklists] = { save_log: 1, issue_done_ratio: 0 } - def test_add_default_project_template_to_issue - @request.session[:user_id] = 1 - @project = Project.find(1) - @template = ChecklistTemplate.create!(:name => 'Default', :template_items => 'def 1', :is_default => true, :user => User.find(1), :project => @project) - compatible_request :get, :new, :project_id => @project.id - assert_response :success - assert_select 'span.checklist-subject', 'def 1' - ensure - @template.destroy - end + issue = Issue.find(1) + journal = issue.journals.create!(user_id: 1) + journal.details.create!(:property => 'attr', + :prop_key => 'checklist', + :old_value => '[ ] TEST', + :value => '[x] TEST') - def test_add_default_tracker_project_template_to_issue + # With permissions @request.session[:user_id] = 1 - @project = Project.find(1) - @tracker = @project.trackers.first - @p_template = ChecklistTemplate.create!(:name => 'Default P', :template_items => 'project 1', :is_default => true, :user => User.find(1), :project => @project) - @t_template = ChecklistTemplate.create!(:name => 'Default T', :template_items => 'tracker 1', :is_default => true, - :tracker_id => @tracker.id, :user => User.find(1), :project => @project) - compatible_request :get, :new, :project_id => @project.id + compatible_request :get, :show, id: issue.id assert_response :success - assert_select 'span.checklist-subject', 'tracker 1' - ensure - @p_template.destroy - @t_template.destroy - end + assert_include 'changed from [ ] TEST to [x] TEST', response.body - def test_apply_default_tracker_template_on_tracker_change - @request.session[:user_id] = 1 - @project = Project.find(1) - @tracker = @project.trackers.first - @t_template = ChecklistTemplate.create!(:name => 'Default T', :template_items => 'tracker-1', :is_default => true, - :tracker_id => @tracker.id, :user => User.find(1), :project => @project) - - parameters = { :tracker_id => @tracker.id + 1, :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => '', '_destroy' => 'false', 'position' => '1', 'id' => '' } } } - - # Tracker without default list - compatible_xhr_request :post, :new, :issue => parameters, :project_id => @project + # Without permissions + @request.session[:user_id] = 5 + compatible_request :get, :show, id: issue.id assert_response :success - assert_no_match %r{tracker-1}, response.body - - # Tracker with default list - compatible_xhr_request :post, :new, :issue => parameters.merge(:tracker_id => @tracker.id), :project_id => @project - assert_response :success - assert_match 'tracker-1', response.body - - # Tracker with custom checklist - compatible_xhr_request :post, :new, :issue => { :tracker_id => @tracker.id, :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => 'CUSTOM', '_destroy' => 'false', 'position' => '1', 'id' => '' } } }, :project_id => @project - assert_response :success - assert_match 'CUSTOM', response.body - ensure - @t_template.destroy - end - - def test_copy_subtask_with_checklits - parent = Issue.find(1) - child = Issue.find(2) - child.parent_issue_id = parent.id - child.fixed_version_id = nil - child.save - - check_attrs = { :checklists_attributes => { '0' => { 'is_done' => '0', 'subject' => 'First todo', '_destroy' => 'false', 'position' => '1', 'id' => '' }, - '1' => { 'is_done' => '1', 'subject' => 'Second todo', '_destroy' => 'false', 'position' => '2', 'id' => '' } } } - compatible_request :post, :create, :copy_from => parent.id, :link_copy => 1, :copy_subtasks => 1, :issue => parent.attributes.except(:created_on, :updated_on).merge(check_attrs) - parent_copy = parent.reload.relations.first.issue_to - assert_not_nil parent_copy - assert_equal 2, parent_copy.checklists.count - child_copy = parent_copy.children.first - assert_not_nil child_copy - assert_equal 1, child_copy.checklists.count - ensure - Issue.find(2).update_attributes(:parent_id => nil, :fixed_version_id => 2) + assert_not_include 'changed from [ ] TEST to [x] TEST', response.body end end diff --git a/plugins/redmine_checklists/test/integration/api_test/checklists_test.rb b/plugins/redmine_checklists/test/integration/api_test/checklists_test.rb index 613b7b5..9ad8c35 100644 --- a/plugins/redmine_checklists/test/integration/api_test/checklists_test.rb +++ b/plugins/redmine_checklists/test/integration/api_test/checklists_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -84,12 +84,12 @@ class Redmine::ApiTest::ChecklistsTest < Redmine::ApiTest::Base assert_equal parameters[:checklist][:subject], checklist.subject assert_response :created - assert_equal 'application/xml', @response.content_type + assert_match 'application/xml', @response.content_type assert_select 'checklist id', :text => checklist.id.to_s end def test_put_checklists_1_xml - parameters = { :checklist => { :subject => 'Item_UPDATED' } } + parameters = { :checklist => { subject: 'Item_UPDATED', is_done: '1' } } assert_no_difference('Checklist.count') do compatible_api_request :put, '/checklists/1.xml', parameters, credentials('admin') @@ -99,12 +99,35 @@ class Redmine::ApiTest::ChecklistsTest < Redmine::ApiTest::Base assert_equal parameters[:checklist][:subject], checklist.subject end + def test_recalculate_ratio_after_multirequests + issue = Issue.find(1) + with_checklists_settings('issue_done_ratio' => '1') do + assert_equal 0, issue.reload.done_ratio + + parameters_array = [ + [1, { :checklist => { subject: 'Item 1', is_done: '1' } }], + [2, { :checklist => { subject: 'Item 2', is_done: '1' } }], + [1, { :checklist => { subject: 'Item 1', is_done: '0' } }], + [2, { :checklist => { subject: 'Item 2', is_done: '1' } }] + ] + + assert_no_difference('Checklist.count') do + parameters_array.each do |params| + compatible_api_request :put, "/checklists/#{params[0]}.xml", params[1], credentials('admin') + assert ['200', '204'].include?(response.code) + end + end + + assert_equal 50, issue.reload.done_ratio + end + end + def test_delete_1_xml assert_difference 'Checklist.count', -1 do compatible_api_request :delete, '/checklists/1.xml', {}, credentials('admin') end - assert_response :ok + assert ['200', '204'].include?(response.code) assert_equal '', @response.body assert_nil Checklist.find_by_id(1) end diff --git a/plugins/redmine_checklists/test/integration/common_issue_test.rb b/plugins/redmine_checklists/test/integration/common_issue_test.rb index c43526a..47520a1 100644 --- a/plugins/redmine_checklists/test/integration/common_issue_test.rb +++ b/plugins/redmine_checklists/test/integration/common_issue_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -53,23 +53,6 @@ class CommonIssueTest < RedmineChecklists::IntegrationTest @issue_1 = Issue.find(1) @checklist_1 = Checklist.find(1) end - def test_checklist_settings - log_user('admin', 'admin') - ChecklistTemplate.create!(:name => 'template1', :template_items => 'item1 item2', :is_public => true, :project => @project_1) - ChecklistTemplate.create!(:name => 'template2', :template_items => 'item1 item2', :is_public => true) - compatible_request :get, '/settings/plugin/redmine_checklists' - assert_response :success - assert_select 'tr.checklist-template', 2 - end - - def test_checklist_project_settings - log_user('admin', 'admin') - ChecklistTemplate.create!(:name => 'template1', :template_items => 'item1 item2', :is_public => true, :project => @project_1) - ChecklistTemplate.create!(:name => 'template2', :template_items => 'item1 item2', :is_public => true) - compatible_request :get, '/projects/ecookbook/settings/checklist_template' - assert_response :success - assert_select 'tr.checklist-template', 2 - end def test_global_search_with_checklist log_user('admin', 'admin') diff --git a/plugins/redmine_checklists/test/test_helper.rb b/plugins/redmine_checklists/test/test_helper.rb index c457c3c..0d949e7 100644 --- a/plugins/redmine_checklists/test/test_helper.rb +++ b/plugins/redmine_checklists/test/test_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -44,11 +44,11 @@ module RedmineChecklists end def with_checklists_settings(options, &block) - Setting.plugin_redmine_checklists.stubs(:[]).returns(nil) - options.each { |k, v| Setting.plugin_redmine_checklists.stubs(:[]).with(k).returns(v) } + original_settings = Setting.plugin_redmine_checklists + Setting.plugin_redmine_checklists = original_settings.merge(Hash[options.map {|k,v| [k, v]}]) yield ensure - options.each { |_k, _v| Setting.plugin_redmine_checklists.unstub(:[]) } + Setting.plugin_redmine_checklists = original_settings end end end @@ -71,6 +71,11 @@ class RedmineChecklists::TestCase end def self.prepare + Role.find([1,2]).each do |r| # For anonymous + r.permissions << :view_checklists + r.save + end + Role.find(1, 2, 3, 4).each do |r| r.permissions << :edit_checklists r.save diff --git a/plugins/redmine_checklists/test/unit/checklist_template_category_test.rb b/plugins/redmine_checklists/test/unit/checklist_template_category_test.rb deleted file mode 100644 index 17ab54b..0000000 --- a/plugins/redmine_checklists/test/unit/checklist_template_category_test.rb +++ /dev/null @@ -1,80 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class ChecklistTemplateCategoryTest < ActiveSupport::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - RedmineChecklists::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_checklists).directory + '/test/fixtures/', [:checklists]) - - def setup - RedmineChecklists::TestCase.prepare - Setting.default_language = 'en' - @project_1 = Project.find(1) - @issue_1 = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 1, - :status_id => 1, :priority => IssuePriority.first, - :subject => 'Invoice Issue 1') - @checklist_1 = Checklist.create(:subject => 'TEST1', :position => 1, :issue => @issue_1) - end - - test "should exist" do - assert ChecklistTemplateCategory, "Checklist template category is not defined" - end - - test "has ordered scope" do - assert ChecklistTemplateCategory.ordered, "Ordered scope not found for checklist template category model" - end - - test "should act as list" do - cat1 = ChecklistTemplateCategory.create!(:name => 'Category 1', :position => 1) - cat2 = ChecklistTemplateCategory.create!(:name => 'Category 2', :position => 2) - assert_equal 'Category 2', ChecklistTemplateCategory.ordered.last.name - - cat2.move_higher - assert_equal ChecklistTemplateCategory.ordered.last.name, 'Category 1' - - cat2.move_to_bottom - assert_equal ChecklistTemplateCategory.ordered.last.name, 'Category 2' - end -end diff --git a/plugins/redmine_checklists/test/unit/checklist_template_test.rb b/plugins/redmine_checklists/test/unit/checklist_template_test.rb deleted file mode 100644 index d26101b..0000000 --- a/plugins/redmine_checklists/test/unit/checklist_template_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class ChecklistTemplateTest < ActiveSupport::TestCase - - def test_save_with_category - ch_temp_cat = ChecklistTemplateCategory.create(:name => 'Category 1', :position => 1) - check_list_template = ChecklistTemplate.new(:name => 'name', :category_id => ch_temp_cat.id, :template_items => 's') - check_list_template.save - assert_equal ch_temp_cat.id, check_list_template.reload.category.id - end -end diff --git a/plugins/redmine_checklists/test/unit/checklist_test.rb b/plugins/redmine_checklists/test/unit/checklist_test.rb index 75d7c92..63e498f 100644 --- a/plugins/redmine_checklists/test/unit/checklist_test.rb +++ b/plugins/redmine_checklists/test/unit/checklist_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -86,4 +86,53 @@ class ChecklistTest < ActiveSupport::TestCase assert_equal "[x] #{@checklist_1.subject}", @checklist_1.info, "Helper info broken" end + def test_should_correct_recalculate_rate + issues = [ + [Issue.create(project_id: 1, tracker_id: 1, author_id: 1, status_id: 1, priority: IssuePriority.first, subject: "TI #1", done_ratio: 0, + checklists_attributes: { + '0' => { subject: 'item 1', is_done: false }, + '1' => { subject: 'item 2', is_done: false }, + '2' => { subject: 'item 3', is_done: true }, + }), + 30], + [Issue.create(project_id: 1, tracker_id: 1, author_id: 1, status_id: 1, priority: IssuePriority.first, subject: "TI #2", done_ratio: 0, + checklists_attributes: { + '0' => { subject: 'item 1', is_done: false }, + '1' => { subject: 'item 2', is_done: true }, + '2' => { subject: 'item 3', is_done: true }, + }), + 60], + [Issue.create(project_id: 1, tracker_id: 1, author_id: 1, status_id: 1, priority: IssuePriority.first, subject: "TI #3", done_ratio: 0, + checklists_attributes: { + '0' => { subject: 'item 1', is_done: true }, + '1' => { subject: 'item 2', is_done: true }, + '2' => { subject: 'item 3', is_done: true }, + }), + 100], + [Issue.create(project_id: 1, tracker_id: 1, author_id: 1, status_id: 1, priority: IssuePriority.first, subject: "TI #4", done_ratio: 0, + checklists_attributes: { + '0' => { subject: 'item 1', is_done: false }, + '1' => { subject: 'item 2', is_done: false }, + '2' => { subject: 'section 1', is_done: true, is_section: true }, + '3' => { subject: 'item 3', is_done: true }, + }), + 30], + [Issue.create(project_id: 1, tracker_id: 1, author_id: 1, status_id: 1, priority: IssuePriority.first, subject: "TI #5", done_ratio: 0, + checklists_attributes: { + '0' => { subject: 'section 1', is_done: true, is_section: true } + }), + 0] + ] + + with_checklists_settings('issue_done_ratio' => '1') do + issues.each do |issue, after_ratio| + assert_equal 0, issue.done_ratio + Checklist.recalc_issue_done_ratio(issue.id) + issue.reload + assert_equal after_ratio, issue.done_ratio + end + end + ensure + issues.each { |issue, ratio| issue.destroy } + end end diff --git a/plugins/redmine_checklists/test/unit/issue_test.rb b/plugins/redmine_checklists/test/unit/issue_test.rb index cbf31d8..8ea4c05 100644 --- a/plugins/redmine_checklists/test/unit/issue_test.rb +++ b/plugins/redmine_checklists/test/unit/issue_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify @@ -76,10 +76,10 @@ class IssueTest < ActiveSupport::TestCase def test_issue_should_close_when_all_checklists_finished with_checklists_settings('block_issue_closing' => '1') do - @checklist_1.update_attributes(:is_done => true) + @checklist_1.update(is_done: true) assert @issue.valid? end ensure - @checklist_1.update_attributes(:is_done => false) + @checklist_1.update(is_done: false) end end diff --git a/plugins/redmine_checklists/test/unit/journal_checklist_history_test.rb b/plugins/redmine_checklists/test/unit/journal_checklist_history_test.rb deleted file mode 100644 index 3e505af..0000000 --- a/plugins/redmine_checklists/test/unit/journal_checklist_history_test.rb +++ /dev/null @@ -1,251 +0,0 @@ -# encoding: utf-8 -# -# This file is a part of Redmine Checklists (redmine_checklists) plugin, -# issue checklists management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_checklists is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_checklists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_checklists. If not, see <http://www.gnu.org/licenses/>. - -require File.expand_path('../../test_helper', __FILE__) - -class JournalChecklistHistoryTest < ActiveSupport::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - RedmineChecklists::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_checklists).directory + '/test/fixtures/', [:checklists]) - - def setup - RedmineChecklists::TestCase.prepare - Setting.default_language = 'en' - @project_1 = Project.find(1) - @issue_1 = Issue.create(:project => @project_1, :tracker_id => 1, :author_id => 1, - :status_id => 1, :priority => IssuePriority.first, - :subject => 'TestIssue') - - @checklist_1 = Checklist.create(:subject => 'TEST1', :position => 1, :issue => @issue_1) - @checklist_2 = Checklist.create(:subject => 'TEST2', :position => 2, :issue => @issue_1, :is_done => true) - @checklist_3 = Checklist.create(:subject => 'TEST3', :position => 3, :issue => @issue_1, :is_done => true) - end - - test 'JournalChecklistHistory exists' do - assert JournalChecklistHistory - end - - test 'JournalChecklistHistory can discover that checklist has been added' do - checklist_set_1 = [@checklist_1, @checklist_2] - checklist_set_2 = [@checklist_1, @checklist_2, @checklist_3] - diff = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).diff - - assert(diff[:added]) - assert_equal(@checklist_3.id, diff[:added].map(&:to_h)[0]['id']) - end - - test 'JournalChecklistHistory can discover that checklist has been removed' do - checklist_set_1 = [@checklist_1, @checklist_2] - checklist_set_2 = [@checklist_1, @checklist_2, @checklist_3] - - diff = JournalChecklistHistory.new(checklist_set_2, checklist_set_1).diff - - assert(diff[:removed]) - assert_equal(@checklist_3.id, diff[:removed].map(&:to_h)[0]['id']) - end - - test 'JournalChecklistHistory can discover that checklist has been renamed' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.subject = 'TEST2_CHANGED' - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_2_dup.id = @checklist_2.id - - checklist_set_2 = [@checklist_1, @checklist_2_dup] - - diff = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).diff - - assert(diff[:renamed]) - assert_equal({ @checklist_2.subject => @checklist_2_dup.subject }, diff[:renamed]) - end - - test 'is able to work with JSON representations' do - checklist_set_1 = [@checklist_1, @checklist_2].to_json - checklist_set_2 = [@checklist_1, @checklist_2, @checklist_3].to_json - - diff = JournalChecklistHistory.new(checklist_set_2, checklist_set_1).diff - - assert(diff[:removed]) - # In fact diff can contain OpenStruct instances instead of Checklists - # But they should have same attributes - assert_equal(@checklist_3.id, diff[:removed].map(&:to_h)[0]['id']) - assert_equal(@checklist_3.subject, diff[:removed].map(&:to_h)[0]['subject']) - assert_equal(@checklist_3.is_done, diff[:removed].map(&:to_h)[0]['is_done']) - end - - test 'JournalChecklistHistory able to create JournalDetail' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.subject = 'TEST2_CHANGED' - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_2_dup.id = @checklist_2.id - checklist_set_2 = [@checklist_2_dup, @checklist_3] - details = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal) - assert_equal details.prop_key, 'checklist' - assert_equal JSON.parse(details.old_value)[1]['subject'], 'TEST2' - assert_equal JSON.parse(details.value)[0]['subject'], 'TEST2_CHANGED' - end - - test 'can define that previous Journal contains only checklist changes' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.subject = 'TEST2_CHANGED' - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_2_dup.id = @checklist_2.id - checklist_set_2 = [@checklist_2_dup, @checklist_3] - @issue_1.init_journal(User.find(1)) - @old_journal = @issue_1.current_journal - @issue_1.current_journal.save! - JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal).save! - assert_equal(1, @issue_1.journals.size) - - @issue_1 = Issue.find(@issue_1.id) - @issue_1.init_journal(User.find(1)) - @issue_1.current_journal.save! - journal_detail = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal) - assert(JournalChecklistHistory.can_fixup?(journal_detail)) - - JournalDetail.new({ - :property => 'attr', - :prop_key => 'color', - :old_value => 'blue', - :value => 'red', - :journal => @old_journal - }).save! - - assert(!JournalChecklistHistory.can_fixup?(journal_detail)) - end - - test 'is able to fixup JournalDetail if previous Journal contains only checklist changes' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.subject = 'TEST2_CHANGED' - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_2_dup.id = @checklist_2.id - checklist_set_2 = [@checklist_2_dup, @checklist_3] - @issue_1.init_journal(User.find(1)) - @old_journal = @issue_1.current_journal - @issue_1.current_journal.save! - JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal).save! - assert_equal(1, @issue_1.journals.size) - - @issue_1 = Issue.find(@issue_1.id) - @issue_1.init_journal(User.find(1)) - @issue_1.current_journal.save! - checklist_set_2 = [@checklist_1, @checklist_3] - journal_detail = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal) - JournalChecklistHistory.fixup(journal_detail) - diff = JournalChecklistHistory.new(JournalDetail.last.old_value, JournalDetail.last.value).diff - - assert_equal(@checklist_3.id, diff[:added].map(&:to_h)[0]['id']) - assert_equal(@checklist_2.id, diff[:removed].map(&:to_h)[0]['id']) - assert_equal(0, diff[:renamed].size) - end - - test 'JournalChecklistHistory detects is_done change to undone' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.is_done = false - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_2_dup.id = @checklist_2.id - checklist_set_2 = [@checklist_1, @checklist_2_dup] - diff = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).diff - - assert(diff[:undone]) - assert_equal(@checklist_2_dup.id, diff[:undone].map(&:to_h)[0]['id']) - end - - test 'JournalChecklistHistory detects is_done change to done' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_1_dup = @checklist_1.dup - @checklist_1_dup.is_done = true - # dup is redefined in ActiveRecord so it nullifies id in return value - # But we are imitating situation when user has modified same instance - @checklist_1_dup.id = @checklist_1.id - checklist_set_2 = [@checklist_1_dup, @checklist_2] - diff = JournalChecklistHistory.new(checklist_set_1, checklist_set_2).diff - - assert(diff[:done]) - assert_equal(@checklist_1_dup.id, diff[:done].map(&:to_h)[0]['id']) - end - - test 'it not fixup after one minute details' do - checklist_set_1 = [@checklist_1, @checklist_2] - - @checklist_2_dup = @checklist_2.dup - @checklist_2_dup.is_done = false - @checklist_2_dup.id = @checklist_2.id - checklist_set_2 = [@checklist_2_dup] - - @issue_1.init_journal(User.find(1)) - @old_journal = @issue_1.current_journal - @issue_1.current_journal.save! - JournalChecklistHistory.new(checklist_set_1, checklist_set_2).journal_details(:journal => @issue_1.current_journal).save! - assert_equal(1, @issue_1.journals.size) - - # Try fixup for fresh data - journal = Journal.new(:journalized => @issue_1, :user => User.find(1)) - detail = JournalDetail.new(:property => 'attr', - :prop_key => 'checklist', - :old_value => checklist_set_1.to_json.to_s, - :value => checklist_set_2.to_json.to_s, - :journal => journal - ) - assert_equal true, JournalChecklistHistory.can_fixup?(detail) - - # Try fixup old data - @issue_1.journals.last.update_attribute(:created_on, Time.zone.now - 10.minutes) - assert_equal false, JournalChecklistHistory.can_fixup?(detail) - end -end diff --git a/plugins/redmine_checklists/test/unit/project_test.rb b/plugins/redmine_checklists/test/unit/project_test.rb index baf52b1..5e8775c 100644 --- a/plugins/redmine_checklists/test/unit/project_test.rb +++ b/plugins/redmine_checklists/test/unit/project_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Checklists (redmine_checklists) plugin, # issue checklists management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_checklists is free software: you can redistribute it and/or modify diff --git a/plugins/redmine_monitoring_controlling b/plugins/redmine_monitoring_controlling deleted file mode 160000 index 3b2d055..0000000 --- a/plugins/redmine_monitoring_controlling +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b2d055a0d24c97e39f8ae45720ad4c4f81b04c4 diff --git a/plugins/redmine_user_specific_theme b/plugins/redmine_user_specific_theme deleted file mode 160000 index 9e3cdc3..0000000 --- a/plugins/redmine_user_specific_theme +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9e3cdc38b8c14ec9550938d4ccc42d86ab8cb12e diff --git a/plugins/redmine_work_time/.hgignore b/plugins/redmine_work_time/.hgignore deleted file mode 100644 index ca4b11c..0000000 --- a/plugins/redmine_work_time/.hgignore +++ /dev/null @@ -1,3 +0,0 @@ -syntax: regexp -.svn -.git diff --git a/plugins/redmine_work_time/README.md b/plugins/redmine_work_time/README.md index 1f37251..00f6186 100644 --- a/plugins/redmine_work_time/README.md +++ b/plugins/redmine_work_time/README.md @@ -1,11 +1,12 @@ -WorkTime is a plugin of Redmine to view and update Spent time by each user. +WorkTime is a Redmine plugin to edit spent time by each user. ### Installation notes ### 0. Setup Redmine -1. Download redmine_work_time-*.zip from https://bitbucket.org/tkusukawa/redmine_work_time/downloads +1. Download redmine_work_time-*.zip from https://github.com/tkusukawa/redmine_work_time/releases 2. Expand the plugin into the plugins directory -3. Migrate plugin: rake redmine:plugins:migrate RAILS_ENV=production +3. Migrate plugin: + $ RAILS_ENV=production bundle exec rake redmine:plugins:migrate 4. Restart Redmine 5. Enable the module on the project setting page. 6. Check the permissions on the Roles and permissions(Administration) @@ -13,5 +14,5 @@ WorkTime is a plugin of Redmine to view and update Spent time by each user. ### Links ### * http://www.redmine.org/plugins/redmine_work_time -* https://bitbucket.org/tkusukawa/redmine_work_time -* http://www.r-labs.org/projects/worktime/ \ No newline at end of file +* https://github.com/tkusukawa/redmine_work_time +* http://www.r-labs.org/projects/worktime/ diff --git a/plugins/redmine_work_time/app/controllers/work_time_controller.rb b/plugins/redmine_work_time/app/controllers/work_time_controller.rb old mode 100755 new mode 100644 index 3a407d1..a706803 --- a/plugins/redmine_work_time/app/controllers/work_time_controller.rb +++ b/plugins/redmine_work_time/app/controllers/work_time_controller.rb @@ -83,6 +83,43 @@ class WorkTimeController < ApplicationController send_data Redmine::CodesetUtil.from_utf8(csv_data, l(:general_csv_encoding)), :type=>"text/csv", :filename=>"member_monthly.csv" end + def member_monthly_data_table + require_login || return + if params.key?(:id) then + find_project + end + prepare_values + make_pack + + csv_data = %Q|""| + (@first_date..@last_date).each do |date| + csv_data << %Q|,"#{date}"| + end + csv_data << "\n" + + @month_pack[:odr_prjs].each do |prj_pack| + next if prj_pack[:count_issues] == 0 + prj_pack[:odr_issues].each do |issue_pack| + next if issue_pack[:count_hours] == 0 + issue = issue_pack[:issue] + + csv_data << %Q|"##{issue.id} #{issue.subject}"| + + (@first_date..@last_date).each do |date| + if issue_pack[:total_by_day].has_key?(date) then + csv_data << %Q|,"#{issue_pack[:total_by_day][date]}"| + else + csv_data << %Q|,""| + end + end + + csv_data << "\n" + end + end + + send_data Redmine::CodesetUtil.from_utf8(csv_data, l(:general_csv_encoding)), :type=>"text/csv", :filename=>"member_monthly_table.csv" + end + def total @message = "" find_project @@ -727,8 +764,12 @@ private next if issue.nil? || !issue.visible? next if !User.current.allowed_to?(:log_time, issue.project) valss.each do |count, vals| - tm_vals = vals.slice! "remaining_hours", "status_id" - tm_vals.merge!(params["new_time_entry_#{issue_id}_#{count}"]) if params.has_key?("new_time_entry_#{issue_id}_#{count}") + tm_vals = vals.except "remaining_hours", "status_id" + if params.has_key?("new_time_entry_#{issue_id}_#{count}") + params["new_time_entry_#{issue_id}_#{count}"].each do |k, v| + tm_vals[k] = v + end + end next if tm_vals["hours"].blank? && vals["remaining_hours"].blank? && vals["status_id"].blank? if tm_vals["hours"].present? then if !tm_vals[:activity_id] then @@ -740,7 +781,7 @@ private append_text += " add time entry of ##{issue.id.to_s}: #{tm_vals[:hours].to_f}h" update_daily_memo(append_text, true) end - new_entry = TimeEntry.new(:project => issue.project, :issue => issue, :user => @this_user, :spent_on => @this_date) + new_entry = TimeEntry.new(:project => issue.project, :issue => issue, :author => User.current, :user => @this_user, :spent_on => @this_date) new_entry.safe_attributes = tm_vals new_entry.save append_error_message_html(@message, hour_update_check_error(new_entry, issue_id)) @@ -757,8 +798,12 @@ private params["time_entry"].each do |id, vals| tm = TimeEntry.find_by_id(id) issue_id = tm.issue.id - tm_vals = vals.slice! "remaining_hours", "status_id" - tm_vals.merge!(params["time_entry_"+id.to_s]) if params.has_key?("time_entry_"+id.to_s) + tm_vals = vals.except "remaining_hours", "status_id" + if params.has_key?("time_entry_"+id.to_s) + params["time_entry_"+id.to_s].each do |k,v| + tm_vals[k] = v + end + end if tm_vals["hours"].blank? then # 工数指定ăŚç©şć–‡ĺ—ă®ĺ ´ĺăŻĺ·Ąć•°é …目を削除 if by_other @@ -1278,20 +1323,10 @@ private next_date = @this_date+1 t1 = Time.local(@this_date.year, @this_date.month, @this_date.day) t2 = Time.local(next_date.year, next_date.month, next_date.day) - isu = Issue.arel_table - jnl = Journal.arel_table - union_sql = Issue.where((isu[:author_id].eq(@this_uid)) - .and( isu[:created_on].gteq(t1)) - .and( isu[:created_on].lt(t2))) - .union( - Issue.joins(isu.join(jnl).on(isu[:id].eq(jnl[:journalized_id])).join_sources.first) - .where(jnl[:journalized_type].eq('Issue') - .and( jnl[:user_id].eq(@this_uid)) - .and( jnl[:created_on].gteq(t1)) - .and( jnl[:created_on].lt(t2)) - ).uniq - ) - issues = Issue.from("#{union_sql.to_sql} issues" ) + issues = Issue.where(["(author_id = :u and created_on >= :t1 and created_on < :t2) or "+ + "id in (select journalized_id from journals where journalized_type = 'Issue' and "+ + "user_id = :u and created_on >= :t1 and created_on < :t2 group by journalized_id)", + {:u => @this_user, :t1 => t1, :t2 => t2}]).all issues.each do |issue| next if @restrict_project && @restrict_project!=issue.project.id diff --git a/plugins/redmine_work_time/app/models/user_issue_month.rb b/plugins/redmine_work_time/app/models/user_issue_month.rb index b9788f7..3543e37 100644 --- a/plugins/redmine_work_time/app/models/user_issue_month.rb +++ b/plugins/redmine_work_time/app/models/user_issue_month.rb @@ -1,3 +1,8 @@ class UserIssueMonth < ActiveRecord::Base - attr_accessible :uid, :issue, :odr + #attr_accessible :uid, :issue, :odr + + #private + #def user_issue_month_params + # params.require(:user_issue_month).permit(:uid, :issue, :odr) + #end end diff --git a/plugins/redmine_work_time/app/models/wt_daily_memo.rb b/plugins/redmine_work_time/app/models/wt_daily_memo.rb index f44b38e..a421394 100644 --- a/plugins/redmine_work_time/app/models/wt_daily_memo.rb +++ b/plugins/redmine_work_time/app/models/wt_daily_memo.rb @@ -1,3 +1,8 @@ class WtDailyMemo < ActiveRecord::Base - attr_accessible :user_id, :day, :created_on, :updated_on, :description + #attr_accessible :user_id, :day, :created_on, :updated_on, :description + + # private + # def wt_daily_memo_params + # params.require(:wt_daily_memo).permit(:user_id, :day, :created_on, :updated_on, :description) + # end end diff --git a/plugins/redmine_work_time/app/models/wt_holidays.rb b/plugins/redmine_work_time/app/models/wt_holidays.rb index 99ac609..0ea5d0c 100644 --- a/plugins/redmine_work_time/app/models/wt_holidays.rb +++ b/plugins/redmine_work_time/app/models/wt_holidays.rb @@ -1,3 +1,8 @@ class WtHolidays < ActiveRecord::Base - attr_accessible :holiday, :created_on, :created_by + #attr_accessible :holiday, :created_on, :created_by + + #private + # def wt_holidays_params + # params.require(:wt_holidays).permit(:holiday, :created_on, :created_by) + # end end diff --git a/plugins/redmine_work_time/app/models/wt_member_order.rb b/plugins/redmine_work_time/app/models/wt_member_order.rb index 7de57bb..9804e6f 100644 --- a/plugins/redmine_work_time/app/models/wt_member_order.rb +++ b/plugins/redmine_work_time/app/models/wt_member_order.rb @@ -1,3 +1,8 @@ class WtMemberOrder < ActiveRecord::Base - attr_accessible :user_id, :position, :prj_id + #attr_accessible :user_id, :position, :prj_id + + # private + # def wt_memver_order_params + # params.require(:wt_memver_order).permit(:user_id, :position, :prj_id) + # end end diff --git a/plugins/redmine_work_time/app/models/wt_project_orders.rb b/plugins/redmine_work_time/app/models/wt_project_orders.rb index 84312c0..da7d2ed 100644 --- a/plugins/redmine_work_time/app/models/wt_project_orders.rb +++ b/plugins/redmine_work_time/app/models/wt_project_orders.rb @@ -1,3 +1,8 @@ class WtProjectOrders < ActiveRecord::Base - attr_accessible :uid, :dsp_prj, :dsp_pos + #attr_accessible :uid, :dsp_prj, :dsp_pos + + # private + # def wt_project_orders_params + # params.require(:wt_project_orders).permit(:uid, :dsp_prj, :dsp_pos) + # end end diff --git a/plugins/redmine_work_time/app/models/wt_ticket_relay.rb b/plugins/redmine_work_time/app/models/wt_ticket_relay.rb index e150b44..f578249 100644 --- a/plugins/redmine_work_time/app/models/wt_ticket_relay.rb +++ b/plugins/redmine_work_time/app/models/wt_ticket_relay.rb @@ -1,3 +1,8 @@ class WtTicketRelay < ActiveRecord::Base - attr_accessible :issue_id, :position, :parent + #attr_accessible :issue_id, :position, :parent + + # private + # def wt_ticket_relay_params + # params.require(:wt_ticket_relay).permit(:issue_id, :position, :parent) + # end end diff --git a/plugins/redmine_work_time/app/views/work_time/_user_month_table.html.erb b/plugins/redmine_work_time/app/views/work_time/_user_month_table.html.erb index 974f4df..0788a31 100644 --- a/plugins/redmine_work_time/app/views/work_time/_user_month_table.html.erb +++ b/plugins/redmine_work_time/app/views/work_time/_user_month_table.html.erb @@ -249,6 +249,10 @@ end onclick="location.href='<%=url_for(@link_params.merge(:action=>"member_monthly_data"))%>'" value="<%=l(:wt_data_download)%>" /> +<input type="button" + onclick="location.href='<%=url_for(@link_params.merge(:action=>"member_monthly_data_table"))%>'" + value="<%=l(:wt_data_download_table)%>" +/> <br/> <script type="text/javascript"> diff --git a/plugins/redmine_work_time/config/locales/ca.yml b/plugins/redmine_work_time/config/locales/ca.yml index 4188bd5..dee1fcd 100644 --- a/plugins/redmine_work_time/config/locales/ca.yml +++ b/plugins/redmine_work_time/config/locales/ca.yml @@ -1,48 +1,49 @@ ca: - work_time: "WorkTime" - wt_update: "Update" - wt_month_names: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec" - wt_week_day_names: "Sun,Mon,Tue,Wed,Thu,Fri,Sat" - wt_monthly_report: "Monthly Report" - wt_daily_report: "Daily Report" - wt_each_member_report: "Member Report" - wt_raw_total: "Monthly Report(not relayed)" - wt_relay_total: "Monthly Report(relayed)" - wt_edit_relay: "Ticket Relay Editor" - wt_ticket: "Ticket" - wt_add_ticket: "Add ticket" - wt_no_permission: "No permission" - wt_loop_relay: "Ticket relation looped" - wt_set_holiday: "set holiday on this date" - wt_del_holiday: "del holiday on this date" - wt_apply_checked: "apply all checked" - wt_select_project: "Restrict project..." - wt_select_user: "Select user..." - wt_edit_memo: "Edit memo" - wt_pre_memo: "pre. memo" - wt_next_memo: "next memo" - wt_data_list: "Data list" - wt_input_ticket_numbers: "or input ticket numbers" - wt_delete_closed_tickets: "delete closed tickets" - wt_data_download: "data download" - wt_data_download_with_act: "data download(each activity)" - wt_opt_disp_ticket_with_closed: "[display closed ticket]" - wt_opt_disp_ticket_opened_only: "[display opened ticket only]" - wt_opt_disp_ticket_with_other_member: "[display other members ticket]" - wt_opt_disp_ticket_mine_only: "[display my ticket only]" - wt_bulkupdate_relations: "bulkupdate to parent" - permission_view_work_time_tab: "View Work time tab" - permission_view_work_time_other_member: "View other members' work time" - permission_edit_work_time_total: "Edit work time totals" - permission_edit_work_time_other_member: "Edit other members' work time" - wt_saved_value: "Saved timed: " - wt_legend: "Legend" - wt_style_default: "By default" - wt_style_assigned: "Assigned" - wt_style_worked: "Worked on" - wt_style__assigned_worked: "Assigned and worked on" - wt_style_overdue: "Overdue" - wt_style_assigned_overdue: "Assigned and overdue" - wt_style_overdue_worked: "Overdue and worked on" - wt_style_assigned_overdue_worked: "Assigned, overdue and worked on" - wt_account_start_day: "Company start day" + work_time: "Agenda" + wt_update: "Actualitzar" + wt_month_names: "Gen,Feb,Mar,Abr,Mai,Jun,Jul,Ago,Sep,Oct,Nov,Dic" + wt_week_day_names: "Diu,Dll,Dm,Dx,Di,Dv,Ds" + wt_monthly_report: "Informe mensual" + wt_daily_report: "Informe diari" + wt_each_member_report: "Informe de membre" + wt_raw_total: "Informe mensual(no se retransmet)" + wt_edit_relay: "Editor transmisiĂł de peticions" + wt_relay_total: "Informe mensual de peticions" + wt_ticket: "PeticiĂł" + wt_add_ticket: "Afegir peticiĂł" + wt_no_permission: "Sense permĂs" + wt_loop_relay: "TransmisiĂł de peticiĂł en bucle" + wt_set_holiday: "Estableix vacances en aquesta data" + wt_del_holiday: "Elimina vacances en aquesta data" + wt_apply_checked: "Aplica a tots els seleccionats" + wt_select_project: "Seleccione projecte..." + wt_select_user: "Selecciona usuari..." + wt_edit_memo: "Edita memorandum" + wt_pre_memo: "Anterior memorandum" + wt_next_memo: "SegĂĽent memorandum" + wt_data_list: "Llista de dades" + wt_input_ticket_numbers: "o introdueix nĂşmero de peticiĂł" + wt_delete_closed_tickets: "borrar tiquests tancats" + wt_data_download: "descarregar dades" + wt_data_download_table: "descarregar dades taula" + wt_data_download_with_act: "descarregar dades(de cada activitat)" + wt_opt_disp_ticket_with_closed: "[mostrar tiquets tancants]" + wt_opt_disp_ticket_opened_only: "[mostrar nomĂ©s tiquets oberts]" + wt_opt_disp_ticket_with_other_member: "[mostrar tiquets d'altres membres]" + wt_opt_disp_ticket_mine_only: "[mostrar nomĂ©s els meus tiquets]" + wt_bulkupdate_relations: "actualizar al pare" + permission_view_work_time_tab: "Veure pestanya temps treballat" + permission_view_work_time_other_member: "Veure el temps treballat d'altres membres" + permission_edit_work_time_total: "Editar temps total treballat" + permission_edit_work_time_other_member: "Editar el tiemps treballat d'altres membres" + wt_saved_value: "Guardar cronometrat: " + wt_legend: "Llegenda" + wt_style_default: "Per defecte" + wt_style_assigned: "Assignat" + wt_style_worked: "Treballat per" + wt_style__assigned_worked: "Assignat i treballat per" + wt_style_overdue: "Atrassat" + wt_style_assigned_overdue: "Assignat i atrassat" + wt_style_overdue_worked: "Atrassat i treballat per" + wt_style_assigned_overdue_worked: "Assignat, atrassat i treballat per" + wt_account_start_day: "Data inici companyia" diff --git a/plugins/redmine_work_time/config/locales/de.yml b/plugins/redmine_work_time/config/locales/de.yml index acdbb8a..396e4af 100644 --- a/plugins/redmine_work_time/config/locales/de.yml +++ b/plugins/redmine_work_time/config/locales/de.yml @@ -25,6 +25,7 @@ de: wt_input_ticket_numbers: "oder Ticket-Nr. direkt eingeben" wt_delete_closed_tickets: "delete closed tickets" wt_data_download: "Daten laden" + wt_data_download_table: "Tabelle Daten laden" wt_data_download_with_act: "Daten laden(each activity)" wt_opt_disp_ticket_with_closed: "[display closed ticket]" wt_opt_disp_ticket_opened_only: "[display opened ticket only]" diff --git a/plugins/redmine_work_time/config/locales/en.yml b/plugins/redmine_work_time/config/locales/en.yml index 020b5e6..4729cf2 100644 --- a/plugins/redmine_work_time/config/locales/en.yml +++ b/plugins/redmine_work_time/config/locales/en.yml @@ -25,6 +25,7 @@ en: wt_input_ticket_numbers: "or input ticket numbers" wt_delete_closed_tickets: "delete closed tickets" wt_data_download: "data download" + wt_data_download_table: "table data download" wt_data_download_with_act: "data download(each activity)" wt_opt_disp_ticket_with_closed: "[display closed ticket]" wt_opt_disp_ticket_opened_only: "[display opened ticket only]" diff --git a/plugins/redmine_work_time/config/locales/es.yml b/plugins/redmine_work_time/config/locales/es.yml index ff94584..840ee72 100644 --- a/plugins/redmine_work_time/config/locales/es.yml +++ b/plugins/redmine_work_time/config/locales/es.yml @@ -23,26 +23,27 @@ es: wt_next_memo: "Siguiente memorandum" wt_data_list: "Lista de datos" wt_input_ticket_numbers: "o introduzca numero de peticiĂłn" - wt_delete_closed_tickets: "delete closed tickets" - wt_data_download: "data download" - wt_data_download_with_act: "data download(each activity)" - wt_opt_disp_ticket_with_closed: "[display closed ticket]" - wt_opt_disp_ticket_opened_only: "[display opened ticket only]" - wt_opt_disp_ticket_with_other_member: "[display other members ticket]" - wt_opt_disp_ticket_mine_only: "[display my ticket only]" - wt_bulkupdate_relations: "bulkupdate to parent" - permission_view_work_time_tab: "View Work time tab" - permission_view_work_time_other_member: "View other members' work time" - permission_edit_work_time_total: "Edit work time totals" - permission_edit_work_time_other_member: "Edit other members' work time" - wt_saved_value: "Saved timed: " - wt_legend: "Legend" - wt_style_default: "By default" - wt_style_assigned: "Assigned" - wt_style_worked: "Worked on" - wt_style__assigned_worked: "Assigned and worked on" - wt_style_overdue: "Overdue" - wt_style_assigned_overdue: "Assigned and overdue" - wt_style_overdue_worked: "Overdue and worked on" - wt_style_assigned_overdue_worked: "Assigned, overdue and worked on" - wt_account_start_day: "Company start day" + wt_delete_closed_tickets: "borrar tiquests cerrados" + wt_data_download: "descargar datos" + wt_data_download_table: "descargar datos tabla" + wt_data_download_with_act: "descargar datos(de cada actividad)" + wt_opt_disp_ticket_with_closed: "[mostrar tiquets cerrados]" + wt_opt_disp_ticket_opened_only: "[mostrar solo tiquets abiertos]" + wt_opt_disp_ticket_with_other_member: "[mostrar tiquets de otros miembros]" + wt_opt_disp_ticket_mine_only: "[mostrar solo mis tiquets]" + wt_bulkupdate_relations: "actualizar al padre" + permission_view_work_time_tab: "Ver pestaña tiempo trabajado" + permission_view_work_time_other_member: "Ver el tiempo trabajado de otros miembros" + permission_edit_work_time_total: "Editar tiempo total trabajado" + permission_edit_work_time_other_member: "Editar el tiempo trabajado de otros miembros" + wt_saved_value: "Guardar cronometrado: " + wt_legend: "Legenda" + wt_style_default: "Por defecto" + wt_style_assigned: "Asignado" + wt_style_worked: "Trabajado por" + wt_style__assigned_worked: "Asignado y trabajado por" + wt_style_overdue: "Atrasado" + wt_style_assigned_overdue: "Asignado y atrasado" + wt_style_overdue_worked: "Atrasado y trabajado por" + wt_style_assigned_overdue_worked: "Asignado, atrasado y trabajado por" + wt_account_start_day: "Fecha inicio compañia" diff --git a/plugins/redmine_work_time/config/locales/fr.yml b/plugins/redmine_work_time/config/locales/fr.yml index 538f5f2..a7b5bcc 100644 --- a/plugins/redmine_work_time/config/locales/fr.yml +++ b/plugins/redmine_work_time/config/locales/fr.yml @@ -25,6 +25,7 @@ fr: wt_input_ticket_numbers: "ou entrez le numĂ©ro de la demande" wt_delete_closed_tickets: "Supprimer les demandes fermĂ©es" wt_data_download: "TĂ©lĂ©chargement des donnĂ©es" + wt_data_download_table: "Tableau de donnĂ©es TĂ©lĂ©chargement" wt_data_download_with_act: "TĂ©lĂ©chargement des donnĂ©es(each activity)" wt_opt_disp_ticket_with_closed: "[afficher les demandes fermĂ©s]" wt_opt_disp_ticket_opened_only: "[n'afficher que les demandes ouvertes]" diff --git a/plugins/redmine_work_time/config/locales/it.yml b/plugins/redmine_work_time/config/locales/it.yml index a1a1b13..2ad1623 100644 --- a/plugins/redmine_work_time/config/locales/it.yml +++ b/plugins/redmine_work_time/config/locales/it.yml @@ -25,6 +25,7 @@ it: wt_input_ticket_numbers: "o inserisci i numeri delle segnalazioni" wt_delete_closed_tickets: "Elimina segnalazioni chiuse" wt_data_download: "download di dati" + wt_data_download_table: "tavolo download di dati" wt_data_download_with_act: "download di dati(each activity)" wt_opt_disp_ticket_with_closed: "[mostra segnalazioni chiuse]" wt_opt_disp_ticket_opened_only: "[mostra solo segnalazioni aperte]" diff --git a/plugins/redmine_work_time/config/locales/ja.yml b/plugins/redmine_work_time/config/locales/ja.yml index cfff85e..1837d0b 100644 --- a/plugins/redmine_work_time/config/locales/ja.yml +++ b/plugins/redmine_work_time/config/locales/ja.yml @@ -25,6 +25,7 @@ ja: wt_input_ticket_numbers: "ăľăźăŻăケăă番号を入力" wt_delete_closed_tickets: "終了ăケăă削除" wt_data_download: "ă‡ăĽă‚żă€ă‚¦ăłăăĽă‰" + wt_data_download_table: "ă‡ăĽă‚żă€ă‚¦ăłăăĽă‰(ćśé–“工数表)" wt_data_download_with_act: "活動ĺĄă‡ăĽă‚żă€ă‚¦ăłăăĽă‰" wt_opt_disp_ticket_with_closed: "[終了ă—ăźăケăăも表示]" wt_opt_disp_ticket_opened_only: "[ă‚ŞăĽă—ăłä¸ă®ăケăăă®ăżčˇ¨ç¤ş]" diff --git a/plugins/redmine_work_time/config/locales/ko.yml b/plugins/redmine_work_time/config/locales/ko.yml index 896436a..240f887 100644 --- a/plugins/redmine_work_time/config/locales/ko.yml +++ b/plugins/redmine_work_time/config/locales/ko.yml @@ -26,6 +26,7 @@ ko: wt_input_ticket_numbers: "ë는 티켓 ë˛í¸ëĄĽ ěž…ë Ąí•ě„¸ěš”." wt_delete_closed_tickets: "ě™„ëŁŚëś í‹°ěĽ“ ě‚ě ś" wt_data_download: "데이터 다운로드" + wt_data_download_table: "í‘ś 데이터 다운로드" wt_data_download_with_act: "데이터 다운로드(each activity)" wt_opt_disp_ticket_with_closed: "[display closed ticket]" wt_opt_disp_ticket_opened_only: "[display opened ticket only]" diff --git a/plugins/redmine_work_time/config/locales/no.yml b/plugins/redmine_work_time/config/locales/no.yml index 6ba4200..913806c 100644 --- a/plugins/redmine_work_time/config/locales/no.yml +++ b/plugins/redmine_work_time/config/locales/no.yml @@ -25,6 +25,7 @@ wt_input_ticket_numbers: "eller legg inn saksnummer" wt_delete_closed_tickets: "Slett lukkede poster" wt_data_download: "Last ned data" + wt_data_download_table: "Tabell nedlasting av data" wt_data_download_with_act: "Last ned data(each activity)" wt_opt_disp_ticket_with_closed: "[vis lukkede saker]" wt_opt_disp_ticket_opened_only: "[vis bare ĂĄpne saker]" diff --git a/plugins/redmine_work_time/config/locales/po.yml b/plugins/redmine_work_time/config/locales/po.yml index 0fab351..8fdc5cb 100644 --- a/plugins/redmine_work_time/config/locales/po.yml +++ b/plugins/redmine_work_time/config/locales/po.yml @@ -25,6 +25,7 @@ po: wt_input_ticket_numbers: "or input ticket numbers" wt_delete_closed_tickets: "delete closed tickets" wt_data_download: "data download" + wt_data_download_table: "Tabela data download" wt_data_download_with_act: "data download(each activity)" wt_opt_disp_ticket_with_closed: "[display closed ticket]" wt_opt_disp_ticket_opened_only: "[display opened ticket only]" diff --git a/plugins/redmine_work_time/config/locales/pt-BR.yml b/plugins/redmine_work_time/config/locales/pt-BR.yml index b097f3d..15beeac 100644 --- a/plugins/redmine_work_time/config/locales/pt-BR.yml +++ b/plugins/redmine_work_time/config/locales/pt-BR.yml @@ -26,6 +26,7 @@ pt-BR: wt_input_ticket_numbers: "ou inclua o nĂşmero da tarefa" wt_delete_closed_tickets: "apagar tarefas fechadas" wt_data_download: "download de dados" + wt_data_download_table: "Tabela download de dados" wt_data_download_with_act: "download de dados(each activity)" wt_opt_disp_ticket_with_closed: "[exibe tarefa fechada]" wt_opt_disp_ticket_opened_only: "[exibe somente tafera aberta]" diff --git a/plugins/redmine_work_time/config/locales/ru.yml b/plugins/redmine_work_time/config/locales/ru.yml index 35dbb1f..39dc975 100644 --- a/plugins/redmine_work_time/config/locales/ru.yml +++ b/plugins/redmine_work_time/config/locales/ru.yml @@ -25,6 +25,7 @@ ru: wt_input_ticket_numbers: "или введите номера задач (через запятŃŃŽ)" wt_delete_closed_tickets: "Ńдалить закрытые задачи" wt_data_download: "ŃŤĐşŃпорт данных" + wt_data_download_table: "ĐĐşŃпорт данных таблицы" wt_data_download_with_act: "ŃŤĐşŃпорт данных(each activity)" wt_opt_disp_ticket_with_closed: "[отобразить закрытые задачи]" wt_opt_disp_ticket_opened_only: "[отобразить только открытые задачи]" diff --git a/plugins/redmine_work_time/config/locales/tr.yml b/plugins/redmine_work_time/config/locales/tr.yml index bff0ae8..28b7408 100644 --- a/plugins/redmine_work_time/config/locales/tr.yml +++ b/plugins/redmine_work_time/config/locales/tr.yml @@ -25,6 +25,7 @@ tr: wt_input_ticket_numbers: "veya iĹź numarası gir" wt_delete_closed_tickets: "kapalı iĹźleri sil" wt_data_download: "veri indir" + wt_data_download_table: "Tablo veri Ä°ndir" wt_data_download_with_act: "veri indir(each activity)" wt_opt_disp_ticket_with_closed: "[kapalı iĹźleri göster]" wt_opt_disp_ticket_opened_only: "[sadece açık iĹźleri göster]" diff --git a/plugins/redmine_work_time/config/locales/zh.yml b/plugins/redmine_work_time/config/locales/zh.yml old mode 100755 new mode 100644 index 6fc4f9c..7c01c58 --- a/plugins/redmine_work_time/config/locales/zh.yml +++ b/plugins/redmine_work_time/config/locales/zh.yml @@ -25,6 +25,7 @@ zh: wt_input_ticket_numbers: "请输入编号" wt_delete_closed_tickets: "ĺ 除已关é—的工作" wt_data_download: "数据下载" + wt_data_download_table: "表数据下载" wt_data_download_with_act: "数据下载ďĽćŚ‰ć´»ĺŠ¨ďĽ‰" wt_opt_disp_ticket_with_closed: "[ćľç¤şĺ·˛ĺ…łé—的工作]" wt_opt_disp_ticket_opened_only: "[只ćľç¤şć‰“开的工作]" diff --git a/plugins/redmine_work_time/db/migrate/001_create_user_issue_months.rb b/plugins/redmine_work_time/db/migrate/001_create_user_issue_months.rb index 19780cb..4bc929c 100644 --- a/plugins/redmine_work_time/db/migrate/001_create_user_issue_months.rb +++ b/plugins/redmine_work_time/db/migrate/001_create_user_issue_months.rb @@ -1,4 +1,4 @@ -class CreateUserIssueMonths < ActiveRecord::Migration +class CreateUserIssueMonths < ActiveRecord::Migration[4.2] def self.up create_table :user_issue_months do |t| t.column :uid, :integer diff --git a/plugins/redmine_work_time/db/migrate/002_create_wt_member_orders.rb b/plugins/redmine_work_time/db/migrate/002_create_wt_member_orders.rb index 9a4884c..4f45857 100644 --- a/plugins/redmine_work_time/db/migrate/002_create_wt_member_orders.rb +++ b/plugins/redmine_work_time/db/migrate/002_create_wt_member_orders.rb @@ -1,4 +1,4 @@ -class CreateWtMemberOrders < ActiveRecord::Migration +class CreateWtMemberOrders < ActiveRecord::Migration[4.2] def self.up create_table :wt_member_orders do |t| t.column :user_id, :integer diff --git a/plugins/redmine_work_time/db/migrate/003_create_wt_ticket_relays.rb b/plugins/redmine_work_time/db/migrate/003_create_wt_ticket_relays.rb index 84099ad..b77ef11 100644 --- a/plugins/redmine_work_time/db/migrate/003_create_wt_ticket_relays.rb +++ b/plugins/redmine_work_time/db/migrate/003_create_wt_ticket_relays.rb @@ -1,4 +1,4 @@ -class CreateWtTicketRelays < ActiveRecord::Migration +class CreateWtTicketRelays < ActiveRecord::Migration[4.2] def self.up create_table :wt_ticket_relays do |t| t.column :issue_id, :integer diff --git a/plugins/redmine_work_time/db/migrate/004_add_prj_to_mem_odr.rb b/plugins/redmine_work_time/db/migrate/004_add_prj_to_mem_odr.rb index fb14bee..431fe31 100644 --- a/plugins/redmine_work_time/db/migrate/004_add_prj_to_mem_odr.rb +++ b/plugins/redmine_work_time/db/migrate/004_add_prj_to_mem_odr.rb @@ -1,4 +1,4 @@ -class AddPrjToMemOdr < ActiveRecord::Migration +class AddPrjToMemOdr < ActiveRecord::Migration[4.2] def self.up add_column :wt_member_orders, :prj_id, :integer, :default => nil end diff --git a/plugins/redmine_work_time/db/migrate/005_create_wt_daily_memos.rb b/plugins/redmine_work_time/db/migrate/005_create_wt_daily_memos.rb index db53882..e529289 100644 --- a/plugins/redmine_work_time/db/migrate/005_create_wt_daily_memos.rb +++ b/plugins/redmine_work_time/db/migrate/005_create_wt_daily_memos.rb @@ -1,4 +1,4 @@ -class CreateWtDailyMemos < ActiveRecord::Migration +class CreateWtDailyMemos < ActiveRecord::Migration[4.2] def self.up create_table :wt_daily_memos do |t| t.column :day, :date diff --git a/plugins/redmine_work_time/db/migrate/006_create_wt_project_orders.rb b/plugins/redmine_work_time/db/migrate/006_create_wt_project_orders.rb index fe65846..a78996d 100644 --- a/plugins/redmine_work_time/db/migrate/006_create_wt_project_orders.rb +++ b/plugins/redmine_work_time/db/migrate/006_create_wt_project_orders.rb @@ -1,4 +1,4 @@ -class CreateWtProjectOrders < ActiveRecord::Migration +class CreateWtProjectOrders < ActiveRecord::Migration[4.2] def self.up create_table :wt_project_orders do |t| t.column :prj, :integer diff --git a/plugins/redmine_work_time/db/migrate/007_create_wt_holidays.rb b/plugins/redmine_work_time/db/migrate/007_create_wt_holidays.rb index a9537f9..0043f09 100644 --- a/plugins/redmine_work_time/db/migrate/007_create_wt_holidays.rb +++ b/plugins/redmine_work_time/db/migrate/007_create_wt_holidays.rb @@ -1,4 +1,4 @@ -class CreateWtHolidays < ActiveRecord::Migration +class CreateWtHolidays < ActiveRecord::Migration[4.2] def self.up create_table :wt_holidays do |t| t.column :holiday, :date diff --git a/plugins/redmine_work_time/db/migrate/008_remove_month_from_user_issue_month.rb b/plugins/redmine_work_time/db/migrate/008_remove_month_from_user_issue_month.rb index 33559ce..26e93f7 100644 --- a/plugins/redmine_work_time/db/migrate/008_remove_month_from_user_issue_month.rb +++ b/plugins/redmine_work_time/db/migrate/008_remove_month_from_user_issue_month.rb @@ -1,4 +1,4 @@ -class RemoveMonthFromUserIssueMonth < ActiveRecord::Migration +class RemoveMonthFromUserIssueMonth < ActiveRecord::Migration[4.2] def self.up remove_column :user_issue_months, :month end diff --git a/plugins/redmine_work_time/db/migrate/009_remove_prj_from_wt_project_orders.rb b/plugins/redmine_work_time/db/migrate/009_remove_prj_from_wt_project_orders.rb index 318bffc..74a1ec8 100644 --- a/plugins/redmine_work_time/db/migrate/009_remove_prj_from_wt_project_orders.rb +++ b/plugins/redmine_work_time/db/migrate/009_remove_prj_from_wt_project_orders.rb @@ -1,4 +1,4 @@ -class RemovePrjFromWtProjectOrders < ActiveRecord::Migration +class RemovePrjFromWtProjectOrders < ActiveRecord::Migration[4.2] def self.up remove_column :wt_project_orders, :prj end diff --git a/plugins/redmine_work_time/init.rb b/plugins/redmine_work_time/init.rb index 5d1caf6..ccb5271 100644 --- a/plugins/redmine_work_time/init.rb +++ b/plugins/redmine_work_time/init.rb @@ -4,7 +4,7 @@ Redmine::Plugin.register :redmine_work_time do name 'Redmine Work Time plugin' author 'Tomohisa Kusukawa' description 'A plugin to view and update TimeEntry by each user' - version '0.3.4' + version '0.4.1' url 'http://www.redmine.org/plugins/redmine_work_time' author_url 'http://about.me/tkusukawa' diff --git a/plugins/redmine_work_time/lib/work_time_projects_helper_patch.rb b/plugins/redmine_work_time/lib/work_time_projects_helper_patch.rb index 6157b27..5d2e14c 100644 --- a/plugins/redmine_work_time/lib/work_time_projects_helper_patch.rb +++ b/plugins/redmine_work_time/lib/work_time_projects_helper_patch.rb @@ -1,22 +1,17 @@ require_dependency 'projects_helper' module WorkTimeProjectsHelperPatch - def self.included base # :nodoc: - base.send :include, ProjectsHelperMethodsWorkTime - base.class_eval do - alias_method_chain :project_settings_tabs, :work_time + module ProjectsHelperPatch + def project_settings_tabs + tabs = super + action = {:name => 'work_time', + :controller => 'work_time', + :action => :show, + :partial => 'settings/work_time_project_settings', :label => :work_time} + tabs << action if User.current.allowed_to?(:edit_work_time_total, @project) + tabs end end end -module ProjectsHelperMethodsWorkTime - def project_settings_tabs_with_work_time - tabs = project_settings_tabs_without_work_time - action = {:name => 'work_time', - :controller => 'work_time', - :action => :show, - :partial => 'settings/work_time_project_settings', :label => :work_time} - tabs << action if User.current.allowed_to?(:edit_work_time_total, @project) - tabs - end -end +ProjectsHelper.prepend WorkTimeProjectsHelperPatch::ProjectsHelperPatch diff --git a/plugins/redmineup_tags/Gemfile.lock b/plugins/redmineup_tags/Gemfile.lock deleted file mode 100644 index a328ca3..0000000 --- a/plugins/redmineup_tags/Gemfile.lock +++ /dev/null @@ -1,108 +0,0 @@ -GEM - specs: - actionmailer (4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.8) - actionview (= 4.2.8) - activesupport (= 4.2.8) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.8) - activesupport (= 4.2.8) - builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.8) - activesupport (= 4.2.8) - globalid (>= 0.3.0) - activemodel (4.2.8) - activesupport (= 4.2.8) - builder (~> 3.1) - activerecord (4.2.8) - activemodel (= 4.2.8) - activesupport (= 4.2.8) - arel (~> 6.0) - activesupport (4.2.8) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - arel (6.0.4) - builder (3.2.3) - concurrent-ruby (1.1.3) - crass (1.0.4) - erubis (2.7.0) - globalid (0.4.1) - activesupport (>= 4.2.0) - i18n (0.7.0) - liquid (2.6.3) - loofah (2.2.3) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.6.6) - mime-types (>= 1.16, < 4) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - mini_portile2 (2.3.0) - minitest (5.11.3) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) - rack (1.6.11) - rack-test (0.6.3) - rack (>= 1.0) - rails (4.2.8) - actionmailer (= 4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) - activemodel (= 4.2.8) - activerecord (= 4.2.8) - activesupport (= 4.2.8) - bundler (>= 1.3.0, < 2.0) - railties (= 4.2.8) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.9) - activesupport (>= 4.2.0, < 5.0) - nokogiri (~> 1.6) - rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (4.2.8) - actionpack (= 4.2.8) - activesupport (= 4.2.8) - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rake (12.3.1) - redmine_crm (0.0.42) - liquid (< 2.6.4) - rails - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (0.20.3) - thread_safe (0.3.6) - tzinfo (1.2.5) - thread_safe (~> 0.1) - -PLATFORMS - ruby - -DEPENDENCIES - redmine_crm - -BUNDLED WITH - 1.17.1 diff --git a/plugins/redmineup_tags/app/controllers/issue_tags_controller.rb b/plugins/redmineup_tags/app/controllers/issue_tags_controller.rb index 7ed8cc7..34bf00c 100644 --- a/plugins/redmineup_tags/app/controllers/issue_tags_controller.rb +++ b/plugins/redmineup_tags/app/controllers/issue_tags_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -20,14 +20,13 @@ class IssueTagsController < ApplicationController unloadable - before_action :find_issues, :only => [:edit, :update] + before_action :find_issues, only: [:edit, :update] def edit return unless User.current.allowed_to?(:edit_tags, @projects.first) @issue_ids = params[:ids] @is_bulk_editing = @issue_ids.size > 1 - @issue_tags = @is_bulk_editing ? [] : @issues.first.tag_list - @most_used_tags = Issue.all_tags(:sort_by => 'count', :order => 'DESC').limit(10) + @most_used_tags = Issue.all_tags(sort_by: 'count', order: 'DESC').limit(10) end def update @@ -39,13 +38,11 @@ class IssueTagsController < ApplicationController return end - Issue.transaction do - @issues.each do |issue| - issue.tag_list = tags - issue.save! - end + if update_tags(@issues, tags) + flash[:notice] = t(:notice_tags_added) + else + flash[:error] = t(:notice_failed_to_add_tags) end - flash[:notice] = t(:notice_tags_added) else flash[:error] = t(:notice_failed_to_add_tags) end @@ -53,6 +50,40 @@ class IssueTagsController < ApplicationController puts e flash[:error] = t(:notice_failed_to_add_tags) ensure - redirect_to_referer_or { render :text => 'Tags updated.', :layout => true } + redirect_to_referer_or { render text: 'Tags updated.', layout: true } + end + + private + + def update_tags(issues, tags) + if tags.present? && issues.size > 1 + add_issues_tags(issues, tags) + else + update_issues_tags(issues, tags) + end + end + + def add_issues_tags(issues, tags) + saved = true + Issue.transaction do + issues.each do |issue| + issue.tag_list = (issue.tag_list + tags).uniq + saved &&= issue.save + raise ActiveRecord::Rollback unless saved + end + end + saved + end + + def update_issues_tags(issues, tags) + saved = true + Issue.transaction do + issues.each do |issue| + issue.tag_list = tags + saved &&= issue.save + raise ActiveRecord::Rollback unless saved + end + end + saved end end diff --git a/plugins/redmineup_tags/app/controllers/tags_controller.rb b/plugins/redmineup_tags/app/controllers/tags_controller.rb index 485fd05..9472886 100644 --- a/plugins/redmineup_tags/app/controllers/tags_controller.rb +++ b/plugins/redmineup_tags/app/controllers/tags_controller.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -20,8 +20,8 @@ class TagsController < ApplicationController unloadable before_action :require_admin - before_action :find_tag, :only => [:edit, :update] - before_action :bulk_find_tags, :only => [:context_menu, :merge, :destroy] + before_action :find_tag, only: [:edit, :update] + before_action :bulk_find_tags, only: [:context_menu, :merge, :destroy] helper :issues_tags @@ -32,11 +32,11 @@ class TagsController < ApplicationController @tags.each do |tag| begin tag.reload.destroy - rescue ::ActiveRecord::RecordNotFound + rescue ::ActiveRecord::RecordNotFound end end - redirect_back_or_default(:controller => 'settings', :action => 'plugin', :id => 'redmineup_tags', :tab => 'manage_tags') + redirect_back_or_default(controller: 'settings', action: 'plugin', id: 'redmineup_tags', tab: 'manage_tags') end def update @@ -44,29 +44,29 @@ class TagsController < ApplicationController if @tag.save flash[:notice] = l(:notice_successful_update) respond_to do |format| - format.html { redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmineup_tags', :tab => 'manage_tags' } + format.html { redirect_to controller: 'settings', action: 'plugin', id: 'redmineup_tags', tab: 'manage_tags' } format.xml {} end else respond_to do |format| - format.html { render :action => 'edit' } + format.html { render action: 'edit' } end end end def context_menu - @tag = @tags.first if (@tags.size == 1) + @tag = @tags.first if @tags.size == 1 @back = back_url - render :layout => false + render layout: false end def merge if request.post? && params[:tag] && params[:tag][:name] RedmineCrm::Tagging.transaction do tag = RedmineCrm::Tag.find_by_name(params[:tag][:name]) || RedmineCrm::Tag.create(params[:tag]) - RedmineCrm::Tagging.where(:tag_id => @tags.map(&:id)).update_all(:tag_id => tag.id) - @tags.select{|t| t.id != tag.id}.each{|t| t.destroy } - redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmineup_tags', :tab => 'manage_tags' + RedmineCrm::Tagging.where(tag_id: @tags.map(&:id)).update_all(tag_id: tag.id) + @tags.select { |t| t.id != tag.id }.each{ |t| t.destroy } + redirect_to controller: 'settings', action: 'plugin', id: 'redmineup_tags', tab: 'manage_tags' end end end @@ -76,7 +76,7 @@ class TagsController < ApplicationController def bulk_find_tags @tags = RedmineCrm::Tag.joins("JOIN #{RedmineCrm::Tagging.table_name} ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id "). select("#{RedmineCrm::Tag.table_name}.*, COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count"). - where(:id => params[:id] ? [params[:id]] : params[:ids]). + where(id: params[:id] ? [params[:id]] : params[:ids]). group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name") raise ActiveRecord::RecordNotFound if @tags.empty? end diff --git a/plugins/redmineup_tags/app/helpers/issues_tags_helper.rb b/plugins/redmineup_tags/app/helpers/issues_tags_helper.rb index 83f526e..ddf543d 100644 --- a/plugins/redmineup_tags/app/helpers/issues_tags_helper.rb +++ b/plugins/redmineup_tags/app/helpers/issues_tags_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -23,21 +23,19 @@ module IssuesTagsHelper def sidebar_tags unless @sidebar_tags @sidebar_tags = [] - if :none != RedmineTags.settings['issues_sidebar'].to_sym - @sidebar_tags = Issue.available_tags({ - :project => @project, - :open_only => (RedmineTags.settings['issues_open_only'].to_i == 1) - }) + projects = [@project] + (@project && Setting.display_subprojects_issues? ? @project.descendants : []) + if RedmineupTags.settings['issues_sidebar'].to_sym != :none + @sidebar_tags = Issue.available_tags(project: @project, + projects: projects, + open_only: (RedmineupTags.settings['issues_open_only'].to_i == 1)) end end @sidebar_tags.to_a end def render_sidebar_tags - render_tags_list(sidebar_tags, { - :show_count => (RedmineTags.settings['issues_show_count'].to_i == 1), - :open_only => (RedmineTags.settings['issues_open_only'].to_i == 1), - :style => RedmineTags.settings['issues_sidebar'].to_sym - }) + render_tags_list(sidebar_tags, show_count: (RedmineupTags.settings['issues_show_count'].to_i == 1), + open_only: (RedmineupTags.settings['issues_open_only'].to_i == 1), + style: RedmineupTags.settings['issues_sidebar'].to_sym) end end diff --git a/plugins/redmineup_tags/app/helpers/tags_helper.rb b/plugins/redmineup_tags/app/helpers/tags_helper.rb index fe09a64..c5d1b84 100644 --- a/plugins/redmineup_tags/app/helpers/tags_helper.rb +++ b/plugins/redmineup_tags/app/helpers/tags_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -27,16 +27,15 @@ module TagsHelper def render_issue_tag_link(tag, options = {}) filters = [[:issue_tags, '=', tag.name]] filters << [:status_id, 'o'] if options[:open_only] - if options[:use_search] - content = link_to(tag, {:controller => "search", :action => "index", :id => @project, :q => tag.name, :wiki_pages => true, :issues => true}) - else - content = link_to_issue_filter tag.name, filters, :project_id => @project - end - if options[:show_count] - content << content_tag('span', "(#{tag.count})", :class => 'tag-count') - end + content = + if options[:use_search] + link_to(tag, controller: 'search', action: 'index', id: @project, q: tag.name, wiki_pages: true, issues: true) + else + link_to_issue_filter tag.name, filters, project_id: @project + end + content << content_tag('span', "(#{tag.count})", class: 'tag-count') if options[:show_count] - style = RedmineTags.settings['issues_use_colors'].to_i > 0 ? {:class => "tag-label-color", :style => "background-color: #{tag_color(tag)}"} : {:class => "tag-label"} + style = RedmineupTags.settings['issues_use_colors'].to_i > 0 ? { class: 'tag-label-color', style: "background-color: #{tag_color(tag)}" } : { class: 'tag-label' } content_tag('span', content, style) end @@ -46,52 +45,51 @@ module TagsHelper end def render_tags_list(tags, options = {}) - unless tags.nil? or tags.empty? + unless tags.nil? || tags.empty? content, style = '', options.delete(:style) tags = tags.all.to_a if tags.respond_to?(:all) - case sorting = "#{RedmineTags.settings['issues_sort_by']}:#{RedmineTags.settings['issues_sort_order']}" - when "name:asc"; tags.sort! { |a,b| a.name <=> b.name } - when "name:desc"; tags.sort! { |a,b| b.name <=> a.name } - when "count:asc"; tags.sort! { |a,b| a.count <=> b.count } - when "count:desc"; tags.sort! { |a,b| b.count <=> a.count } - # Unknown sorting option. Fallback to default one - else - logger.warn "[redmine_tags] Unknown sorting option: <#{sorting}>" - tags.sort! { |a,b| a.name <=> b.name } + case sorting = "#{RedmineupTags.settings['issues_sort_by']}:#{RedmineupTags.settings['issues_sort_order']}" + when 'name:asc' then tags.sort! { |a, b| a.name <=> b.name } + when 'name:desc' then tags.sort! { |a, b| b.name <=> a.name } + when 'count:asc' then tags.sort! { |a, b| a.count <=> b.count } + when 'count:desc' then tags.sort! { |a, b| b.count <=> a.count } + # Unknown sorting option. Fallback to default one + else + logger.warn "[redmine_tags] Unknown sorting option: <#{sorting}>" + tags.sort! { |a, b| a.name <=> b.name } end - if :list == style + if style == :list list_el, item_el = 'ul', 'li' - elsif :simple_cloud == style + elsif style == :simple_cloud list_el, item_el = 'div', 'span' - elsif :cloud == style + elsif style == :cloud list_el, item_el = 'div', 'span' else - raise "Unknown list style" + raise 'Unknown list style' end content = content.html_safe - if :list == style && RedmineTags.settings['issues_sort_by'] == 'name' + if style == :list && RedmineupTags.settings['issues_sort_by'] == 'name' tags.group_by { |tag| tag.name.downcase.first }.each do |letter, grouped_tags| - content << content_tag(item_el, letter.upcase, :class => 'letter', :style => '') + content << content_tag(item_el, letter.upcase, class: 'letter', :style => '') add_tags(style, grouped_tags, content, item_el, options) end else add_tags(style, tags, content, item_el, options) end - content_tag(list_el, content, :class => 'tags-cloud', :style => (:simple_cloud == style ? "text-align: left;" : "")) + content_tag(list_el, content, class: 'tags-cloud', style: (style == :simple_cloud ? 'text-align: left;' : '')) end end -def link_to_issue_filter(title, filters, options = {}) + def link_to_issue_filter(title, filters, options = {}) options.merge! link_to_issue_filter_options(filters) link_to title, options end - # returns hash suitable for passing it to the <tt>to_link</tt> # === parameters # * <i>filters</i> = array of arrays. each child array is an array of strings: @@ -103,12 +101,12 @@ def link_to_issue_filter(title, filters, options = {}) # link_to 'bazbaz', link_to_issue_filter_options filters def link_to_issue_filter_options(filters) options = { - :controller => 'issues', - :action => 'index', - :set_filter => 1, - :fields => [], - :values => {}, - :operators => {} + controller: 'issues', + action: 'index', + set_filter: 1, + fields: [], + values: {}, + operators: {} } filters.each do |f| @@ -125,8 +123,7 @@ def link_to_issue_filter(title, filters, options = {}) def add_tags(style, tags, content, item_el, options) tag_cloud tags, (1..8).to_a do |tag, weight| - content << ' '.html_safe + content_tag(item_el, render_issue_tag_link(tag, options), :class => "tag-nube-#{weight}", :style => (:simple_cloud == style ? "font-size: 1em;" : "")) + " ".html_safe + content << ' '.html_safe + content_tag(item_el, render_issue_tag_link(tag, options), class: "tag-nube-#{weight}", style: (style == :simple_cloud ? 'font-size: 1em;' : '')) + ' '.html_safe end end - end diff --git a/plugins/redmineup_tags/app/views/context_menus/_issues_tags.html.erb b/plugins/redmineup_tags/app/views/context_menus/_issues_tags.html.erb index 53ac386..9923f00 100644 --- a/plugins/redmineup_tags/app/views/context_menus/_issues_tags.html.erb +++ b/plugins/redmineup_tags/app/views/context_menus/_issues_tags.html.erb @@ -1,11 +1,6 @@ -<% if User.current.allowed_to?(:edit_tags, @project) %> - <li class="folder"> - <a href="#" class="submenu"><%= l(:tags) %></a> - <ul> - <li><%= context_menu_link l(:button_add), - edit_issue_tags_path(:ids => @issue_ids), - :remote => true, - :class => 'icon-add' %></li> - </ul> - </li> +<% if User.current.allowed_to?(:edit_tags, @projects || @project) %> + <li><%= context_menu_link l(:label_add_tags), + edit_issue_tags_path(:ids => @issue_ids), + :remote => true, + :class => 'icon icon-add-tags' %></li> <% end %> diff --git a/plugins/redmineup_tags/app/views/issue_tags/_edit_modal.html.erb b/plugins/redmineup_tags/app/views/issue_tags/_edit_modal.html.erb index 12c77a5..121a3cc 100644 --- a/plugins/redmineup_tags/app/views/issue_tags/_edit_modal.html.erb +++ b/plugins/redmineup_tags/app/views/issue_tags/_edit_modal.html.erb @@ -20,12 +20,12 @@ <legend><%= l(:tags) %></legend> <div id="issue_tags"> <%= select2_tag 'issue[tag_list]', - options_for_select(@issue_tags.map{ |tag| [tag, tag] }, @issue_tags), - :multiple => true, - :style => 'width: 100%;', - :url => auto_complete_redmine_tags_path, - :placeholder => @is_bulk_editing ? t(:label_no_change_option) : '+ add tag', - :tags => User.current.allowed_to?(:create_tags, @project) %> + [], + width: '100%', + multiple: true, + url: auto_complete_redmine_tags_url, + placeholder: @is_bulk_editing ? t(:label_no_change_option) : '+ add tag', + tags: User.current.allowed_to?(:create_tags, @project) %> </div> <p class="most_used_tags"> diff --git a/plugins/redmineup_tags/app/views/issues/._tags_sidebar.html.erb.swp b/plugins/redmineup_tags/app/views/issues/._tags_sidebar.html.erb.swp deleted file mode 100644 index 11569ee85b5237b593006f876dc13fe98a470e1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&%}T>S5C`zBCoiJZ7g#F+JtSMLNRhM!Zz6gT1@TbQ?zUY_(<Pf!Jg8US!}stN zyn58T58)&vP=(@I@?RJ>*_nL(Acv3{G_Ow1c-w0-8XJtge}3I;JapLGF=K%-{aDGV zj#QRiI;2wOA_?N91x4a<;e^7(Gnoz(J&Qzf4Y%bTc!$D-=~zd$Tun_s3C32?v0|>( zofX>JR9TTP_pO#{5X9ajnTDQ<2Y=lI@(`$`K$?u)_8M#L@8z%hc4Lcg)-Nkbfro|w z1Rwwb2tWV=5P(2c1rk$ZPxSCB1;A=C*M7~#E+z;-00Izz00bZa0SG_<0uX=z1pc8w zAQ`K#Gj>jq{r@k&|37H|mEwisnc|j0Qe0DHwWkyq5P$##AOHafKmY;|fB*y_0D&qB z_>S*z6-n+oUADL>q@H&f=f2b7F%_t|>|4*9*g2I`a?<K%1>w$7*6_YdtG=K$=F`!9 u7zWmM8b!Oin^iROi=<JgtmQ=_KcA<jCb{pGx1mQ|su4Bu%ifQ&JpT>iCU!Fb diff --git a/plugins/redmineup_tags/app/views/issues/_tags.html.erb b/plugins/redmineup_tags/app/views/issues/_tags.html.erb index 7975133..15a2574 100644 --- a/plugins/redmineup_tags/app/views/issues/_tags.html.erb +++ b/plugins/redmineup_tags/app/views/issues/_tags.html.erb @@ -1,5 +1,5 @@ <% if issue.tag_list.present? %> <%= issue_fields_rows do |rows| - rows.left l(:tags), safe_join(issue.tag_counts.collect{ |t| render_issue_tag_link(t, :show_count => false, :open_only => RedmineTags.settings['issues_open_only'].to_i > 0 ) }, RedmineTags.settings['issues_use_colors'].to_i > 0 ? ' ' : ', ') + rows.left l(:tags), safe_join(issue.tag_counts.collect{ |t| render_issue_tag_link(t, :show_count => false, :open_only => RedmineupTags.settings['issues_open_only'].to_i > 0 ) }, RedmineupTags.settings['issues_use_colors'].to_i > 0 ? ' ' : ', ') end %> <% end %> diff --git a/plugins/redmineup_tags/app/views/issues/_tags_form.html.erb b/plugins/redmineup_tags/app/views/issues/_tags_form.html.erb index 83730d6..865cbb2 100644 --- a/plugins/redmineup_tags/app/views/issues/_tags_form.html.erb +++ b/plugins/redmineup_tags/app/views/issues/_tags_form.html.erb @@ -1,4 +1,4 @@ -<% if User.current.allowed_to?(:edit_tags, @project) %> +<% if User.current.allowed_to?(:edit_tags, (@issue && @issue.project) || @projects || @project) %> <div class="<%= 'splitcontentleft' if defined? form %>"> <p id="issue_tags"> <label><%= l(:tags) %></label> @@ -10,9 +10,9 @@ :multiple => true, :include_hidden => (params[:action] != 'bulk_edit'), :style => 'width: 100%;', - :url => auto_complete_redmine_tags_path, + :url => auto_complete_redmine_tags_url, :placeholder => params[:action] == 'bulk_edit' ? t(:label_no_change_option) : '+ add tag', - :tags => User.current.allowed_to?(:create_tags, @project) %> + :tags => User.current.allowed_to?(:create_tags, @projects || @project) %> <% else %> <%= label_tag :tags, nil, :for => :issue_tag_list %> diff --git a/plugins/redmineup_tags/app/views/issues/_tags_sidebar.html.erb b/plugins/redmineup_tags/app/views/issues/_tags_sidebar.html.erb index 7d20add..966a296 100644 --- a/plugins/redmineup_tags/app/views/issues/_tags_sidebar.html.erb +++ b/plugins/redmineup_tags/app/views/issues/_tags_sidebar.html.erb @@ -1,8 +1,6 @@ -<% if defined? sidebar_tags -%> <% unless sidebar_tags.empty? -%> <div class="sidebar-tags"> <h3><%= l(:tags) %></h3> <%= render_sidebar_tags %> </div> <% end -%> -<% end -%> diff --git a/plugins/redmineup_tags/app/views/tags/context_menu.html.erb b/plugins/redmineup_tags/app/views/tags/context_menu.html.erb index dec5445..31bf288 100644 --- a/plugins/redmineup_tags/app/views/tags/context_menu.html.erb +++ b/plugins/redmineup_tags/app/views/tags/context_menu.html.erb @@ -1,12 +1,12 @@ <ul> <% if @tag -%> <li><%= link_to l(:button_edit), edit_tag_path(@tag), - :class => 'icon-edit' %></li> + :class => 'icon icon-edit' %></li> <% else %> <li><%= link_to l(:issue_tags_button_merge), merge_tags_path(:ids => @tags.collect(&:id)), - :class => 'icon-tags-merge' %></li> + :class => 'icon icon-tags-merge' %></li> <% end %> <li><%= link_to l(:button_delete), tags_path(:ids => @tags.collect(&:id), :back_url => @back), - :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon-del' %></li> + :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del' %></li> </ul> diff --git a/plugins/redmineup_tags/assets/stylesheets/redmine_tags.css b/plugins/redmineup_tags/assets/stylesheets/redmine_tags.css index 9efaaf0..67be79b 100644 --- a/plugins/redmineup_tags/assets/stylesheets/redmine_tags.css +++ b/plugins/redmineup_tags/assets/stylesheets/redmine_tags.css @@ -38,6 +38,7 @@ div.tags-cloud .tag-nube-8 { font-size: 1.5em; } #tr_tags .select2-selection__rendered { padding-left: 8px; } .icon-tags { background-image: url(../images/tag-icon.png);} +.icon-add-tags { background-image: url(../images/tag_blue_add.png);} .most_used_tag { cursor: pointer; color: #169; } .most_used_tag:hover { text-decoration: underline;} diff --git a/plugins/redmineup_tags/config/locales/de.yml b/plugins/redmineup_tags/config/locales/de.yml index e369536..2b57337 100644 --- a/plugins/redmineup_tags/config/locales/de.yml +++ b/plugins/redmineup_tags/config/locales/de.yml @@ -1,39 +1,21 @@ -# This file is a part of redmine_tags -# redMine plugin, that adds tagging support. -# -# German translation for redmine_tags -# by: Terence Miller AKA cforce, <cforce(at)gmx.de>, -# Jörg Jans -# -# Copyright (c) 2010 Terence Miller AKA cforce -# Copyright (c) 2010 Jörg Jans -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - de: tags: Tags field_tags: Tags field_tag_list: Tags + label_add_tags: Tags hinzufĂĽgen + notice_tags_added: Tags hinzugefĂĽgt + notice_failed_to_add_tags: Fehler beim hinzufĂĽgen der Tags setting_issue_tags: Ticket Tags issues_sidebar: Zeige die Tags auf der Sidebar issues_show_count: Zeige die Ticketanzahl an issues_open_only: Zeige nur noch offene Tickets issues_sort_by: Sortiere Tags nach + issues_use_colors: Farben verwenden issue_tags_sidebar_none: Keine issue_tags_sidebar_list: Liste issue_tags_sidebar_cloud: Cloud + issue_tags_sidebar_simple_cloud: Simple Cloud issues_sort_by_name: Name issues_sort_by_count: Anzahl tickets @@ -41,3 +23,14 @@ de: issues_sort_order_desc: Absteigend auto_complete_new_tag: HinzufĂĽgen... + + issue_tags_label_add_tag: + tag hinzufĂĽgen + issue_tags_manage_tags: Tags bearbeiten + issue_tags_tag: Tag + issue_tags_button_merge: zusammenfĂĽhren + issue_tags_label_merge: ausgwählte Tags zusammenfĂĽhren + + tags_suggestion_order: Reihenfolge der Vorschläge + tags_order_by_name: Name + tags_order_by_last_created: zuletzt angelegt + tags_order_by_most_used: am meisten benutzt diff --git a/plugins/redmineup_tags/config/locales/ru.yml b/plugins/redmineup_tags/config/locales/ru.yml index 2ae790e..d381ddc 100644 --- a/plugins/redmineup_tags/config/locales/ru.yml +++ b/plugins/redmineup_tags/config/locales/ru.yml @@ -1,36 +1,15 @@ -# This file is a part of redmine_tags -# redMine plugin, that adds tagging support. -# -# Russian translation for redmine_tags -# by Aleksey V Zapparov AKA ixti -# -# Copyright (c) 2010 Aleksey V Zapparov AKA ixti -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - ru: - tags: Метки - field_tags: Метки - field_tag_list: Метки + tags: Теги + field_tags: Теги + field_tag_list: Теги label_add_tags: Добавить теги notice_tags_added: Теги добавлены notice_failed_to_add_tags: Не ŃдалоŃŃŚ добавить теги - setting_issue_tags: Метки задач + setting_issue_tags: Теги задач issues_sidebar: БоковŃŃŽ панель как issues_show_count: Показать кол-во задач issues_open_only: Только открытые задачи - issues_sort_by: Упорядочить метки по + issues_sort_by: Упорядочить теги по issues_use_colors: ĐŃпользовать цвета issue_tags_sidebar_none: Не показывать @@ -44,7 +23,7 @@ ru: issues_sort_order_desc: по Ńбыванию auto_complete_new_tag: Добавить... - issue_tags_label_add_tag: + добавить ĐĽĐµŃ‚ĐşŃ + issue_tags_label_add_tag: + добавить тег tags_suggestion_order: Порядок поиŃка tags_order_by_name: По названию diff --git a/plugins/redmineup_tags/config/locales/zh.yml b/plugins/redmineup_tags/config/locales/zh.yml index 3e830c1..b83afbb 100644 --- a/plugins/redmineup_tags/config/locales/zh.yml +++ b/plugins/redmineup_tags/config/locales/zh.yml @@ -23,19 +23,35 @@ zh: tags: ć ‡çľ field_tags: ć ‡çľ field_tag_list: ć ‡çľ + label_add_tags: ć·»ĺŠ ć ‡çľ + notice_tags_added: ć·»ĺŠ çš„ć ‡çľ + notice_failed_to_add_tags: ć·»ĺŠ ć ‡çľĺ¤±č´Ą setting_issue_tags: é—®é˘ć ‡çľ issues_sidebar: ĺś¨äľ§čľąć Źćľç¤şć ‡çľ issues_show_count: ćľç¤şé—®é˘č®ˇć•° issues_open_only: ä»…ćľç¤şć‰“ĺĽ€çš„é—®é˘ issues_sort_by: ć ‡çľćŚ‰ä˝•ç§Ťć–ąĺĽŹćŽ’序 + issues_use_colors: 使用颜色 issue_tags_sidebar_none: ć— issue_tags_sidebar_list: ĺ—表ćľç¤ş issue_tags_sidebar_cloud: äş‘ćľç¤ş + issue_tags_sidebar_simple_cloud: 简单云 issues_sort_by_name: ĺŤç§° issues_sort_by_count: é—®é˘č®ˇć•° issues_sort_order_asc: 顺序 issues_sort_order_desc: 倒序 - auto_complete_new_tag: ć·»ĺŠ ć–°ć ‡çľ ... ... + auto_complete_new_tag: ć·»ĺŠ ć–°ć ‡çľ ... + + issue_tags_label_add_tag: + ć·»ĺŠ ć ‡çľ + issue_tags_manage_tags: 管ç†ć ‡çľ + issue_tags_tag: ć ‡çľ + issue_tags_button_merge: ĺ并 + issue_tags_label_merge: ĺĺą¶é€‰ć‹©çš„ć ‡çľ + + tags_suggestion_order: 建议顺序 + tags_order_by_name: ĺŤç§° + tags_order_by_last_created: 最ĺŽĺ›ĺ»şçš„ć ‡çľ + tags_order_by_most_used: ćś€ĺ¸¸ç”¨çš„ć ‡çľ diff --git a/plugins/redmineup_tags/config/routes.rb b/plugins/redmineup_tags/config/routes.rb index f8d54e8..9a4d198 100644 --- a/plugins/redmineup_tags/config/routes.rb +++ b/plugins/redmineup_tags/config/routes.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -18,19 +18,19 @@ # along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. RedmineApp::Application.routes.draw do - match '/issue_tags/auto_complete/:project_id', :to => 'auto_completes#issue_tags', :via => :get, :as => 'auto_complete_issue_tags' - match '/wiki_tags/auto_complete/:project_id', :to => 'auto_completes#wiki_tags', :via => :get, :as => 'auto_complete_wiki_tags' - match 'auto_completes/redmine_tags' => 'auto_completes#redmine_tags', :via => :get, :as => 'auto_complete_redmine_tags' - match '/tags/context_menu', :to => 'tags#context_menu', :as => 'tags_context_menu', :via => [:get, :post] - match '/tags', :controller => 'tags', :action => 'destroy', :via => :delete + match '/issue_tags/auto_complete/:project_id', to: 'auto_completes#issue_tags', via: :get, as: 'auto_complete_issue_tags' + match '/wiki_tags/auto_complete/:project_id', to: 'auto_completes#wiki_tags', via: :get, as: 'auto_complete_wiki_tags' + match 'auto_completes/redmine_tags' => 'auto_completes#redmine_tags', via: :get, as: 'auto_complete_redmine_tags' + match '/tags/context_menu', to: 'tags#context_menu', as: 'tags_context_menu', via: [:get, :post] + match '/tags', controller: 'tags', action: 'destroy', via: :delete end -resources :tags, :only => [:edit, :update] do +resources :tags, only: [:edit, :update] do collection do post :merge get :context_menu, :merge end end -get :edit_issue_tags, :to => 'issue_tags#edit' -post :update_issue_tags, :to => 'issue_tags#update' +get :edit_issue_tags, to: 'issue_tags#edit' +post :update_issue_tags, to: 'issue_tags#update' diff --git a/plugins/redmineup_tags/db/migrate/001_add_tags_and_taggings.rb b/plugins/redmineup_tags/db/migrate/001_add_tags_and_taggings.rb index 8da3ff6..1b6540a 100644 --- a/plugins/redmineup_tags/db/migrate/001_add_tags_and_taggings.rb +++ b/plugins/redmineup_tags/db/migrate/001_add_tags_and_taggings.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify diff --git a/plugins/redmineup_tags/db/migrate/002_create_tags.rb b/plugins/redmineup_tags/db/migrate/002_create_tags.rb index db11635..900be33 100644 --- a/plugins/redmineup_tags/db/migrate/002_create_tags.rb +++ b/plugins/redmineup_tags/db/migrate/002_create_tags.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify diff --git a/plugins/redmineup_tags/doc/CHANGELOG b/plugins/redmineup_tags/doc/CHANGELOG old mode 100755 new mode 100644 index b1a81c0..ffea186 --- a/plugins/redmineup_tags/doc/CHANGELOG +++ b/plugins/redmineup_tags/doc/CHANGELOG @@ -1,9 +1,53 @@ == Redmine Tags plugin changelog Redmine Tags plugin - Tags board plugin for redmine -Copyright (C) 2011-2018 RedmineUP +Copyright (C) 2011-2021 RedmineUP http://www.redmineup.com/ +== 2021-06-14 v2.0.11 + +* Fixed global issue list bug + +== 2021-06-04 v2.0.10 + +* Fixed patches typo + +== 2021-05-31 v2.0.9 + +* Added Redmine 4.2 compatibility +* Changed path to url for autocomplete +* Updated br locale +* Updated zh locale +* Fixed production load error +* Fixed tags column bug +* Fixed copy tags bug +* Fixed global spent time bug +* Fixed tags count bug +* Fixed bulk edit issue tags bug +* Fixed tags link + +== 2019-06-14 v2.0.8 + +* Tags report for issues summary + +== 2019-06-14 v2.0.7 + +* Agile plugin compatibility fixes + +== 2019-05-16 v2.0.6 + +* Added tags to Time entries report +* Added tags to Agile Versions board +* Updated german translation (Werner Maier) +* Fixed Agile plugin compatibility bug +* Fixed adding tags context menu permissions +* Fixed bulk edit issue tags form permissions + +== 2019-04-08 v2.0.5 + + * Changed tags filter by condition AND + * Fixed tags field on issues bulk edit form + == 2018-09-25 v2.0.4 * Fixed bug with Agile board view diff --git a/plugins/redmineup_tags/init.rb b/plugins/redmineup_tags/init.rb index be594ed..6f7810d 100644 --- a/plugins/redmineup_tags/init.rb +++ b/plugins/redmineup_tags/init.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -17,34 +17,32 @@ # You should have received a copy of the GNU General Public License # along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. -requires_redmine_crm :version_or_higher => '0.0.38' rescue raise "\n\033[31mRedmine requires newer redmine_crm gem version.\nPlease update with 'bundle update redmine_crm'.\033[0m" +requires_redmine_crm version_or_higher: '0.0.55' rescue raise "\n\033[31mRedmine requires newer redmine_crm gem version.\nPlease update with 'bundle update redmine_crm'.\033[0m" require 'redmine' -require 'redmine_tags' -TAGS_VERSION_NUMBER = '2.0.4' -TAGS_VERSION_TYPE = "Light version" +TAGS_VERSION_NUMBER = '2.0.11' +TAGS_VERSION_TYPE = 'Light version' Redmine::Plugin.register :redmineup_tags do name "Redmine Tags plugin (#{TAGS_VERSION_TYPE})" author 'RedmineUP' description 'Redmine issues tagging support' version TAGS_VERSION_NUMBER - url 'https://www.redmineup.com/pages/tags' + url 'https://www.redmineup.com/pages/plugins/tags/' author_url 'mailto:support@redmineup.com' - requires_redmine :version_or_higher => '2.4' + requires_redmine version_or_higher: '3.0' - settings :default => { - :issues_sidebar => 'none', - :issues_show_count => 0, - :issues_open_only => 0, - :issues_sort_by => 'name', - :issues_sort_order => 'asc', - :tags_suggestion_order => 'name', - }, :partial => 'tags/settings' + settings default: { issues_sidebar: 'none', + issues_show_count: 0, + issues_open_only: 0, + issues_sort_by: 'name', + issues_sort_order: 'asc', + tags_suggestion_order: 'name' + }, partial: 'tags/settings' - menu :admin_menu, :tags, {:controller => 'settings', :action => 'plugin', :id => "redmineup_tags"}, :caption => :tags, :html => {:class => 'icon'} + menu :admin_menu, :tags, { controller: 'settings', action: 'plugin', id: 'redmineup_tags' }, caption: :tags, html: { class: 'icon' } project_module :issue_tracking do permission :create_tags, {} @@ -52,24 +50,4 @@ Redmine::Plugin.register :redmineup_tags do end end -(Rails.version < '5.1' ? ActionDispatch::Callbacks : ActiveSupport::Reloader).to_prepare do - unless Issue.included_modules.include?(RedmineTags::Patches::IssuePatch) - Issue.send(:include, RedmineTags::Patches::IssuePatch) - end - - [IssuesController, CalendarsController, GanttsController, SettingsController].each do |controller| - RedmineTags::Patches::AddHelpersForIssueTagsPatch.apply(controller) - end - RedmineTags::Patches::AddHelpersForIssueTagsPatch.apply(ImportsController) if Redmine::VERSION.to_s > '3.2' - - base = ActiveSupport::Dependencies::search_for_file('issue_queries_helper') ? IssueQueriesHelper : QueriesHelper - unless base.included_modules.include?(RedmineTags::Patches::QueriesHelperPatch) - base.send(:include, RedmineTags::Patches::QueriesHelperPatch) - end -end - -require 'redmine_tags/hooks/model_issue_hook' -require 'redmine_tags/hooks/views_issues_hook' -require 'redmine_tags/hooks/views_layouts_hook' -require 'redmine_tags/hooks/views_context_menus_hook' -require 'redmine_tags/patches/agile_query_patch' if Redmine::Plugin.installed?(:redmine_agile) && Redmine::Plugin.find(:redmine_agile).version >= '1.4.3' +require 'redmineup_tags' diff --git a/plugins/redmineup_tags/lib/query_tags_column.rb b/plugins/redmineup_tags/lib/query_tags_column.rb index 95ca616..23b4544 100644 --- a/plugins/redmineup_tags/lib/query_tags_column.rb +++ b/plugins/redmineup_tags/lib/query_tags_column.rb @@ -1,7 +1,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify diff --git a/plugins/redmineup_tags/lib/redmine_tags.rb b/plugins/redmineup_tags/lib/redmine_tags.rb deleted file mode 100644 index 1eacd1f..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags.rb +++ /dev/null @@ -1,27 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require 'redmine_tags/patches/auto_completes_controller_patch' -require 'redmine_tags/patches/action_controller_patch' -require 'redmine_tags/patches/issue_query_patch' -require 'query_tags_column' - -module RedmineTags - def self.settings() Setting[:plugin_redmineup_tags].stringify_keys end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/hooks/model_issue_hook.rb b/plugins/redmineup_tags/lib/redmine_tags/hooks/model_issue_hook.rb deleted file mode 100644 index 371b7d2..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/hooks/model_issue_hook.rb +++ /dev/null @@ -1,47 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Hooks - class ModelIssueHook < Redmine::Hook::ViewListener - def controller_issues_edit_before_save(context = {}) - tags_log context - end - - def controller_issues_bulk_edit_before_save(context = {}) - tags_log context - end - - def tags_log(context, create_journal = true) - issue = context[:issue] - params = context[:params] - if params && params[:issue] && !params[:issue][:tag_list].nil? - old_tags = Issue.find(issue.id).tag_list.to_s - new_tags = issue.tag_list.to_s - if create_journal && !(old_tags == new_tags || issue.current_journal.blank?) - issue.current_journal.details << JournalDetail.new(:property => 'attr', - :prop_key => 'tag_list', - :old_value => old_tags, - :value => new_tags) - end - end - end - end - end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_context_menus_hook.rb b/plugins/redmineup_tags/lib/redmine_tags/hooks/views_context_menus_hook.rb deleted file mode 100644 index 33f41b0..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_context_menus_hook.rb +++ /dev/null @@ -1,26 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Hooks - class ViewsContextMenuesHook < Redmine::Hook::ViewListener - render_on :view_issues_context_menu_end, :partial => 'context_menus/issues_tags' - end - end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_issues_hook.rb b/plugins/redmineup_tags/lib/redmine_tags/hooks/views_issues_hook.rb deleted file mode 100644 index 7fa1fec..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_issues_hook.rb +++ /dev/null @@ -1,30 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Hooks - class ViewsIssuesHook < Redmine::Hook::ViewListener - render_on :view_issues_show_details_bottom, :partial => 'issues/tags' - render_on :view_issues_form_details_bottom, :partial => 'issues/tags_form' - render_on :view_issues_sidebar_planning_bottom, :partial => 'issues/tags_sidebar' - render_on :view_issues_bulk_edit_details_bottom, :partial => 'issues/tags_form' - end - end -end - diff --git a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_layouts_hook.rb b/plugins/redmineup_tags/lib/redmine_tags/hooks/views_layouts_hook.rb deleted file mode 100644 index b8d413b..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/hooks/views_layouts_hook.rb +++ /dev/null @@ -1,27 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Hooks - class ViewsLayoutsHook < Redmine::Hook::ViewListener - render_on :view_layouts_base_html_head, partial: 'tags/additional_assets' - render_on :view_layouts_base_body_bottom, partial: 'tags/select2_transformation_rules' - end - end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/action_controller_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/action_controller_patch.rb deleted file mode 100644 index 3d01e8c..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/action_controller_patch.rb +++ /dev/null @@ -1,49 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Patches - module ActionControllerPatch - def self.included(base) - base.extend(ClassMethods) if Rails::VERSION::MAJOR < 4 - - base.class_eval do - end - end - - module ClassMethods - def before_action(*filters, &block) - before_filter(*filters, &block) - end - - def after_action(*filters, &block) - after_filter(*filters, &block) - end - - def skip_before_action(*filters) - skip_before_filter(*filters) - end - end - end - end -end - -unless ActionController::Base.included_modules.include?(RedmineTags::Patches::ActionControllerPatch) - ActionController::Base.send(:include, RedmineTags::Patches::ActionControllerPatch) -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/add_helpers_for_issue_tags_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/add_helpers_for_issue_tags_patch.rb deleted file mode 100644 index d3a5dc8..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/add_helpers_for_issue_tags_patch.rb +++ /dev/null @@ -1,29 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -module RedmineTags - module Patches - module AddHelpersForIssueTagsPatch - def self.apply(controller) - controller.send(:helper, 'tags') - controller.send(:helper, 'issues_tags') - end - end - end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/agile_query_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/agile_query_patch.rb deleted file mode 100644 index 5bc69bc..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/agile_query_patch.rb +++ /dev/null @@ -1,69 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'query' - -module RedmineTags - module Patches - module AgileQueryPatch - def self.included(base) - base.send(:include, InstanceMethods) - base.class_eval do - unloadable - - alias_method :available_filters_without_redmine_tags, :available_filters - alias_method :available_filters, :available_filters_with_redmine_tags - - add_available_column QueryTagsColumn.new(:tags_relations, caption: :tags) - end - end - - module InstanceMethods - def sql_for_issue_tags_field(_field, operator, value) - case operator - when '=', '!' - issues = Issue.tagged_with(value.clone) - when '!*' - issues = Issue.joins(:tags).uniq - else - issues = Issue.tagged_with(RedmineCrm::Tag.all.map(&:to_s), :any => true) - end - - compare = operator.include?('!') ? 'NOT IN' : 'IN' - ids_list = issues.collect(&:id).push(0).join(',') - "( #{Issue.table_name}.id #{compare} (#{ids_list}) ) " - end - - def available_filters_with_redmine_tags - available_filters_without_redmine_tags - selected_tags = [] - if filters['issue_tags'].present? - selected_tags = Issue.all_tags(:project => project, :open_only => RedmineTags.settings['issues_open_only'].to_i == 1). - where(:name => filters['issue_tags'][:values]).map { |c| [c.name, c.name] } - end - add_available_filter('issue_tags', type: :issue_tags, name: l(:tags), values: selected_tags) - end - end - end - end -end - -unless AgileQuery.included_modules.include?(RedmineTags::Patches::AgileQueryPatch) - AgileQuery.send(:include, RedmineTags::Patches::AgileQueryPatch) -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/auto_completes_controller_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/auto_completes_controller_patch.rb deleted file mode 100644 index 3a9e993..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/auto_completes_controller_patch.rb +++ /dev/null @@ -1,56 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'auto_completes_controller' - -module RedmineContacts - module Patches - module AutoCompletesControllerPatch - def self.included(base) - base.send(:include, InstanceMethods) - - base.class_eval do - end - end - - module InstanceMethods - SORTING_FIELDS = { - 'name' => 'name', - 'last_created' => 'created_at', - 'most_used' => 'count' - } - - def redmine_tags - suggestion_order = RedmineTags.settings['tags_suggestion_order'] || 'name' - options = { - :name_like => (params[:q] || params[:term]).to_s.strip, - :sort_by => SORTING_FIELDS[suggestion_order], - :order => (suggestion_order == 'name' ? 'ASC' : 'DESC') - } - @redmine_tags = Issue.all_tags(options).limit(params[:limit] || 10) - render :layout => false, :partial => 'redmine_tags' - end - end - end - end -end - -unless AutoCompletesController.included_modules.include?(RedmineContacts::Patches::AutoCompletesControllerPatch) - AutoCompletesController.send(:include, RedmineContacts::Patches::AutoCompletesControllerPatch) -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/issue_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/issue_patch.rb deleted file mode 100644 index 4f5d869..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/issue_patch.rb +++ /dev/null @@ -1,115 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'issue' - -module RedmineTags - module Patches - module IssuePatch - - def self.included(base) - base.extend(ClassMethods) - - base.class_eval do - include InstanceMethods - - unloadable - rcrm_acts_as_taggable - - alias_method :safe_attributes_without_safe_tags=, :safe_attributes= - alias_method :safe_attributes=, :safe_attributes_with_safe_tags= - - class << self - alias_method :available_tags_without_redmine_tags, :available_tags - alias_method :available_tags, :available_tags_with_redmine_tags - end - - scope :on_project, lambda { |project| - project = project.id if project.is_a? Project - { :conditions => ["#{Project.table_name}.id=?", project] } - } - end - end - - # Class used to represent the tags relations of an issue - class TagsRelations < IssueRelation::Relations - def to_s(*args) - map(&:name).join(', ') - end - end - - module ClassMethods - def available_tags_with_redmine_tags(options = {}) - scope = available_tags_without_redmine_tags(options) - return scope unless options[:open_only] - scope.joins("JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id"). - where("#{IssueStatus.table_name}.is_closed = ?", false) - end - - def all_tags(options = {}) - scope = RedmineCrm::Tag.where({}) - scope = scope.where("LOWER(#{RedmineCrm::Tag.table_name}.name) LIKE LOWER(?)", "%#{options[:name_like]}%") if options[:name_like] - join = [] - join << "JOIN #{RedmineCrm::Tagging.table_name} ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id " - join << "JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{RedmineCrm::Tagging.table_name}.taggable_id - AND #{RedmineCrm::Tagging.table_name}.taggable_type = '#{Issue.name}' " - scope = scope.joins(join.join(' ')) - - columns = [ - "#{RedmineCrm::Tag.table_name}.*", - "COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count" - ] - if options[:sort_by] == 'created_at' - columns << "MIN(#{RedmineCrm::Tagging.table_name}.created_at) AS created_at" - end - scope = scope.select(columns.join(', ')) - - scope = scope.group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name ") - scope = scope.having('COUNT(*) > 0') - - column = options[:sort_by] || "#{RedmineCrm::Tag.table_name}.name" - order = options[:order] || 'ASC' - scope.order("#{column} #{order}") - end - - def allowed_tags?(tags) - allowed_tags = all_tags.map(&:name) - tags.all? { |tag| allowed_tags.include?(tag) } - end - end - - module InstanceMethods - def safe_attributes_with_safe_tags=(attrs, user=User.current) - self.safe_attributes_without_safe_tags = attrs - if attrs && attrs[:tag_list] && user.allowed_to?(:edit_tags, self.project) - tags = attrs[:tag_list].reject(&:empty?) - if user.allowed_to?(:create_tags, self.project) || Issue.allowed_tags?(tags) - self.tag_list = tags - end - end - end - - def tags_relations - TagsRelations.new(self, tags.to_a) - end - end - - end - end -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/issue_query_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/issue_query_patch.rb deleted file mode 100644 index 1b9c1f1..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/issue_query_patch.rb +++ /dev/null @@ -1,87 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'issue_query' - -module RedmineTags - module Patches - module IssueQueryPatch - def self.included(base) - base.send(:include, InstanceMethods) - - base.class_eval do - unloadable - - alias_method :statement_without_redmine_tags, :statement - alias_method :statement, :statement_with_redmine_tags - - alias_method :available_filters_without_redmine_tags, :available_filters - alias_method :available_filters, :available_filters_with_redmine_tags - - add_available_column QueryTagsColumn.new(:tags_relations, caption: :tags) - end - end - - module InstanceMethods - def statement_with_redmine_tags - filter = filters.delete 'issue_tags' - clauses = statement_without_redmine_tags || '' - - if filter - filters['issue_tags'] = filter - - issues = Issue.where({}) - - op = operator_for('issue_tags') - case op - when '=', '!' - issues = issues.tagged_with(values_for('issue_tags').clone) - when '!*' - issues = issues.joins(:tags).uniq - else - issues = issues.tagged_with(RedmineCrm::Tag.all.map(&:to_s), :any => true) - end - - compare = op.include?('!') ? 'NOT IN' : 'IN' - ids_list = issues.collect(&:id).push(0).join(',') - - clauses << ' AND ' unless clauses.empty? - clauses << "( #{Issue.table_name}.id #{compare} (#{ids_list}) ) " - end - - clauses - end - - def available_filters_with_redmine_tags - available_filters_without_redmine_tags - selected_tags = [] - if filters['issue_tags'].present? - selected_tags = Issue.all_tags(:project => project, :open_only => RedmineTags.settings['issues_open_only'].to_i == 1). - where(:name => filters['issue_tags'][:values]).map { |c| [c.name, c.name] } - end - add_available_filter('issue_tags', type: :issue_tags, name: l(:tags), values: selected_tags) - end - end - end - end -end - -unless IssueQuery.included_modules.include?(RedmineTags::Patches::IssueQueryPatch) - IssueQuery.send(:include, RedmineTags::Patches::IssueQueryPatch) -end diff --git a/plugins/redmineup_tags/lib/redmine_tags/patches/queries_helper_patch.rb b/plugins/redmineup_tags/lib/redmine_tags/patches/queries_helper_patch.rb deleted file mode 100644 index c76bb3d..0000000 --- a/plugins/redmineup_tags/lib/redmine_tags/patches/queries_helper_patch.rb +++ /dev/null @@ -1,51 +0,0 @@ -# This file is a part of Redmine Tags (redmine_tags) plugin, -# customer relationship management plugin for Redmine -# -# Copyright (C) 2011-2018 RedmineUP -# http://www.redmineup.com/ -# -# redmine_tags is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# redmine_tags is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with redmine_tags. If not, see <http://www.gnu.org/licenses/>. - -require_dependency 'queries_helper' -if ActiveSupport::Dependencies::search_for_file('issue_queries_helper') - require_dependency 'issue_queries_query' -end - -module RedmineTags - module Patches - module QueriesHelperPatch - def self.included(base) - base.send(:include, InstanceMethods) - - base.class_eval do - unloadable - alias_method :column_value_without_tags, :column_value - alias_method :column_value, :column_value_with_tags - end - end - - module InstanceMethods - include TagsHelper - - def column_value_with_tags(column, list_object, value) - if column.name == :tags_relations && list_object.is_a?(Issue) - [value].flatten.collect{ |t| render_issue_tag_link(t) }.join(RedmineTags.settings['issues_use_colors'].to_i > 0 ? ' ' : ', ').html_safe - else - column_value_without_tags(column, list_object, value) - end - end - end - end - end -end diff --git a/plugins/redmineup_tags/test/functional/auto_completes_controller_test.rb b/plugins/redmineup_tags/test/functional/auto_completes_controller_test.rb index 1ae343d..dd9046f 100644 --- a/plugins/redmineup_tags/test/functional/auto_completes_controller_test.rb +++ b/plugins/redmineup_tags/test/functional/auto_completes_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -42,16 +42,15 @@ class AutoCompletesControllerTest < ActionController::TestCase :custom_fields_projects, :custom_fields_trackers - def setup - @tag = RedmineCrm::Tag.create(:name => 'Test_tag') + @tag = RedmineCrm::Tag.create(name: 'Test_tag') @request.session[:user_id] = 1 end def test_redmine_tags_should_not_be_case_sensitive issue = Issue.find(1) issue.tags << @tag - compatible_request :get, :redmine_tags, :project_id => 'ecookbook', :q => 'te' + compatible_request :get, :redmine_tags, project_id: 'ecookbook', q: 'te' assert_response :success redmine_tags = ActiveSupport::JSON.decode(response.body).map { |item| item['id'] } assert_not_nil redmine_tags @@ -61,7 +60,7 @@ class AutoCompletesControllerTest < ActionController::TestCase def test_contacts_should_return_json issue = Issue.find(1) issue.tags << @tag - compatible_request :get, :redmine_tags, :project_id => 'ecookbook', :q => 'te' + compatible_request :get, :redmine_tags, project_id: 'ecookbook', q: 'te' assert_response :success json = ActiveSupport::JSON.decode(response.body) assert_kind_of Array, json @@ -72,38 +71,38 @@ class AutoCompletesControllerTest < ActionController::TestCase end def test_suggestion_order_default - with_settings :plugin_redmineup_tags => Setting.available_settings['plugin_redmineup_tags']['default'] do - compatible_request :get, :redmine_tags, :project_id => 'ecookbook' + with_settings plugin_redmineup_tags: Setting.available_settings['plugin_redmineup_tags']['default'] do + compatible_request :get, :redmine_tags, project_id: 'ecookbook' end assert_response :success tags = ActiveSupport::JSON.decode(response.body).map { |item| item['id'] } - assert_equal %w(first second third), tags + assert_equal %w[first second third], tags end def test_suggestion_order_name - with_settings :plugin_redmineup_tags => { :tags_suggestion_order => 'name' } do - compatible_request :get, :redmine_tags, :project_id => 'ecookbook' + with_settings plugin_redmineup_tags: { tags_suggestion_order: 'name' } do + compatible_request :get, :redmine_tags, project_id: 'ecookbook' end assert_response :success tags = ActiveSupport::JSON.decode(response.body).map { |item| item['id'] } - assert_equal %w(first second third), tags + assert_equal %w[first second third], tags end def test_suggestion_order_most_used - with_settings :plugin_redmineup_tags => { :tags_suggestion_order => 'most_used' } do - compatible_request :get, :redmine_tags, :project_id => 'ecookbook' + with_settings plugin_redmineup_tags: { tags_suggestion_order: 'most_used' } do + compatible_request :get, :redmine_tags, project_id: 'ecookbook' end assert_response :success tags = ActiveSupport::JSON.decode(response.body).map { |item| item['id'] } - assert_equal %w(second third first), tags + assert_equal %w[second third first], tags end def test_suggestion_order_last_created - with_settings :plugin_redmineup_tags => { :tags_suggestion_order => 'last_created' } do - compatible_request :get, :redmine_tags, :project_id => 'ecookbook' + with_settings plugin_redmineup_tags: { tags_suggestion_order: 'last_created' } do + compatible_request :get, :redmine_tags, project_id: 'ecookbook' end assert_response :success tags = ActiveSupport::JSON.decode(response.body).map { |item| item['id'] } - assert_equal %w(third second first), tags + assert_equal %w[third second first], tags end end diff --git a/plugins/redmineup_tags/test/functional/issue_tags_controller_test.rb b/plugins/redmineup_tags/test/functional/issue_tags_controller_test.rb index 39df2fa..cd517d8 100644 --- a/plugins/redmineup_tags/test/functional/issue_tags_controller_test.rb +++ b/plugins/redmineup_tags/test/functional/issue_tags_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -42,7 +42,6 @@ class IssueTagsControllerTest < ActionController::TestCase :custom_fields_projects, :custom_fields_trackers - def setup @request.env['HTTP_REFERER'] = '/update_issue_tags' @request.session[:user_id] = 2 @@ -52,117 +51,110 @@ class IssueTagsControllerTest < ActionController::TestCase @issue_8 = issues(:issues_008) @issues = [@issue_1, @issue_2, @issue_8] @ids = [1, 2, 8] - @most_used_tags = %w(second third first) + @most_used_tags = %w[second third first] @role = roles(:roles_001) # Manager role @role.add_permission! :edit_tags end def test_should_get_edit_when_one_issue_chose - compatible_xhr_request :get, :edit, :ids => [1] + compatible_xhr_request :get, :edit, ids: [1] assert_response :success - assert_equal 'text/javascript', response.content_type + assert_match 'text/javascript', response.content_type html_form = response.body[/<form.+form>/].delete('\\') - assert_select_in html_form, 'select#issue_tag_list', 1 do - assert_select 'option[selected="selected"]', 2 - assert_select 'option[selected="selected"]', :text => 'second', :count => 1 - assert_select 'option[selected="selected"]', :text => 'third', :count => 1 - end - - assert_select_in html_form, '.most_used_tags', :text => /.+second.+third.+first.+/, :count => 1 do + assert_select_in html_form, '.most_used_tags', text: /.+second.+third.+first.+/, count: 1 do assert_select '.most_used_tag', 3 - @most_used_tags.each { |tag| assert_select '.most_used_tag', :text => tag, :count => 1 } + @most_used_tags.each { |tag| assert_select '.most_used_tag', text: tag, count: 1 } end end def test_should_get_edit_when_several_issues_chose - compatible_xhr_request :get, :edit, :ids => @ids + compatible_xhr_request :get, :edit, ids: @ids assert_response :success - assert_equal 'text/javascript', response.content_type + assert_match 'text/javascript', response.content_type html_form = response.body[/<form.+form>/].delete('\\') - assert_select_in html_form, 'select#issue_tag_list', 1 do - assert_select 'option[selected="selected"]', 0 - end - - assert_select_in html_form, '.most_used_tags', :text => /.+second.+third.+first.+/, :count => 1 do + assert_select_in html_form, '.most_used_tags', text: /.+second.+third.+first.+/, count: 1 do assert_select '.most_used_tag', 3 - @most_used_tags.each { |tag| assert_select '.most_used_tag', :text => tag, :count => 1 } + @most_used_tags.each { |tag| assert_select '.most_used_tag', text: tag, count: 1 } end end def test_should_get_not_found_when_no_ids - compatible_xhr_request :get, :edit, :ids => [] + compatible_xhr_request :get, :edit, ids: [] assert_response :missing - compatible_request :post, :update, :ids => [], :issue => {:tag_list => []} + compatible_request :post, :update, ids: [], issue: { tag_list: [] } assert_response :missing end def test_should_change_issue_tags_empty_tags - compatible_request :post, :update, :ids => [1], :issue => {:tag_list => ['', '', '']} + compatible_request :post, :update, ids: [1], issue: { tag_list: ['', '', ''] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] assert_equal [], @issue_1.tag_list end def test_should_change_issue_tags_no_tags - compatible_request :post, :update, :ids => [1], :issue => {:tag_list => []} + compatible_request :post, :update, ids: [1], issue: { tag_list: [] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] assert_equal [], @issue_1.tag_list end def test_should_change_issue_tags_one_tag - compatible_request :post, :update, :ids => [1], :issue => {:tag_list => %w(first)} + compatible_request :post, :update, ids: [1], issue: { tag_list: %w[first] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] - assert_equal %w(first), @issue_1.tag_list + assert_equal %w[first], @issue_1.tag_list end def test_should_change_issue_tags_several_tags - compatible_request :post, :update, :ids => [1], :issue => {:tag_list => %w(first second third)} + compatible_request :post, :update, ids: [1], issue: { tag_list: %w[first second third] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] - assert_equal %w(first second third), @issue_1.tag_list.sort + assert_equal %w[first second third], @issue_1.tag_list.sort end def test_should_bulk_change_issue_tags_no_tags - compatible_request :post, :update, :ids => @ids, :issue => {:tag_list => []} + compatible_request :post, :update, ids: @ids, issue: { tag_list: [] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] @issues.each { |issue| assert_equal [], issue.tag_list } end def test_should_bulk_change_issue_tags_one_tag - compatible_request :post, :update, :ids => @ids, :issue => {:tag_list => %w(first)} + compatible_request :post, :update, ids: @ids, issue: { tag_list: %w[first] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] - @issues.each { |issue| assert_equal %w(first), issue.tag_list } + + assert_equal %w[first second third], @issue_1.tag_list.sort + assert_equal %w[first second third], @issue_2.tag_list.sort + assert_equal %w[first second], @issue_8.tag_list.sort end def test_should_bulk_change_issue_tags_several_tags - compatible_request :post, :update, :ids => @ids, :issue => {:tag_list => %w(first second third)} + compatible_request :post, :update, ids: @ids, issue: { tag_list: %w[first second third] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] - @issues.each { |issue| assert_equal %w(first second third), issue.tag_list.sort } + @issues.each { |issue| assert_equal %w[first second third], issue.tag_list.sort } end def test_edit_tags_permission tag = 'first' assert_not_equal Issue.find(1).tag_list, [tag] assert Issue.all_tags.map(&:name).include?(tag) - compatible_request :post, :update, :ids => [1], :issue => { :tag_list => [tag] } + compatible_request :post, :update, ids: [1], issue: { tag_list: [tag] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] assert_equal Issue.find(1).tag_list, [tag] @@ -170,9 +162,9 @@ class IssueTagsControllerTest < ActionController::TestCase tag2 = 'second' assert Issue.all_tags.map(&:name).include?(tag2) - compatible_request :post, :update, :ids => [1], :issue => { :tag_list => [tag2] } + compatible_request :post, :update, ids: [1], issue: { tag_list: [tag2] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_failed_to_add_tags), flash[:error] assert_equal Issue.find(1).tag_list, [tag] end @@ -180,23 +172,23 @@ class IssueTagsControllerTest < ActionController::TestCase def test_bulk_edit_tags_permission tag = 'first' assert Issue.all_tags.map(&:name).include?(tag) - compatible_request :post, :update, :ids => [1, 2], :issue => { :tag_list => [tag] } + compatible_request :post, :update, ids: [1, 8], issue: { tag_list: [tag] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] - assert_equal Issue.find(1).tag_list, [tag] - assert_equal Issue.find(2).tag_list, [tag] + assert_equal Issue.find(1).tag_list.sort, %w[first second third] + assert_equal Issue.find(8).tag_list.sort, %w[first second] @role.remove_permission! :edit_tags tag2 = 'second' assert Issue.all_tags.map(&:name).include?(tag2) - compatible_request :post, :update, :ids => [1, 2], :issue => { :tag_list => [tag2] } + compatible_request :post, :update, ids: [1, 8], issue: { tag_list: [tag2] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_failed_to_add_tags), flash[:error] - assert_equal Issue.find(1).tag_list, [tag] - assert_equal Issue.find(2).tag_list, [tag] + assert_equal Issue.find(1).tag_list.sort, %w[first second third] + assert_equal Issue.find(8).tag_list.sort, %w[first second] end def test_create_tags_permission @@ -205,9 +197,9 @@ class IssueTagsControllerTest < ActionController::TestCase assert_not_equal Issue.find(1).tag_list, [new_tag] assert !Issue.all_tags.map(&:name).include?(new_tag) - compatible_request :post, :update, :ids => [1], :issue => { :tag_list => [new_tag] } + compatible_request :post, :update, ids: [1], issue: { tag_list: [new_tag] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_tags_added), flash[:notice] assert_equal Issue.find(1).tag_list, [new_tag] @@ -215,9 +207,9 @@ class IssueTagsControllerTest < ActionController::TestCase new_tag2 = 'disable_create_tags_permission' assert !Issue.all_tags.map(&:name).include?(new_tag2) - compatible_request :post, :update, :ids => [1], :issue => { :tag_list => [new_tag2] } + compatible_request :post, :update, ids: [1], issue: { tag_list: [new_tag2] } assert_response :redirect - assert_redirected_to :action => 'update' + assert_redirected_to action: 'update' assert_equal I18n.t(:notice_failed_to_add_tags), flash[:error] assert_equal Issue.find(1).tag_list, [new_tag] end diff --git a/plugins/redmineup_tags/test/functional/issues_controller_test.rb b/plugins/redmineup_tags/test/functional/issues_controller_test.rb index 31bbd88..b2cd970 100644 --- a/plugins/redmineup_tags/test/functional/issues_controller_test.rb +++ b/plugins/redmineup_tags/test/functional/issues_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -43,8 +43,8 @@ class IssuesControllerTest < ActionController::TestCase :custom_fields_trackers def setup - @tag = RedmineCrm::Tag.create(:name => 'test_tag') - @last_tag = RedmineCrm::Tag.create(:name => 'last_tag') + @tag = RedmineCrm::Tag.create(name: 'test_tag') + @last_tag = RedmineCrm::Tag.create(name: 'last_tag') @request.session[:user_id] = 1 end @@ -54,11 +54,11 @@ class IssuesControllerTest < ActionController::TestCase compatible_request( :get, :index, - :f => ['status_id', 'issue_tags',''], - :op => { :status_id => 'o', :issue_tags => '=' }, - :v => { :issue_tags => ['test_tag'] }, - :c => ['status', 'priority', 'subject', 'tags_relations'], - :project_id => 'ecookbook' + f: ['status_id', 'issue_tags', ''], + op: { status_id: 'o', issue_tags: '=' }, + v: { issue_tags: ['test_tag'] }, + c: ['status', 'priority', 'subject', 'tags_relations'], + project_id: 'ecookbook' ) assert_response :success assert_select 'table.list.issues tr.issue', 1 @@ -74,18 +74,18 @@ class IssuesControllerTest < ActionController::TestCase issue2 = Issue.find(2) issue2.tags << @tag issue2.tags << @last_tag - RedmineTags.stubs(:settings).returns({ 'issues_sidebar' => 'list', + RedmineupTags.stubs(:settings).returns('issues_sidebar' => 'list', 'issues_show_count' => '1', 'issues_sort_by' => 'count', - 'issues_sort_order' => 'desc' }) + 'issues_sort_order' => 'desc') - compatible_request :get, :index, :project_id => 'ecookbook' + compatible_request :get, :index, project_id: 'ecookbook' assert_response :success assert_select '.tag-label', 'test_tag(2)' ensure issue1.tags = [] issue2.tags = [] - RedmineTags.unstub(:settings) + RedmineupTags.unstub(:settings) end def test_get_index_with_sidebar_tags_in_cloud_by_count @@ -96,52 +96,58 @@ class IssuesControllerTest < ActionController::TestCase issue2.tags << @tag issue2.tags << @last_tag - RedmineTags.stubs(:settings).returns({ 'issues_sidebar' => 'cloud', + RedmineupTags.stubs(:settings).returns('issues_sidebar' => 'cloud', 'issues_show_count' => '1', 'issues_sort_by' => 'count', - 'issues_sort_order' => 'desc' }) - compatible_request :get, :index, :project_id => 'ecookbook' + 'issues_sort_order' => 'desc') + compatible_request :get, :index, project_id: 'ecookbook' assert_response :success assert_select '.tag-label', 'last_tag(2)' ensure issue1.tags = [] issue2.tags = [] - RedmineTags.unstub(:settings) + RedmineupTags.unstub(:settings) end def test_should_not_update_without_tag_list - tags = %w(second third) + tags = %w[second third] assert_equal tags, Issue.find(1).tag_list.sort - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1 } + compatible_request :post, :update, id: 1, issue: { project_id: 1 } assert_response :redirect assert_equal tags, Issue.find(1).tag_list.sort end def test_should_update_with_empty_string_tags - assert_equal %w(second third), Issue.find(1).tag_list.sort - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :tag_list => ['', ''] } + assert_equal %w[second third], Issue.find(1).tag_list.sort + compatible_request :post, :update, id: 1, issue: { project_id: 1, tag_list: ['', ''] } assert_response :redirect assert_equal [], Issue.find(1).tag_list end def test_should_update_with_new_tags - assert_equal %w(second third), Issue.find(1).tag_list.sort - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :tag_list => %w(new_tag1 new_tag2) } + assert_equal %w[second third], Issue.find(1).tag_list.sort + compatible_request :post, :update, id: 1, issue: { project_id: 1, tag_list: %w[new_tag1 new_tag2] } assert_response :redirect - assert_equal %w(new_tag1 new_tag2), Issue.find(1).tag_list.sort + assert_equal %w[new_tag1 new_tag2], Issue.find(1).tag_list.sort end def test_should_update_issue_and_tags - assert_equal %w(second third), Issue.find(1).tag_list.sort - compatible_request :post, :update, :id => 1, :issue => { - :project_id => 1, - :description => 'Test should update issue and tags', - :tag_list => %w(new_tag1 new_tag2) + assert_equal %w[second third], Issue.find(1).tag_list.sort + compatible_request :post, :update, id: 1, issue: { + project_id: 1, + description: 'Test should update issue and tags', + tag_list: %w[new_tag1 new_tag2] } assert_response :redirect issue = Issue.find(1) assert_equal 'Test should update issue and tags', issue.description - assert_equal %w(new_tag1 new_tag2), issue.tag_list.sort + assert_equal %w[new_tag1 new_tag2], issue.tag_list.sort + end + + def test_get_bulk_edit_with_tags + compatible_request :get, :bulk_edit, ids: [1, 2] + assert_select '#issue_tags' + assert_response :success end def test_post_bulk_edit_without_tag_list @@ -151,19 +157,19 @@ class IssuesControllerTest < ActionController::TestCase issue2 = Issue.find(2) issue2.tags = [@last_tag] - compatible_request :post, :bulk_update, :ids => [1, 2], :issue => { :project_id => '', :tracker_id => '' } + compatible_request :post, :bulk_update, ids: [1, 2], issue: { project_id: '', tracker_id: '' } assert_response :redirect assert_equal [@tag.name], Issue.find(1).tag_list assert_equal [@last_tag.name], Issue.find(2).tag_list ensure issue1.tags = [] issue2.tags = [] - RedmineTags.unstub(:settings) + RedmineupTags.unstub(:settings) end def test_post_bulk_edit_with_empty_string_tags - (1..2).each { |i| assert_equal %w(second third), Issue.find(i).tag_list.sort } - compatible_request :post, :bulk_update, :ids => [1, 2], :issue => { :project_id => '', :tracker_id => '', :tag_list => ['', ''] } + (1..2).each { |i| assert_equal %w[second third], Issue.find(i).tag_list.sort } + compatible_request :post, :bulk_update, ids: [1, 2], issue: { project_id: '', tracker_id: '', tag_list: ['', ''] } assert_response :redirect (1..2).each { |i| assert_equal [], Issue.find(i).tag_list } end @@ -175,16 +181,68 @@ class IssuesControllerTest < ActionController::TestCase issue2 = Issue.find(2) issue2.tags << @last_tag - compatible_request :post, :bulk_update, :ids => [1, 2], :issue => { :project_id => '', :tracker_id => '', :tag_list => ['bulk_tag'] } + compatible_request :post, :bulk_update, ids: [1, 2], issue: { project_id: '', tracker_id: '', tag_list: ['bulk_tag'] } assert_response :redirect assert_equal ['bulk_tag'], Issue.find(1).tag_list assert_equal ['bulk_tag'], Issue.find(2).tag_list ensure issue1.tags = [] issue2.tags = [] - RedmineTags.unstub(:settings) + RedmineupTags.unstub(:settings) + end + + def test_get_new_with_permission_edit_tags + # User(id: 2) has role Manager in Project(id: 1) + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.add_permission! :edit_tags + compatible_request :get, :new, issue: { project_id: 1 } + assert_select '#issue_tags' + end + + def test_get_new_without_permission_edit_tags + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.remove_permission! :edit_tags + compatible_request :get, :new, issue: { project_id: 1 } + assert_select '#issue_tags', 0 + end + + def test_get_new_with_permission_edit_tags_in_other_project + # User(id: 2) has role Manager in Project(id: 1) and role Developer in Project(id: 2) + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.add_permission! :edit_tags + compatible_request :get, :new, issue: { project_id: 2 } + assert_select '#issue_tags', 0 + end + + def test_get_edit_with_permission_edit_tags + # User(id: 2) has role Manager in Project(id: 1) and Project(id: 1) contains Issue(id: 1) + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.add_permission! :edit_tags + compatible_request :get, :edit, id: 1, issue: { project_id: 1 } + assert_select '#issue_tags' end + def test_get_edit_without_permission_edit_tags + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.remove_permission! :edit_tags + compatible_request :get, :edit, id: 1, issue: { project_id: 1 } + assert_select '#issue_tags', 0 + end + + def test_get_edit_with_permission_edit_tags_in_other_project + # User(id: 2) has role Manager in Project(id: 1) and role Developer in Project(id: 2) + # Project(id: 1) contains Issue(id: 1) + @request.session[:user_id] = 2 + manager_role = Role.find(1) + manager_role.add_permission! :edit_tags + compatible_request :get, :edit, id: 1, issue: { project_id: 2 } + assert_select '#issue_tags', 0 + end def test_edit_tags_permission # User(id: 2) has role Manager in Project(id: 1) and Project(id: 1) contains Issue(id: 1) @@ -195,7 +253,7 @@ class IssuesControllerTest < ActionController::TestCase assert_not_equal Issue.find(1).tag_list, [tag] assert Issue.all_tags.map(&:name).include?(tag) - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :tag_list => [tag] } + compatible_request :post, :update, id: 1, issue: { project_id: 1, tag_list: [tag] } assert_response :redirect assert_equal Issue.find(1).tag_list, [tag] @@ -204,7 +262,7 @@ class IssuesControllerTest < ActionController::TestCase assert Issue.all_tags.map(&:name).include?(tag2) assert_equal Issue.find(1).description, 'Unable to print recipes' - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :description => 'New description', :tag_list => [tag2] } + compatible_request :post, :update, id: 1, issue: { project_id: 1, description: 'New description', tag_list: [tag2] } assert_response :redirect issue = Issue.find(1) assert_equal 'New description', issue.description @@ -219,7 +277,7 @@ class IssuesControllerTest < ActionController::TestCase assert_not_equal Issue.find(1).tag_list, [new_tag] assert !Issue.all_tags.map(&:name).include?(new_tag) # The project should not contain the new tag - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :tag_list => [new_tag] } + compatible_request :post, :update, id: 1, issue: { project_id: 1, tag_list: [new_tag] } assert_response :redirect assert_equal [new_tag], Issue.find(1).tag_list @@ -227,8 +285,27 @@ class IssuesControllerTest < ActionController::TestCase new_tag2 = 'disable_create_tags_permission' assert !Issue.all_tags.map(&:name).include?(new_tag2) - compatible_request :post, :update, :id => 1, :issue => { :project_id => 1, :tag_list => [new_tag2] } + compatible_request :post, :update, id: 1, issue: { project_id: 1, tag_list: [new_tag2] } assert_response :redirect assert_equal Issue.find(1).tag_list, [new_tag] end + + def test_filter_by_tags_equal + tags = %w(first second) + compatible_request :get, :index, project_id: 1, set_filter: 1, f: ['issue_tags', ''], op: { issue_tags: '=' }, v: { issue_tags: tags } + assert_response :success + issues_in_list.each { |issue| assert_equal (tags & issue.tag_list), tags } + end + + def test_filter_by_tags_not_equal + tags = %w(first second) + compatible_request :get, :index, project_id: 1, set_filter: 1, f: ['issue_tags', ''], op: { issue_tags: '!' }, v: { issue_tags: tags } + assert_response :success + issues_in_list.each { |issue| assert_not_equal (tags & issue.tag_list), tags } + end + + def test_get_index_without_project + compatible_request(:get, :index) + assert_response :success + end end diff --git a/plugins/redmineup_tags/test/functional/tags_controller_test.rb b/plugins/redmineup_tags/test/functional/tags_controller_test.rb index 7e415b1..7143c80 100644 --- a/plugins/redmineup_tags/test/functional/tags_controller_test.rb +++ b/plugins/redmineup_tags/test/functional/tags_controller_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -42,7 +42,6 @@ class TagsControllerTest < ActionController::TestCase :custom_fields_projects, :custom_fields_trackers - def setup # run as the admin @request.session[:user_id] = 1 @@ -50,16 +49,16 @@ class TagsControllerTest < ActionController::TestCase @project_a = Project.generate! @project_b = Project.generate! - add_issue @project_a, %w{a1 a2}, false - add_issue @project_a, %w{a2 a3}, false - add_issue @project_a, %w{a4 a5}, true - add_issue @project_b, %w{b6 b7}, true - add_issue @project_b, %w{b8 b9}, false + add_issue @project_a, %w[a1 a2], false + add_issue @project_a, %w[a2 a3], false + add_issue @project_a, %w[a4 a5], true + add_issue @project_b, %w[b6 b7], true + add_issue @project_b, %w[b8 b9], false end def test_should_get_edit tag = RedmineCrm::Tag.find_by_name('a1') - compatible_request :get, :edit, :id => tag.id + compatible_request :get, :edit, id: tag.id assert_response :success assert_select "input#tag_name[value='#{tag.name}']", 1 end @@ -67,37 +66,37 @@ class TagsControllerTest < ActionController::TestCase def test_should_put_update tag1 = RedmineCrm::Tag.find_by_name('a1') new_name = 'updated main' - compatible_request :put, :update, :id => tag1.id, :tag => { :name => new_name } - assert_redirected_to :controller => 'settings', :action => 'plugin', :id => 'redmineup_tags', :tab => 'manage_tags' + compatible_request :put, :update, id: tag1.id, tag: { name: new_name } + assert_redirected_to controller: 'settings', action: 'plugin', id: 'redmineup_tags', tab: 'manage_tags' tag1.reload assert_equal new_name, tag1.name end - test "should delete destroy" do - tag1 = RedmineCrm::Tag.find_by_name("a1") + test 'should delete destroy' do + tag1 = RedmineCrm::Tag.find_by_name('a1') assert_difference 'RedmineCrm::Tag.count', -1 do - compatible_request :post, :destroy, :ids => tag1.id + compatible_request :post, :destroy, ids: tag1.id assert_response 302 end end - test "should post merge" do - tag1 = RedmineCrm::Tag.find_by_name("a1") - tag2 = RedmineCrm::Tag.find_by_name("b8") + test 'should post merge' do + tag1 = RedmineCrm::Tag.find_by_name('a1') + tag2 = RedmineCrm::Tag.find_by_name('b8') assert_difference 'RedmineCrm::Tag.count', -1 do - compatible_request :post, :merge, :ids => [tag1.id, tag2.id], :tag => {:name => "a1"} - assert_redirected_to :controller => 'settings', :action => 'plugin', :id => "redmineup_tags", :tab => "manage_tags" + compatible_request :post, :merge, ids: [tag1.id, tag2.id], tag: { name: 'a1' } + assert_redirected_to controller: 'settings', action: 'plugin', id: 'redmineup_tags', tab: 'manage_tags' end - assert_equal 0, Issue.tagged_with("b8").count - assert_equal 2, Issue.tagged_with("a1").count + assert_equal 0, Issue.tagged_with('b8').count + assert_equal 2, Issue.tagged_with('a1').count end private def add_issue(project, tags, closed) - issue = Issue.generate!(:project_id => project.id) + issue = Issue.generate!(project_id: project.id) issue.tag_list = tags - issue.status = IssueStatus.where(:is_closed => true).first if closed + issue.status = IssueStatus.where(is_closed: true).first if closed issue.save end end diff --git a/plugins/redmineup_tags/test/test_helper.rb b/plugins/redmineup_tags/test/test_helper.rb index 78d94c8..a89955b 100644 --- a/plugins/redmineup_tags/test/test_helper.rb +++ b/plugins/redmineup_tags/test/test_helper.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -21,7 +21,6 @@ require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') - def plugin_fixtures(*fixtures) fixtures_directory = "#{File.dirname(__FILE__)}/fixtures/" fixture_names = @@ -33,20 +32,29 @@ def plugin_fixtures(*fixtures) fixtures.flatten.map { |n| n.to_s } end + create_fixtures(fixtures_directory, fixture_names, class_names = {}) +end + +def create_fixtures(fixtures_directory, table_names, class_names = {}) if ActiveRecord::VERSION::MAJOR >= 4 - ActiveRecord::FixtureSet.create_fixtures fixtures_directory, fixture_names + ActiveRecord::FixtureSet.create_fixtures(fixtures_directory, table_names, class_names) else - ActiveRecord::Fixtures.create_fixtures fixtures_directory, fixture_names + ActiveRecord::Fixtures.create_fixtures(fixtures_directory, table_names, class_names) end end plugin_fixtures :all - def compatible_request(type, action, parameters = {}) - Rails.version < '5.1' ? send(type, action, parameters) : send(type, action, :params => parameters) + Rails.version < '5.1' ? send(type, action, parameters) : send(type, action, params: parameters) end def compatible_xhr_request(type, action, parameters = {}) - Rails.version < '5.1' ? xhr(type, action, parameters) : send(type, action, :params => parameters, :xhr => true) + Rails.version < '5.1' ? xhr(type, action, parameters) : send(type, action, params: parameters, xhr: true) +end + +# Returns the issues that are displayed in the list in the same order +def issues_in_list + ids = css_select('tr.issue td.id').map{ |tag| tag['text'].to_i } + Issue.where(id: ids).sort_by { |issue| ids.index(issue.id) } end diff --git a/plugins/redmineup_tags/test/unit/issue_test.rb b/plugins/redmineup_tags/test/unit/issue_test.rb index 0be3dc2..f1b6e50 100644 --- a/plugins/redmineup_tags/test/unit/issue_test.rb +++ b/plugins/redmineup_tags/test/unit/issue_test.rb @@ -3,7 +3,7 @@ # This file is a part of Redmine Tags (redmine_tags) plugin, # customer relationship management plugin for Redmine # -# Copyright (C) 2011-2018 RedmineUP +# Copyright (C) 2011-2021 RedmineUP # http://www.redmineup.com/ # # redmine_tags is free software: you can redistribute it and/or modify @@ -21,8 +21,8 @@ require File.expand_path('../../test_helper', __FILE__) -class RedmineTags::Patches::IssueTest < ActiveSupport::TestCase - fixtures :users, :projects, :issues, :issue_statuses, :enumerations, :trackers +class RedmineupTags::Patches::IssueTest < ActiveSupport::TestCase + fixtures :users, :projects, :issues, :issue_statuses, :enumerations, :trackers, :enabled_modules def setup # run as the admin @@ -32,43 +32,43 @@ class RedmineTags::Patches::IssueTest < ActiveSupport::TestCase @project_b = Project.find 3 end - test "patch was applied" do + test 'patch was applied' do assert_respond_to Issue, :available_tags, 'Issue has available_tags getter' assert_respond_to Issue.new, :tags, 'Issue instance has tags getter' assert_respond_to Issue.new, :tags=, 'Issue instance has tags setter' assert_respond_to Issue.new, :tag_list=, 'Issue instance has tag_list setter' end - test "available tags should return list of distinct tags" do + test 'available tags should return list of distinct tags' do assert_equal 3, Issue.available_tags.to_a.size end - test "available tags should allow list tags of open issues only" do - assert_equal 2, Issue.available_tags(:open_only => true).to_a.size + test 'available tags should allow list tags of open issues only' do + assert_equal 2, Issue.available_tags(open_only: true).to_a.size end - test "available tags should allow list tags of specific project only" do - assert_equal 3, Issue.available_tags(:project => @project_a).to_a.size - assert_equal 2, Issue.available_tags(:project => @project_b).to_a.size + test 'available tags should allow list tags of specific project only' do + assert_equal 3, Issue.available_tags(project: @project_a).to_a.size + assert_equal 2, Issue.available_tags(project: @project_b).to_a.size - assert_equal 2, Issue.available_tags(:open_only => true, :project => @project_a).to_a.size - assert_equal 2, Issue.available_tags(:open_only => true, :project => @project_b).to_a.size + assert_equal 2, Issue.available_tags(open_only: true, project: @project_a).to_a.size + assert_equal 2, Issue.available_tags(open_only: true, project: @project_b).to_a.size end - test "available tags should allow list tags found by name" do - assert_equal 2, Issue.available_tags(:name_like => 'i').to_a.size - assert_equal 1, Issue.available_tags(:name_like => 'rd').to_a.size - assert_equal 2, Issue.available_tags(:name_like => 's').to_a.size - assert_equal 1, Issue.available_tags(:name_like => 'e').to_a.size + test 'available tags should allow list tags found by name' do + assert_equal 2, Issue.available_tags(name_like: 'i').to_a.size + assert_equal 1, Issue.available_tags(name_like: 'rd').to_a.size + assert_equal 2, Issue.available_tags(name_like: 's').to_a.size + assert_equal 1, Issue.available_tags(name_like: 'e').to_a.size - assert_equal 1, Issue.available_tags(:name_like => 'f', :project => @project_a).to_a.size - assert_equal 0, Issue.available_tags(:name_like => 'b', :project => @project_a).to_a.size - assert_equal 1, Issue.available_tags(:name_like => 'sec', :open_only => true, :project => @project_a).to_a.size - assert_equal 0, Issue.available_tags(:name_like => 'fir', :open_only => true, :project => @project_a).to_a.size + assert_equal 1, Issue.available_tags(name_like: 'f', project: @project_a).to_a.size + assert_equal 0, Issue.available_tags(name_like: 'b', project: @project_a).to_a.size + assert_equal 1, Issue.available_tags(name_like: 'sec', open_only: true, project: @project_a).to_a.size + assert_equal 0, Issue.available_tags(name_like: 'fir', open_only: true, project: @project_a).to_a.size end test 'Issue.all_tags should return all tags kind of Issue' do - tags = Issue.all_tags.map &:name - assert_equal %w(first second third), tags + tags = Issue.all_tags.map(&:name) + assert_equal %w[first second third], tags end end -- GitLab