diff --git a/.travis.yml b/.travis.yml index 03b9743251..694956c311 100644 --- a/.travis.yml +++ b/.travis.yml @@ -85,210 +85,138 @@ jobs: - bash script/ci/runner.sh npm - stage: test - name: 'spec_legacy (1/1) - mysql' + name: 'spec_legacy (1/1) - standard' script: - - bash script/ci/setup.sh spec_legacy mysql + - bash script/ci/setup.sh spec_legacy - bash script/ci/runner.sh spec_legacy 1 1 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'spec_legacy (1/1) - postgres standard' + name: 'spec_legacy (1/1) - bim' script: - - bash script/ci/setup.sh spec_legacy postgres - - bash script/ci/runner.sh spec_legacy 1 1 - - stage: test - name: 'spec_legacy (1/1) - postgres bim' - script: - - bash script/ci/setup.sh spec_legacy postgres bim + - bash script/ci/setup.sh spec_legacy bim - bash script/ci/runner.sh spec_legacy 1 1 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'units (1/4) - mysql' + name: 'units (1/4) - standard' script: - - bash script/ci/setup.sh units mysql + - bash script/ci/setup.sh units - bash script/ci/runner.sh units 4 1 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'units (1/4) - postgres standard' + name: 'units (1/4) - bim' script: - - bash script/ci/setup.sh units postgres - - bash script/ci/runner.sh units 4 1 - - stage: test - name: 'units (1/4) - postgres bim' - script: - - bash script/ci/setup.sh units postgres bim + - bash script/ci/setup.sh units bim - bash script/ci/runner.sh units 4 1 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'units (2/4) - mysql' - script: - - bash script/ci/setup.sh units mysql - - bash script/ci/runner.sh units 4 2 - if: env(SKIP_MYSQL_TESTING) IS blank - - stage: test - name: 'units (2/4) - postgres standard' + name: 'units (2/4) - standard' script: - - bash script/ci/setup.sh units postgres + - bash script/ci/setup.sh units - bash script/ci/runner.sh units 4 2 - stage: test - name: 'units (2/4) - postgres bim' + name: 'units (2/4) - bim' script: - - bash script/ci/setup.sh units postgres bim + - bash script/ci/setup.sh units bim - bash script/ci/runner.sh units 4 2 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'units (3/4) - mysql' + name: 'units (3/4) - standard' script: - - bash script/ci/setup.sh units mysql + - bash script/ci/setup.sh units - bash script/ci/runner.sh units 4 3 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'units (3/4) - postgres standard' + name: 'units (3/4) - bim' script: - - bash script/ci/setup.sh units postgres - - bash script/ci/runner.sh units 4 3 - - stage: test - name: 'units (3/4) - postgres bim' - script: - - bash script/ci/setup.sh units postgres bim + - bash script/ci/setup.sh units bim - bash script/ci/runner.sh units 4 3 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'units (4/4) - mysql' + name: 'units (4/4) - standard' script: - - bash script/ci/setup.sh units mysql + - bash script/ci/setup.sh units - bash script/ci/runner.sh units 4 4 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'units (4/4) - postgres standard' + name: 'units (4/4) - bim' script: - - bash script/ci/setup.sh units postgres - - bash script/ci/runner.sh units 4 4 - - stage: test - name: 'units (4/4) - postgres bim' - script: - - bash script/ci/setup.sh units postgres bim + - bash script/ci/setup.sh units bim - bash script/ci/runner.sh units 4 4 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'features (1/4) - mysql' - script: - - bash script/ci/setup.sh features mysql - - bash script/ci/runner.sh features 4 1 - if: env(SKIP_MYSQL_TESTING) IS blank - - stage: test - name: 'features (1/4) - postgres standard' + name: 'features (1/4) - standard' script: - - bash script/ci/setup.sh features postgres + - bash script/ci/setup.sh features - bash script/ci/runner.sh features 4 1 - stage: test - name: 'features (1/4) - postgres bim' + name: 'features (1/4) - bim' script: - - bash script/ci/setup.sh features postgres bim + - bash script/ci/setup.sh features bim - bash script/ci/runner.sh features 4 1 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'features (2/4) - mysql' + name: 'features (2/4) - standard' script: - - bash script/ci/setup.sh features mysql + - bash script/ci/setup.sh features - bash script/ci/runner.sh features 4 2 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'features (2/4) - postgres standard' + name: 'features (2/4) - bim' script: - - bash script/ci/setup.sh features postgres - - bash script/ci/runner.sh features 4 2 - - stage: test - name: 'features (2/4) - postgres bim' - script: - - bash script/ci/setup.sh features postgres bim + - bash script/ci/setup.sh features bim - bash script/ci/runner.sh features 4 2 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'features (3/4) - mysql' + name: 'features (3/4) - standard' script: - - bash script/ci/setup.sh features mysql + - bash script/ci/setup.sh features - bash script/ci/runner.sh features 4 3 - if: env(SKIP_MYSQL_TESTING) IS blank - stage: test - name: 'features (3/4) - postgres standard' + name: 'features (3/4) - bim' script: - - bash script/ci/setup.sh features postgres - - bash script/ci/runner.sh features 4 3 - - stage: test - name: 'features (3/4) - postgres bim' - script: - - bash script/ci/setup.sh features postgres bim + - bash script/ci/setup.sh features bim - bash script/ci/runner.sh features 4 3 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'features (4/4) - mysql' - script: - - bash script/ci/setup.sh features mysql - - bash script/ci/runner.sh features 4 4 - if: env(SKIP_MYSQL_TESTING) IS blank - - stage: test - name: 'features (4/4) - postgres standard' + name: 'features (4/4) - standard' script: - - bash script/ci/setup.sh features postgres + - bash script/ci/setup.sh features - bash script/ci/runner.sh features 4 4 - stage: test - name: 'features (4/4) - postgres bim' + name: 'features (4/4) - bim' script: - - bash script/ci/setup.sh features postgres bim + - bash script/ci/setup.sh features bim - bash script/ci/runner.sh features 4 4 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'plugins:units (1/1) - mysql' + name: 'plugins:units (1/1) - standard' script: - - bash script/ci/setup.sh plugins:units mysql - - bash script/ci/runner.sh plugins:units 1 1 - if: env(SKIP_MYSQL_TESTING) IS blank AND head_branch !~ /^core\// - - stage: test - name: 'plugins:units (1/1) - postgres standard' - script: - - bash script/ci/setup.sh plugins:units postgres + - bash script/ci/setup.sh plugins:units - bash script/ci/runner.sh plugins:units 1 1 if: head_branch !~ /^core\// - stage: test - name: 'plugins:units (1/1) - postgres bim' + name: 'plugins:units (1/1) - bim' script: - - bash script/ci/setup.sh plugins:units postgres bim + - bash script/ci/setup.sh plugins:units bim - bash script/ci/runner.sh plugins:units 1 1 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'plugins:features (1/1) - mysql' + name: 'plugins:features (1/1) - standard' script: - - bash script/ci/setup.sh plugins:features mysql - - bash script/ci/runner.sh plugins:features 1 1 - if: env(SKIP_MYSQL_TESTING) IS blank AND head_branch !~ /^core\// - - stage: test - name: 'plugins:features (1/1) - postgres standard' - script: - - bash script/ci/setup.sh plugins:features postgres + - bash script/ci/setup.sh plugins:features - bash script/ci/runner.sh plugins:features 1 1 if: head_branch !~ /^core\// - stage: test - name: 'plugins:features (1/1) - postgres bim' + name: 'plugins:features (1/1) - bim' script: - - bash script/ci/setup.sh plugins:features postgres bim + - bash script/ci/setup.sh plugins:features bim - bash script/ci/runner.sh plugins:features 1 1 if: head_branch =~ /^(bim\/|dev|release\/)/ - stage: test - name: 'plugins:cucumber (1/1) - mysql' - script: - - bash script/ci/setup.sh plugins:cucumber mysql - - bash script/ci/runner.sh plugins:cucumber 1 1 - if: env(SKIP_MYSQL_TESTING) IS blank AND head_branch !~ /^core\// - - stage: test - name: 'plugins:cucumber (1/1) - postgres standard' + name: 'plugins:cucumber (1/1) - standard' script: - - bash script/ci/setup.sh plugins:cucumber postgres + - bash script/ci/setup.sh plugins:cucumber - bash script/ci/runner.sh plugins:cucumber 1 1 if: head_branch !~ /^core\// - stage: test - name: 'plugins:cucumber (1/1) - postgres bim' + name: 'plugins:cucumber (1/1) - bim' script: - - bash script/ci/setup.sh plugins:cucumber postgres bim + - bash script/ci/setup.sh plugins:cucumber bim - bash script/ci/runner.sh plugins:cucumber 1 1 if: head_branch =~ /^(bim\/|dev|release\/)/ diff --git a/Gemfile b/Gemfile index 8b08f49bce..be87ff11ed 100644 --- a/Gemfile +++ b/Gemfile @@ -103,11 +103,6 @@ gem 'bcrypt', '~> 3.1.6' gem 'multi_json', '~> 1.13.1' gem 'oj', '~> 3.7.0' -# We rely on this specific version, which is the latest as of now (start of 2019), -# because we have to apply to it a bugfix which could break things in other versions. -# This can be removed as soon as said bugfix is integrated into rabl itself. -# See: config/initializers/rabl_hack.rb -gem 'rabl', '~> 0.14.0' gem 'daemons' gem 'delayed_job_active_record', '~> 4.1.1' @@ -281,10 +276,6 @@ gem 'reform-rails', '~> 0.1.7' gem 'roar', '~> 1.1.0' platforms :mri, :mingw, :x64_mingw do - group :mysql2 do - gem 'mysql2', '~> 0.5.0' - end - group :postgres do gem 'pg', '~> 1.1.0' end diff --git a/Gemfile.lock b/Gemfile.lock index c0c90065ea..83799d89cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -578,7 +578,6 @@ GEM mustermann (1.0.3) mustermann-grape (1.0.0) mustermann (~> 1.0.0) - mysql2 (0.5.2) net-ldap (0.16.1) netrc (0.11.0) newrelic_rpm (6.0.0.351) @@ -650,8 +649,6 @@ GEM pry (>= 0.9.11) public_suffix (3.0.3) puma (3.12.0) - rabl (0.14.0) - activesupport (>= 2.3.14) rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) @@ -954,7 +951,6 @@ DEPENDENCIES meta-tags (~> 2.11.0) multi_json (~> 1.13.1) my_page! - mysql2 (~> 0.5.0) net-ldap (~> 0.16.0) newrelic_rpm nokogiri (~> 1.10.3) @@ -998,7 +994,6 @@ DEPENDENCIES pry-rescue (~> 1.5.0) pry-stack_explorer (~> 0.4.9.2) puma (~> 3.12.0) - rabl (~> 0.14.0) rack-attack (~> 5.4.2) rack-mini-profiler rack-protection (~> 2.0.0) diff --git a/app/assets/javascripts/onboarding/homescreen_tour.js b/app/assets/javascripts/onboarding/homescreen_tour.js index 2c747be4aa..7907d91052 100644 --- a/app/assets/javascripts/onboarding/homescreen_tour.js +++ b/app/assets/javascripts/onboarding/homescreen_tour.js @@ -5,7 +5,8 @@ 'next #top-menu': I18n.t('js.onboarding.steps.welcome'), 'skipButton': {className: 'enjoyhint_btn-transparent', text: I18n.t('js.onboarding.buttons.skip')}, 'nextButton': {text: I18n.t('js.onboarding.buttons.next')}, - 'containerClass': '-hidden-arrow' + 'containerClass': '-hidden-arrow', + 'bottom': 7 }, { 'description': I18n.t('js.onboarding.steps.project_selection'), diff --git a/app/assets/stylesheets/content/_modal.sass b/app/assets/stylesheets/content/_modal.sass index 7abfdfe307..bb277ff4c1 100644 --- a/app/assets/stylesheets/content/_modal.sass +++ b/app/assets/stylesheets/content/_modal.sass @@ -63,7 +63,7 @@ $modal-footer-height: $modal-header-height max-width: 60vw overflow-y: auto - @include styled-scroll-bar-vertical + @include styled-scroll-bar &.-wide min-width: 75vw @@ -81,7 +81,7 @@ $modal-footer-height: $modal-header-height padding: 0 1.5rem max-height: calc(100vh - #{$modal-header-height} - #{$modal-footer-height}) overflow: auto - @include styled-scroll-bar-vertical + @include styled-scroll-bar &.-formattable p:last-of-type diff --git a/app/assets/stylesheets/content/_table.sass b/app/assets/stylesheets/content/_table.sass index 266b6c7aa6..8c5f50505b 100644 --- a/app/assets/stylesheets/content/_table.sass +++ b/app/assets/stylesheets/content/_table.sass @@ -50,6 +50,7 @@ $input-elements: input, 'input.form--text-field', select, 'select.form--select', overflow: x: auto y: auto + @include styled-scroll-bar .generic-table--action-buttons margin-top: 2rem diff --git a/app/assets/stylesheets/content/_user.sass b/app/assets/stylesheets/content/_user.sass index 1989764c42..50424cec5a 100644 --- a/app/assets/stylesheets/content/_user.sass +++ b/app/assets/stylesheets/content/_user.sass @@ -64,10 +64,12 @@ user-select: none h1, h2, h3, h4 - .avatar, .avatar-mini, .avatar-medium + user-avatar vertical-align: middle - margin-right: 7px - + margin-right: 5px + +tr user-avatar + margin-right: 5px .user-link display: inline-block diff --git a/app/assets/stylesheets/content/_wiki.sass b/app/assets/stylesheets/content/_wiki.sass index ac2793bdb3..7e524adaa0 100644 --- a/app/assets/stylesheets/content/_wiki.sass +++ b/app/assets/stylesheets/content/_wiki.sass @@ -72,6 +72,10 @@ div.wiki margin-left: 0 display: table font-size: $wiki-toc-ul-font-size + + .section-nav + margin-bottom: 0 + margin-left: 12px &.right float: right margin-left: 12px @@ -107,8 +111,10 @@ div.wiki a.wiki-anchor display: none - margin-left: 6px text-decoration: none + font-size: 16px + vertical-align: middle + padding-right: 2px &:hover color: #aaa !important diff --git a/app/assets/stylesheets/content/menus/_project_autocompletion.sass b/app/assets/stylesheets/content/menus/_project_autocompletion.sass index 8f622be0f3..53704123cb 100644 --- a/app/assets/stylesheets/content/menus/_project_autocompletion.sass +++ b/app/assets/stylesheets/content/menus/_project_autocompletion.sass @@ -82,7 +82,7 @@ // Borders to complete the menu look border-right: 1px solid $header-drop-down-border-color border-left: 1px solid $header-drop-down-border-color - @include styled-scroll-bar-vertical + @include styled-scroll-bar // Cut off result element width .ui-menu-item-wrapper diff --git a/app/assets/stylesheets/content/work_packages/_table_content.sass b/app/assets/stylesheets/content/work_packages/_table_content.sass index aea448d07a..82807c5974 100644 --- a/app/assets/stylesheets/content/work_packages/_table_content.sass +++ b/app/assets/stylesheets/content/work_packages/_table_content.sass @@ -55,7 +55,7 @@ // Avoid that the select field gets too small .wp-inline-edit--field.ng-select - min-width: 110px + min-width: 140px // Styles for inline editable attributes .work-package-table--container td.-editable @@ -148,12 +148,7 @@ html:not(.-browser-mobile) .wp-table--cell-span padding: 2px -// On edge, pointer-events only work on -// block or inline-block elements body.-browser-edge - .wp-table--cell-span - display: inline-block !important - // Ensure height is set in table .work-package-table .wp-table--cell-span height: 22px !important diff --git a/app/assets/stylesheets/content/work_packages/_table_hierarchy.sass b/app/assets/stylesheets/content/work_packages/_table_hierarchy.sass index 6c90abf1c7..53ece51eef 100644 --- a/app/assets/stylesheets/content/work_packages/_table_hierarchy.sass +++ b/app/assets/stylesheets/content/work_packages/_table_hierarchy.sass @@ -9,11 +9,6 @@ &:hover text-decoration: none - // On edge, pointer-events only work on - // block or inline-block elements - html.-browser-edge & - display: inline-block !important - // Toggle the indicator accessibility texts // accordingly .wp-table--hierarchy-indicator-collapsed diff --git a/app/assets/stylesheets/content/work_packages/tabs/_relations.sass b/app/assets/stylesheets/content/work_packages/tabs/_relations.sass index 72dd51d68b..25be2de58d 100644 --- a/app/assets/stylesheets/content/work_packages/tabs/_relations.sass +++ b/app/assets/stylesheets/content/work_packages/tabs/_relations.sass @@ -80,9 +80,6 @@ .wp-relations-controls-section text-align: right flex-shrink: 1 - .force-right - position: absolute - right: 0 a:hover text-decoration: none @@ -144,10 +141,12 @@ .icon-button cursor: not-allowed -// Disable overflow in grid-content of create form -// To allow results indicator to overflow it -.wp-relations-create--form .grid-content - overflow-y: visible +.wp-relations-create--form + display: flex + + .wp-relations-input-section + margin-right: 10px + flex: 1 .detail-panel--relations .panel-toggler .icon-small diff --git a/app/assets/stylesheets/layout/_main_menu.sass b/app/assets/stylesheets/layout/_main_menu.sass index 54fd2fbf6d..4b9c429319 100644 --- a/app/assets/stylesheets/layout/_main_menu.sass +++ b/app/assets/stylesheets/layout/_main_menu.sass @@ -50,7 +50,7 @@ $menu-item-line-height: 30px +allow-vertical-scrolling height: calc(100vh - #{$header-height}) position: relative - @include styled-scroll-bar-vertical + @include styled-scroll-bar // Fixed heights to allow inner scrolling .menu_root.closed, @@ -67,7 +67,7 @@ $menu-item-line-height: 30px .main-menu--children height: calc(100% - (#{$main-menu-item-height} + 10px)) // 10px spacing overflow: auto - @include styled-scroll-bar-vertical + @include styled-scroll-bar ul margin: 0 @@ -318,7 +318,7 @@ a.main-menu--parent-node padding-left: 7px padding-right: 7px - @include styled-scroll-bar-vertical + @include styled-scroll-bar .main-menu--segment-header @include varprop(color, main-menu-fieldset-header-color) diff --git a/app/assets/stylesheets/layout/_toolbar_mobile.sass b/app/assets/stylesheets/layout/_toolbar_mobile.sass index 53c901ba58..e4b86e414d 100644 --- a/app/assets/stylesheets/layout/_toolbar_mobile.sass +++ b/app/assets/stylesheets/layout/_toolbar_mobile.sass @@ -38,7 +38,7 @@ background: #fff display: flex flex-wrap: wrap - justify-content: stretch + justify-content: flex-end width: calc(100% + 10px) > li diff --git a/app/assets/stylesheets/layout/_top_menu.sass b/app/assets/stylesheets/layout/_top_menu.sass index dfa5ce9279..3bb7bc7ee3 100644 --- a/app/assets/stylesheets/layout/_top_menu.sass +++ b/app/assets/stylesheets/layout/_top_menu.sass @@ -107,7 +107,7 @@ $search-input-height: 30px overflow-y: auto overflow-x: hidden - @include styled-scroll-bar-vertical + @include styled-scroll-bar li float: none @@ -244,7 +244,7 @@ $search-input-height: 30px @include varprop(color, header-search-field-font-color) .scroll-host - @include styled-scroll-bar-vertical + @include styled-scroll-bar max-height: 80vh height: auto diff --git a/app/assets/stylesheets/layout/work_packages/_details_view.sass b/app/assets/stylesheets/layout/work_packages/_details_view.sass index 46bb5cda0f..41fd4aa1a5 100644 --- a/app/assets/stylesheets/layout/work_packages/_details_view.sass +++ b/app/assets/stylesheets/layout/work_packages/_details_view.sass @@ -79,8 +79,11 @@ body.router--work-packages-split-view-new top: 50px bottom: 55px width: 100% - +allow-vertical-scrolling - padding: 0 15px 0 20px + overflow-x: hidden + overflow-y: scroll + padding: 0 5px 0 20px + + @include styled-scroll-bar .work-packages--details diff --git a/app/assets/stylesheets/layout/work_packages/_full_view.sass b/app/assets/stylesheets/layout/work_packages/_full_view.sass index f02eeac6ad..1e490822ac 100644 --- a/app/assets/stylesheets/layout/work_packages/_full_view.sass +++ b/app/assets/stylesheets/layout/work_packages/_full_view.sass @@ -79,9 +79,10 @@ overflow-x: hidden flex: 2 position: relative + @include styled-scroll-bar .work-packages--panel-inner - padding: 5px 15px 20px 0px + padding: 0px 5px 20px 0 width: 100% // These styles were taken over from the details tab styling. @@ -97,12 +98,13 @@ .work-packages-full-view--split-right min-width: 500px - overflow-y: auto + overflow-y: scroll overflow-x: auto position: relative + @include styled-scroll-bar .work-packages--panel-inner - padding: 15px 15px 0px 15px + padding: 15px 5px 0px 15px .work-package-details-activities-activity-contents ul.work-package-details-activities-messages padding-left: 0 diff --git a/app/assets/stylesheets/layout/work_packages/_query_menu.sass b/app/assets/stylesheets/layout/work_packages/_query_menu.sass index e833dc576c..8ada4778e1 100644 --- a/app/assets/stylesheets/layout/work_packages/_query_menu.sass +++ b/app/assets/stylesheets/layout/work_packages/_query_menu.sass @@ -15,7 +15,7 @@ $wp-query-menu-search-container-height: 35px background: none z-index: 0 // Prevent overlapping with project select dropdown (https://community.openproject.com/wp/28175) - @include styled-scroll-bar-vertical + @include styled-scroll-bar .wp-query-menu--results-container diff --git a/app/assets/stylesheets/layout/work_packages/_table.sass b/app/assets/stylesheets/layout/work_packages/_table.sass index 8fe1f565d3..0840d89f81 100644 --- a/app/assets/stylesheets/layout/work_packages/_table.sass +++ b/app/assets/stylesheets/layout/work_packages/_table.sass @@ -121,6 +121,7 @@ flex: 1 1 // Show scrollbars for inner content overflow: auto + @include styled-scroll-bar // relative for loading indicator position: relative // Hint browser that this will inner-scroll @@ -142,6 +143,7 @@ overflow-x: scroll // Show the vertical scrollbar when necessary overflow-y: auto + @include styled-scroll-bar // Hidden by default display: none // Hint browser that this will inner-scroll diff --git a/app/assets/stylesheets/layout/work_packages/_table_embedded.sass b/app/assets/stylesheets/layout/work_packages/_table_embedded.sass index ceebd51764..1911208c53 100644 --- a/app/assets/stylesheets/layout/work_packages/_table_embedded.sass +++ b/app/assets/stylesheets/layout/work_packages/_table_embedded.sass @@ -47,6 +47,7 @@ $table-timeline--compact-row-height: 28px // Allow scrolling in narrow views .work-packages-split-view--tabletimeline-content overflow: auto + @include styled-scroll-bar // Disable css containment since we have no inner elements .work-packages-tabletimeline--table-side, diff --git a/app/assets/stylesheets/openproject/_mixins.sass b/app/assets/stylesheets/openproject/_mixins.sass index 4a4a066570..e8dd832698 100644 --- a/app/assets/stylesheets/openproject/_mixins.sass +++ b/app/assets/stylesheets/openproject/_mixins.sass @@ -87,16 +87,17 @@ $scrollbar-color: #DDDDDD +$scrollbar-size: 10px -@mixin styled-scroll-bar-vertical +@mixin styled-scroll-bar // Firefox specific styles scrollbar-color: transparent transparent scrollbar-width: thin // Other browser styles &::-webkit-scrollbar - width: 8px - height: 12px + height: $scrollbar-size + width: $scrollbar-size &::-webkit-scrollbar-track background: transparent @@ -110,24 +111,6 @@ $scrollbar-color: #DDDDDD &::-webkit-scrollbar-thumb visibility: visible -@mixin styled-scroll-bar-horizontal - // Firefox specific styles - scrollbar-color: $scrollbar-color transparent - scrollbar-width: thin - - // Other browser styles - &::-webkit-scrollbar - width: 12px - height: 8px - - &::-webkit-scrollbar-track - background: transparent - - // Should always be visible otherwise it would not be displayed on mobile or tablet - &::-webkit-scrollbar-thumb - background: $scrollbar-color - visibility: visible - @mixin two-column-layout column-count: 2 column-gap: 3rem diff --git a/app/contracts/delete_contract.rb b/app/contracts/delete_contract.rb new file mode 100644 index 0000000000..4ab9d858aa --- /dev/null +++ b/app/contracts/delete_contract.rb @@ -0,0 +1,70 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +class DeleteContract < ModelContract + class << self + def delete_permission(permission = nil) + if permission + @delete_permission = permission + end + + @delete_permission + end + end + + def validate + user_allowed + + super + end + + def user_allowed + unless authorized? + errors.add :base, :error_unauthorized + end + end + + protected + + def validate_model? + false + end + + def authorized? + permission = self.class.delete_permission + + case permission + when :admin + user.admin? + when Proc + instance_exec(&permission) + else + !model.project || user.allowed_to?(permission, model.project) + end + end +end diff --git a/app/contracts/members/base_contract.rb b/app/contracts/members/base_contract.rb index 107a709089..152c3be224 100644 --- a/app/contracts/members/base_contract.rb +++ b/app/contracts/members/base_contract.rb @@ -28,10 +28,6 @@ module Members class BaseContract < ::ModelContract - def self.model - Member - end - delegate :principal, :project, :new_record?, @@ -42,7 +38,6 @@ module Members def validate user_allowed_to_manage roles_grantable - principal_assignable super end @@ -54,15 +49,10 @@ module Members end def roles_grantable - unless roles.all? { |r| r.builtin == Role::NON_BUILTIN && r.class == Role } - errors.add(:roles, :ungrantable) - end - end + unmarked_roles = model.member_roles.reject(&:marked_for_destruction?).map(&:role) - def principal_assignable - if principal && - [Principal::STATUSES[:builtin], Principal::STATUSES[:locked]].include?(principal.status) - errors.add(:principal, :unassignable) + unless unmarked_roles.all? { |r| r.builtin == Role::NON_BUILTIN && r.class == Role } + errors.add(:roles, :ungrantable) end end end diff --git a/app/contracts/members/create_contract.rb b/app/contracts/members/create_contract.rb index 4da1731960..4646509a2f 100644 --- a/app/contracts/members/create_contract.rb +++ b/app/contracts/members/create_contract.rb @@ -30,6 +30,17 @@ module Members class CreateContract < BaseContract attribute :project attribute :user_id - attribute :principal + attribute :principal do + principal_assignable + end + + private + + def principal_assignable + if principal && + [Principal::STATUSES[:builtin], Principal::STATUSES[:locked]].include?(principal.status) + errors.add(:principal, :unassignable) + end + end end end diff --git a/script/templates/database.travis.mysql.yml b/app/contracts/members/delete_contract.rb similarity index 79% rename from script/templates/database.travis.mysql.yml rename to app/contracts/members/delete_contract.rb index 0dafaa5d83..8f284b0f54 100644 --- a/script/templates/database.travis.mysql.yml +++ b/app/contracts/members/delete_contract.rb @@ -26,18 +26,8 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -test: - adapter: mysql2 - database: travis_ci_test - username: travis - encoding: utf8 - pool: 20 - variables: - sql_mode: - "no_auto_value_on_zero,\ - strict_trans_tables,\ - no_zero_date,\ - strict_all_tables,\ - no_zero_in_date,\ - error_for_division_by_zero,\ - no_engine_substitution" +module Members + class DeleteContract < ::DeleteContract + delete_permission :manage_members + end +end diff --git a/app/contracts/members/update_contract.rb b/app/contracts/members/update_contract.rb new file mode 100644 index 0000000000..ceaa398d24 --- /dev/null +++ b/app/contracts/members/update_contract.rb @@ -0,0 +1,32 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Members + class UpdateContract < BaseContract + end +end diff --git a/app/contracts/model_contract.rb b/app/contracts/model_contract.rb index 3b5202a37f..19a47d8488 100644 --- a/app/contracts/model_contract.rb +++ b/app/contracts/model_contract.rb @@ -45,6 +45,10 @@ class ModelContract < Reform::Contract @attribute_validations ||= [] end + def attribute_permissions + @attribute_permissions ||= {} + end + def attribute_aliases @attribute_aliases ||= {} end @@ -57,12 +61,23 @@ class ModelContract < Reform::Contract property attribute add_writable(attribute, options[:writeable]) + attribute_permission(attribute, options[:permission]) if block attribute_validations << block end end + def default_attribute_permission(permission) + attribute_permission(:default_permission, permission) + end + + def attribute_permission(attribute, permission) + return unless permission + + attribute_permissions[attribute] = Array(permission) + end + private def add_writable(attribute, writeable) @@ -89,7 +104,7 @@ class ModelContract < Reform::Contract end # we want to add a validation error whenever someone sets a property that we don't know. - # However AR will cleverly try to resolve the value for errorneous properties. Thus we need + # However AR will cleverly try to resolve the value for erroneous properties. Thus we need # to hook into this method and return nil for unknown properties to avoid NoMethod errors... def read_attribute_for_validation(attribute) if respond_to? attribute @@ -99,13 +114,7 @@ class ModelContract < Reform::Contract def writable_attributes @writable_attributes ||= begin - writable = collect_ancestor_attributes(:writable_attributes) - - collect_ancestor_attributes(:writable_conditions).each do |attribute, condition| - writable -= [attribute, "#{attribute}_id"] unless instance_exec(&condition) - end - - writable + reduce_writable_attributes(collect_writable_attributes) end end @@ -141,7 +150,7 @@ class ModelContract < Reform::Contract end def self.model - raise NotImplementedError + @model ||= name.deconstantize.singularize.constantize end # use activerecord as the base scope instead of 'activemodel' to be compatible @@ -166,7 +175,7 @@ class ModelContract < Reform::Contract private def readonly_attributes_unchanged - invalid_changes = model.changed - writable_attributes + invalid_changes = attributes_changed_by_user - writable_attributes invalid_changes.each do |attribute| outside_attribute = collect_ancestor_attributes(:attribute_aliases)[attribute] || attribute @@ -175,6 +184,16 @@ class ModelContract < Reform::Contract end end + def attributes_changed_by_user + changed = model.changed + + if model.respond_to?(:changed_by_system) + changed -= model.changed_by_system + end + + changed + end + def run_attribute_validations attribute_validations.each { |validation| instance_exec(&validation) } end @@ -208,4 +227,47 @@ class ModelContract < Reform::Contract attributes.send(cleanup_method) end + + def collect_writable_attributes + writable = collect_ancestor_attributes(:writable_attributes) + + if model.respond_to?(:available_custom_fields) + writable += model.available_custom_fields.map { |cf| "custom_field_#{cf.id}" } + end + + writable + end + + def reduce_writable_attributes(attributes) + attributes = reduce_by_writable_conditions(attributes) + reduce_by_writable_permissions(attributes) + end + + def reduce_by_writable_conditions(attributes) + collect_ancestor_attributes(:writable_conditions).each do |attribute, condition| + attributes -= [attribute, "#{attribute}_id"] unless instance_exec(&condition) + end + + attributes + end + + def reduce_by_writable_permissions(attributes) + attribute_permissions = collect_ancestor_attributes(:attribute_permissions) + + attributes.reject do |attribute| + canonical_attribute = attribute.gsub(/_id\z/, '') + + permissions = attribute_permissions[canonical_attribute] || + attribute_permissions["#{canonical_attribute}_id"] || + attribute_permissions[:default_permission] + + next unless permissions + + # This will break once a model that does not respond to project is used. + # This is intended to be worked on then with the additional knowledge. + next if permissions.any? { |p| user.allowed_to?(p, model.project, global: model.project.nil?) } + + true + end + end end diff --git a/app/contracts/projects/delete_contract.rb b/app/contracts/projects/delete_contract.rb index 9f13836333..17eb9162be 100644 --- a/app/contracts/projects/delete_contract.rb +++ b/app/contracts/projects/delete_contract.rb @@ -31,13 +31,7 @@ require 'model_contract' module Projects - class DeleteContract < BaseContract - def validate - unless user.admin? - errors.add :base, :error_unauthorized - end - - super - end + class DeleteContract < ::DeleteContract + delete_permission :admin end end diff --git a/app/contracts/roles/base_contract.rb b/app/contracts/roles/base_contract.rb new file mode 100644 index 0000000000..83dc362727 --- /dev/null +++ b/app/contracts/roles/base_contract.rb @@ -0,0 +1,92 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Roles + class BaseContract < ::ModelContract + attribute :name + attribute :assignable + + def validate + check_permission_prerequisites + + super + end + + def assignable_permissions + if model.is_a?(GlobalRole) + assignable_global_permissions + else + assignable_member_permissions + end + end + + private + + def assignable_member_permissions + permissions_to_remove = case model.builtin + when Role::BUILTIN_NON_MEMBER + OpenProject::AccessControl.members_only_permissions + when Role::BUILTIN_ANONYMOUS + OpenProject::AccessControl.loggedin_only_permissions + else + [] + end + + OpenProject::AccessControl.permissions - + OpenProject::AccessControl.public_permissions - + OpenProject::AccessControl.global_permissions - + permissions_to_remove + end + + def assignable_global_permissions + OpenProject::AccessControl.global_permissions + end + + def check_permission_prerequisites + model.permissions.each do |name| + permission = OpenProject::AccessControl.permission(name) + + next unless permission + + unmet_dependencies = permission.dependencies - model.permissions + + unmet_dependencies.each do |unmet_dependency| + add_unmet_dependency_error(name, unmet_dependency) + end + end + end + + def add_unmet_dependency_error(selected, unmet) + errors.add(:permissions, + I18n.t(:'activerecord.errors.models.role.permissions.dependency_missing', + permission: I18n.t("permission_#{selected}"), + dependency: I18n.t("permission_#{unmet}")), + error_symbol: :dependency_missing) + end + end +end diff --git a/app/views/timelog/show.rabl b/app/contracts/roles/create_contract.rb similarity index 67% rename from app/views/timelog/show.rabl rename to app/contracts/roles/create_contract.rb index 41a3e58826..ed237c8dee 100644 --- a/app/views/timelog/show.rabl +++ b/app/contracts/roles/create_contract.rb @@ -1,12 +1,12 @@ #-- copyright # OpenProject is a project management system. -# Copyright (C) 2012-2013 the OpenProject Foundation (OPF) +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or @@ -26,32 +26,22 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -object @entry => "timeEntry" -attributes :id, - :spent_on, - :comments, - :hours +module Roles + class CreateContract < BaseContract + attribute :type -node :user do |e| - partial('users/show', object: e.user) -end - -child :activity => :activity do - attributes :name -end + def validate + type_in_allowed -child :project do - attributes :id, :name -end + super + end -child :work_package do - attributes :id, :subject + private - node :isVisible do |w| - w.visible? + def type_in_allowed + unless [Role.name, GlobalRole.name].include?(model.type) + errors.add(:type, :inclusion) + end + end end end - -node :isEditable do |e| - e.editable_by?(User.current) -end diff --git a/app/contracts/roles/update_contract.rb b/app/contracts/roles/update_contract.rb new file mode 100644 index 0000000000..3c39f8ef36 --- /dev/null +++ b/app/contracts/roles/update_contract.rb @@ -0,0 +1,32 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Roles + class UpdateContract < BaseContract + end +end diff --git a/app/contracts/time_entries/delete_contract.rb b/app/contracts/time_entries/delete_contract.rb index c5b4df3446..96ffae0a0a 100644 --- a/app/contracts/time_entries/delete_contract.rb +++ b/app/contracts/time_entries/delete_contract.rb @@ -29,22 +29,8 @@ #++ module TimeEntries - class DeleteContract < BaseContract - def validate - unless user_allowed_to_delete? - errors.add :base, :error_unauthorized - end - - super - end - - private - - ## - # Users may delete time entries IF - # they have the :edit_time_entries or - # user == deleting user and :edit_own_time_entries - def user_allowed_to_delete? + class DeleteContract < ::DeleteContract + delete_permission -> { edit_all = user.allowed_to?(:edit_time_entries, model.project) edit_own = user.allowed_to?(:edit_own_time_entries, model.project) @@ -53,6 +39,6 @@ module TimeEntries else edit_all end - end + } end end diff --git a/app/contracts/versions/delete_contract.rb b/app/contracts/versions/delete_contract.rb index 8d9b757c3e..6e9cbd6b92 100644 --- a/app/contracts/versions/delete_contract.rb +++ b/app/contracts/versions/delete_contract.rb @@ -26,11 +26,10 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -require 'grids/base_contract' - module Versions - class DeleteContract < BaseContract - # super checks that we can manage the version + class DeleteContract < ::DeleteContract + delete_permission :manage_versions + def validate validate_no_work_packages_attached @@ -44,9 +43,5 @@ module Versions errors.add(:base, :undeletable_work_packages_attached) end - - def validate_model? - false - end end end diff --git a/app/contracts/work_packages/base_contract.rb b/app/contracts/work_packages/base_contract.rb index 5f0fc70ecb..8e11dcfcea 100644 --- a/app/contracts/work_packages/base_contract.rb +++ b/app/contracts/work_packages/base_contract.rb @@ -34,23 +34,19 @@ module WorkPackages class BaseContract < ::ModelContract include ::Attachments::ValidateReplacements - def self.model - WorkPackage - end - attribute :subject attribute :description attribute :status_id attribute :type_id attribute :priority_id attribute :category_id - attribute :fixed_version_id do + attribute :fixed_version_id, + permission: :assign_versions do validate_fixed_version_is_assignable end validate :validate_no_reopen_on_closed_version - attribute :lock_version attribute :project_id attribute :done_ratio, @@ -63,9 +59,8 @@ module WorkPackages model.leaf? } - attribute :parent_id do - validate_user_allowed_to_set_parent if model.changed.include?('parent_id') - end + attribute :parent_id, + permission: :manage_subtasks attribute :assigned_to_id do next unless model.project @@ -128,13 +123,15 @@ module WorkPackages end def writable_attributes + ret = super + # If we're in a readonly status and did not move into that status right now # only allow other status transitions if model.readonly_status? && !model.status_id_change - return %w[status status_id] + ret &= %w(status status_id) end - super + model.available_custom_fields.map { |cf| "custom_field_#{cf.id}" } + ret end private @@ -212,10 +209,6 @@ module WorkPackages end end - def validate_user_allowed_to_set_parent - errors.add :base, :error_unauthorized unless @can.allowed?(model, :manage_subtasks) - end - def validate_no_reopen_on_closed_version if model.fixed_version_id && model.reopened? && model.fixed_version.closed? errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version) diff --git a/app/contracts/work_packages/create_contract.rb b/app/contracts/work_packages/create_contract.rb index f7a05e4dac..2ac6bfa863 100644 --- a/app/contracts/work_packages/create_contract.rb +++ b/app/contracts/work_packages/create_contract.rb @@ -32,10 +32,15 @@ require 'work_packages/base_contract' module WorkPackages class CreateContract < BaseContract - attribute :author_id do + # TODO: Think about whether this can be removed + # as it is unwriteable. So why bother checking for the correct author + attribute :author_id, + writeable: false do errors.add :author_id, :invalid if model.author != user end + default_attribute_permission :add_work_packages + def validate user_allowed_to_add @@ -51,5 +56,10 @@ module WorkPackages errors.add :base, :error_unauthorized end end + + def attributes_changed_by_user + # lock version is initialized by AR itself + super - ['lock_version'] + end end end diff --git a/app/contracts/work_packages/delete_contract.rb b/app/contracts/work_packages/delete_contract.rb index c903fe5dc3..8c6fec087d 100644 --- a/app/contracts/work_packages/delete_contract.rb +++ b/app/contracts/work_packages/delete_contract.rb @@ -29,23 +29,7 @@ #++ module WorkPackages - class DeleteContract < ::ModelContract - def self.model - WorkPackage - end - - def validate - user_allowed_to_delete - - super - end - - private - - def user_allowed_to_delete - unless user.allowed_to?(:delete_work_packages, model.project) - errors.add(:base, :error_unauthorized) - end - end + class DeleteContract < ::DeleteContract + delete_permission :delete_work_packages end end diff --git a/app/contracts/work_packages/skip_authorization_checks.rb b/app/contracts/work_packages/skip_authorization_checks.rb index 4f33a3cc82..577dfd214d 100644 --- a/app/contracts/work_packages/skip_authorization_checks.rb +++ b/app/contracts/work_packages/skip_authorization_checks.rb @@ -36,4 +36,8 @@ module WorkPackages::SkipAuthorizationChecks def user_allowed_to_edit; end def user_allowed_to_move; end + + def reduce_by_writable_permissions(attributes) + attributes + end end diff --git a/app/contracts/work_packages/update_contract.rb b/app/contracts/work_packages/update_contract.rb index 357e58ec1d..259c0e55b5 100644 --- a/app/contracts/work_packages/update_contract.rb +++ b/app/contracts/work_packages/update_contract.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) @@ -31,7 +32,8 @@ require 'work_packages/base_contract' module WorkPackages class UpdateContract < BaseContract - attribute :lock_version do + attribute :lock_version, + permission: %i[edit_work_packages assign_versions manage_subtasks move] do if model.lock_version.nil? || model.lock_version_changed? errors.add :base, :error_conflict end @@ -41,45 +43,31 @@ module WorkPackages validate :user_allowed_to_edit - validate :user_allowed_to_move - validate :can_move_to_milestone + default_attribute_permission :edit_work_packages + attribute_permission :project_id, :move_work_packages + private def user_allowed_to_edit with_unchanged_project_id do - next if @can.allowed?(model, :edit) - next user_allowed_to_change_parent if @can.allowed?(model, :manage_subtasks) + next if @can.allowed?(model, :edit) || + @can.allowed?(model, :assign_version) || + @can.allowed?(model, :manage_subtasks) || + @can.allowed?(model, :move) next if allowed_journal_addition? errors.add :base, :error_unauthorized end end - def user_allowed_to_change_parent - allowed_changes = { parent_id: true, lock_version: true } - - model.changed.each do |key| - next if allowed_changes[key.to_sym] - return errors.add :base, :error_unauthorized - end - end - def user_allowed_to_access unless ::WorkPackage.visible(@user).exists?(model.id) errors.add :base, :error_not_found end end - def user_allowed_to_move - if model.project_id_changed? && - !@can.allowed?(model, :move) - - errors.add :project, :error_unauthorized - end - end - def with_unchanged_project_id if model.project_id_changed? current_project_id = model.project_id diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d26c56f84e..8a454532ac 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -199,7 +199,7 @@ class ProjectsController < ApplicationController flash[:error] = I18n.t(:error_can_not_archive_project) redirect_back fallback_location: projects_url end - + update_demo_project_settings @project, false end @@ -231,10 +231,10 @@ class ProjectsController < ApplicationController end def level_list - @projects = Project.project_level_list(Project.visible) + projects = Project.project_level_list(Project.visible) respond_to do |format| - format.api + format.json { render json: projects_level_list_json(projects) } end end @@ -242,6 +242,7 @@ class ProjectsController < ApplicationController def find_optional_project return true unless params[:id] + @project = Project.find(params[:id]) authorize rescue ActiveRecord::RecordNotFound diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 6a9dc8b9a2..c032e47615 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -29,42 +29,35 @@ class RolesController < ApplicationController include PaginationHelper + include Roles::NotifyMixin layout 'admin' before_action :require_admin, except: [:autocomplete_for_role] def index - @roles = Role - .order(Arel.sql('builtin, position')) - .page(page_param) - .per_page(per_page_param) + @roles = roles_scope + .page(page_param) + .per_page(per_page_param) render action: 'index', layout: false if request.xhr? end def new - # Prefills the form with 'Non member' role permissions - @role = Role.new(permitted_params.role? || {permissions: Role.non_member.permissions}) + @role = Role.new(permitted_params.role? || { permissions: Role.non_member.permissions }) - define_setable_permissions - - @roles = Role.order(Arel.sql('builtin, position')) + @roles = roles_scope end def create - @role = Role.new(permitted_params.role? || {permissions: Role.non_member.permissions}) - if @role.save - # workflow copy - if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by(id: params[:copy_workflow_from])) - @role.workflows.copy_from_role(copy_from) - end - flash[:notice] = l(:notice_successful_create) + @call = create_role + @role = @call.result + + if @call.success? + flash[:notice] = t(:notice_successful_create) redirect_to action: 'index' - notify_changed_roles(:added, @role) else - define_setable_permissions - @roles = Role.order(Arel.sql('builtin, position')) + @roles = roles_scope render action: 'new' end @@ -72,18 +65,17 @@ class RolesController < ApplicationController def edit @role = Role.find(params[:id]) - define_setable_permissions + @call = set_role_attributes(@role, 'update') end def update @role = Role.find(params[:id]) + @call = update_role(@role, permitted_params.role) - if @role.update_attributes(permitted_params.role) + if @call.success? flash[:notice] = l(:notice_successful_update) redirect_to action: 'index' - notify_changed_roles(:updated, @role) else - @permissions = @role.setable_permissions render action: 'edit' end end @@ -101,21 +93,22 @@ class RolesController < ApplicationController def report @roles = Role.order(Arel.sql('builtin, position')) - @permissions = Redmine::AccessControl.permissions.select {|p| !p.public?} + @permissions = OpenProject::AccessControl.permissions.reject(&:public?) end def bulk_update - @roles = Role.order(Arel.sql('builtin, position')) + @roles = roles_scope - @roles.each do |role| - new_permissions = params[:permissions][role.id.to_s].presence || [] - role.permissions = new_permissions - role.save - end + calls = bulk_update_roles(@roles) - flash[:notice] = l(:notice_successful_update) - redirect_to action: 'index' - notify_changed_roles(:bulk_update, @roles) + if calls.all?(&:success?) + flash[:notice] = l(:notice_successful_update) + redirect_to action: 'index' + else + @calls = calls + @permissions = OpenProject::AccessControl.permissions.reject(&:public?) + render action: 'report' + end end def autocomplete_for_role @@ -134,11 +127,37 @@ class RolesController < ApplicationController private - def notify_changed_roles(action, changed_role) - OpenProject::Notifications.send(:roles_changed, action: action, role: changed_role) + def set_role_attributes(role, create_or_update) + contract = "Roles::#{create_or_update.camelize}Contract".constantize + + Roles::SetAttributesService + .new(user: current_user, model: role, contract_class: contract) + .call(new_params) end - protected + def update_role(role, params) + Roles::UpdateService + .new(user: current_user, model: role) + .call(params) + end + + def bulk_update_roles(roles) + roles.map do |role| + new_permissions = { permissions: params[:permissions][role.id.to_s].presence || [] } + + update_role(role, new_permissions) + end + end + + def create_role + Roles::CreateService + .new(user: current_user) + .call(create_params) + end + + def roles_scope + Role.order(Arel.sql('builtin, position')) + end def default_breadcrumb if action_name == 'index' @@ -152,17 +171,13 @@ class RolesController < ApplicationController true end - def define_setable_permissions - @permissions = group_permissions_by_module(@role.setable_permissions) + def new_params + permitted_params.role? || {} end - def group_permissions_by_module(perms) - perms_by_module = perms.group_by {|p| p.project_module.to_s} - ::Redmine::AccessControl - .sorted_modules - .select {|module_name| perms_by_module[module_name].present?} - .map do |module_name| - [module_name, perms_by_module[module_name]] - end + def create_params + new_params + .merge(copy_workflow_from: params[:copy_workflow_from], + global_role: params[:global_role]) end end diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 83e2f3c59f..429817062d 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -29,8 +29,6 @@ #++ class TimelogController < ApplicationController - menu_item :issues - before_action :disable_api, except: %i[index destroy] before_action :find_work_package, only: %i[new create] before_action :find_project, only: %i[new create] @@ -72,49 +70,8 @@ class TimelogController < ApplicationController respond_to do |format| format.html do - # Paginate results - @entry_count = TimeEntry - .visible - .includes(:project, :work_package) - .references(:projects) - .where(cond.conditions) - .count - @total_hours = TimeEntry - .visible - .includes(:project, :work_package) - .references(:projects) - .where(cond.conditions) - .distinct(false) - .sum(:hours).to_f - - set_entries(cond) - - gon.rabl template: 'app/views/timelog/index.rabl' - gon.project_id = @project.id if @project - gon.work_package_id = @issue.id if @issue - gon.sort_column = 'spent_on' - gon.sort_direction = 'desc' - gon.total_count = total_entry_count(cond) - gon.settings = client_preferences - render layout: layout_non_or_no_menu end - format.json do - set_entries(cond) - - gon.rabl template: 'app/views/timelog/index.rabl' - end - format.atom do - entries = TimeEntry - .visible - .includes(:project, :activity, :user, work_package: :type) - .references(:projects) - .where(cond.conditions) - .order("#{TimeEntry.table_name}.created_on DESC") - .limit(Setting.feeds_limit.to_i) - - render_feed(entries, title: l(:label_spent_time)) - end format.csv do # Export all entries @entries = TimeEntry @@ -133,15 +90,6 @@ class TimelogController < ApplicationController end end - def show - respond_to do |format| - # TODO: Implement html response - format.html do - head 406 - end - end - end - def new @time_entry = new_time_entry(@project, @issue, permitted_params.time_entry.to_h) @@ -206,30 +154,6 @@ class TimelogController < ApplicationController private - def total_entry_count(cond) - TimeEntry - .visible - .includes(:project, :activity, :user, work_package: :type) - .references(:projects) - .where(cond.conditions) - .count - end - - def set_entries(cond) - # .visible introduces a distinct which we don't need here and which interferes - # with the order on postgresql. - # The distinct is therefore explicitly removed - @entries = TimeEntry - .visible - .includes(:project, :activity, :user, work_package: :type) - .references(:projects) - .where(cond.conditions) - .distinct(false) - .order(sort_clause) - .page(page_param) - .per_page(per_page_param) - end - def find_time_entry @time_entry = TimeEntry.find(params[:id]) unless @time_entry.editable_by?(User.current) diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index 4c109e2223..1e01ca6fe0 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -144,10 +144,14 @@ class VersionsController < ApplicationController end def retrieve_selected_type_ids(selectable_types, default_types = nil) - @selected_type_ids = if (ids = params[:type_ids]) - ids.is_a?(Array) ? ids : ids.split('/') - else - default_types || selectable_types - end.map { |t| t.id.to_s } + @selected_type_ids = selected_type_ids selectable_types, default_types + end + + def selected_type_ids(selectable_types, default_types = nil) + if (ids = params[:type_ids]) + ids.is_a?(Array) ? ids.map(&:to_s) : ids.split('/') + else + (default_types || selectable_types).map { |t| t.id.to_s } + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 895997b815..b5557a966b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -235,6 +235,22 @@ module ProjectsHelper s end + def projects_level_list_json(projects) + projects_list = projects.map do |item| + project = item[:project] + + { + "id": project.id, + "name": project.name, + "identifier": project.identifier, + "has_children": !project.leaf?, + "level": item[:level] + } + end + + { projects: projects_list } + end + def projects_with_levels_order_sensitive(projects, &block) if sorted_by_lft? project_tree(projects, &block) diff --git a/app/helpers/roles_helper.rb b/app/helpers/roles_helper.rb index f89612d1ae..a11fe4213e 100644 --- a/app/helpers/roles_helper.rb +++ b/app/helpers/roles_helper.rb @@ -28,4 +28,27 @@ #++ module RolesHelper + def setable_permissions(role) + # Use the base contract for now as we are only interested in the setable permissions + # which do not differentiate. + contract = Roles::BaseContract.new(role, current_user) + + contract.assignable_permissions + end + + def grouped_setable_permissions(role) + group_permissions_by_module(setable_permissions(role)) + end + + private + + def group_permissions_by_module(perms) + perms_by_module = perms.group_by { |p| p.project_module.to_s } + ::OpenProject::AccessControl + .sorted_modules + .select { |module_name| perms_by_module[module_name].present? } + .map do |module_name| + [module_name, perms_by_module[module_name]] + end + end end diff --git a/app/helpers/warning_bar_helper.rb b/app/helpers/warning_bar_helper.rb index 874f0e648b..48e566b30e 100644 --- a/app/helpers/warning_bar_helper.rb +++ b/app/helpers/warning_bar_helper.rb @@ -35,12 +35,6 @@ module WarningBarHelper OpenProject::Database.migrations_pending? end - def render_mysql_deprecation_warning? - current_user.admin? && - OpenProject::Database.mysql? && - current_layout == 'admin' - end - ## # By default, never show a warning bar in the # test mode due to overshadowing other elements. diff --git a/app/mailers/project_mailer.rb b/app/mailers/project_mailer.rb index edf6036254..663cac24ab 100644 --- a/app/mailers/project_mailer.rb +++ b/app/mailers/project_mailer.rb @@ -61,7 +61,7 @@ class ProjectMailer < BaseMailer open_project_headers 'Source-Project' => source_project.identifier, 'Author' => user.login - message_id project, user + message_id source_project, user with_locale_for(user) do subject = I18n.t('copy_project.failed', source_project_name: source_project.name) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index c698ed1396..d8b93c512d 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -175,6 +175,21 @@ class Attachment < ActiveRecord::Base content_type || fallback end + def copy(&block) + attachment = dup + attachment.file = diskfile + + yield attachment if block_given? + + attachment + end + + def copy!(&block) + attachment = copy &block + + attachment.save! + end + def extract_fulltext return unless OpenProject::Database.allows_tsv? job = ExtractFulltextJob.new(id) @@ -183,16 +198,26 @@ class Attachment < ActiveRecord::Base # Extract the fulltext of any attachments where fulltext is still nil. # This runs inline and not in a asynchronous worker. - def self.extract_fulltext_where_missing + def self.extract_fulltext_where_missing(run_now: true) return unless OpenProject::Database.allows_tsv? - Attachment.where(fulltext: nil).pluck(:id).each do |id| + + Attachment + .where(fulltext: nil) + .pluck(:id) + .each do |id| job = ExtractFulltextJob.new(id) - job.perform + + if run_now + job.perform + else + Delayed::Job.enqueue job, priority: ::ApplicationJob.priority_number(:low) + end end end def self.force_extract_fulltext return unless OpenProject::Database.allows_tsv? + Attachment.pluck(:id).each do |id| job = ExtractFulltextJob.new(id) job.perform diff --git a/app/models/custom_field/order_statements.rb b/app/models/custom_field/order_statements.rb index f4f54a19bc..2ea903ad6f 100644 --- a/app/models/custom_field/order_statements.rb +++ b/app/models/custom_field/order_statements.rb @@ -108,15 +108,8 @@ module CustomField::OrderStatements end def select_custom_values_as_group - aggr_sql = - if OpenProject::Database.mysql? - "GROUP_CONCAT(cv_sort.value SEPARATOR '.')" - else - "string_agg(cv_sort.value, '.')" - end - <<-SQL - COALESCE((SELECT #{aggr_sql} FROM #{CustomValue.table_name} cv_sort + COALESCE((SELECT string_agg(cv_sort.value, '.') FROM #{CustomValue.table_name} cv_sort WHERE cv_sort.customized_type='#{self.class.customized_class.name}' AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id AND cv_sort.custom_field_id=#{id} @@ -125,15 +118,8 @@ module CustomField::OrderStatements end def select_custom_values_joined_options_as_group - aggr_sql = - if OpenProject::Database.mysql? - "GROUP_CONCAT(co_sort.value SEPARATOR '.')" - else - "string_agg(co_sort.value, '.' ORDER BY co_sort.position ASC)" - end - <<-SQL - COALESCE((SELECT #{aggr_sql} FROM #{CustomOption.table_name} co_sort + COALESCE((SELECT string_agg(co_sort.value, '.' ORDER BY co_sort.position ASC) FROM #{CustomOption.table_name} co_sort LEFT JOIN #{CustomValue.table_name} cv_sort ON co_sort.id = CAST(cv_sort.value AS decimal(60,3)) WHERE cv_sort.customized_type='#{self.class.customized_class.name}' diff --git a/app/models/journal.rb b/app/models/journal.rb index 15394eaf07..68332e4fd0 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -56,17 +56,10 @@ class Journal < ActiveRecord::Base # Ensure that no INSERT/UPDATE/DELETE statements as well as other code inside :with_write_lock # is run concurrently to the code inside this block, by using database locking. - # Note for PostgreSQL: If this is called from inside a transaction, the lock will last until the + # Note: If this is called from inside a transaction, the lock will last until the # end of that transaction. - # Note for MySQL: THis method does not currently change anything (no locking at all) def self.with_write_lock(journable) - lock_name = - if OpenProject::Database.mysql? - # MySQL only supports a single lock - "journals.write_lock" - else - "journal.#{journable.class}.#{journable.id}" - end + lock_name = "journal.#{journable.class}.#{journable.id}" result = Journal.with_advisory_lock_result(lock_name, timeout_seconds: 60) do yield diff --git a/app/models/journal/aggregated_journal.rb b/app/models/journal/aggregated_journal.rb index 7165de89df..59f1382689 100644 --- a/app/models/journal/aggregated_journal.rb +++ b/app/models/journal/aggregated_journal.rb @@ -100,10 +100,10 @@ class Journal::AggregatedJournal # that our own row (master) would not already have been merged by its predecessor. If it is # (that means if we can find a valid predecessor), we drop our current row, because it will # already be present (in a merged form) in the row of our predecessor. - Journal.from("(#{sql_rough_group(1, journable, until_version, journal_id)}) #{table_name}") - .joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(2, journable, until_version, journal_id)}) addition + Journal.from("(#{sql_rough_group(journable, until_version, journal_id)}) #{table_name}") + .joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(journable, until_version, journal_id)}) addition ON #{sql_on_groups_belong_condition(table_name, 'addition')}")) - .joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(3, journable, until_version, journal_id)}) predecessor + .joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(journable, until_version, journal_id)}) predecessor ON #{sql_on_groups_belong_condition('predecessor', table_name)}")) .where(Arel.sql('predecessor.id IS NULL')) .order(Arel.sql("COALESCE(addition.created_at, #{table_name}.created_at) ASC")) @@ -166,7 +166,7 @@ class Journal::AggregatedJournal # To be able to self-join results of this statement, we add an additional column called # "group_number" to the result. This allows to compare a group resulting from this query with # its predecessor and successor. - def sql_rough_group(uid, journable, until_version, journal_id) + def sql_rough_group(journable, until_version, journal_id) if until_version && !journable raise 'need to provide a journable, when specifying a version limit' elsif journable && journable.id.nil? @@ -175,8 +175,8 @@ class Journal::AggregatedJournal conditions = additional_conditions(journable, until_version, journal_id) - "SELECT predecessor.*, #{sql_group_counter(uid)} AS group_number - FROM #{sql_rough_group_from_clause(uid)} + "SELECT predecessor.*, #{sql_group_counter} AS group_number + FROM journals predecessor #{sql_rough_group_join(conditions[:join_conditions])} #{sql_rough_group_where(conditions[:where_conditions])} #{sql_rough_group_order}" @@ -229,33 +229,10 @@ class Journal::AggregatedJournal "ORDER BY predecessor.created_at" end - # The "group_number" required in :sql_rough_group has to be generated differently depending on - # the DBMS used. This method returns the appropriate statement to be used inside a SELECT to + # This method returns the appropriate statement to be used inside a SELECT to # obtain the current group number. - # The :uid parameter allows to define non-conflicting variable names (for MySQL). - def sql_group_counter(uid) - if OpenProject::Database.mysql? - group_counter = mysql_group_count_variable(uid) - "(#{group_counter} := #{group_counter} + 1)" - else - 'row_number() OVER (ORDER BY predecessor.version ASC)' - end - end - - # MySQL requires some initialization to be performed before being able to count the groups. - # This method allows to inject further FROM sources to achieve that in a single SQL statement. - # Sadly MySQL requires the whole statement to be wrapped in parenthesis, while PostgreSQL - # prohibits that. - def sql_rough_group_from_clause(uid) - if OpenProject::Database.mysql? - "(journals predecessor, (SELECT #{mysql_group_count_variable(uid)}:=0) number_initializer)" - else - 'journals predecessor' - end - end - - def mysql_group_count_variable(uid) - "@aggregated_journal_row_counter_#{uid}" + def sql_group_counter + 'row_number() OVER (ORDER BY predecessor.version ASC)' end # Similar to the WHERE statement used in :sql_rough_group. However, this condition will @@ -281,13 +258,8 @@ class Journal::AggregatedJournal return '(true = true)' end - if OpenProject::Database.mysql? - difference = "TIMESTAMPDIFF(second, #{predecessor}.created_at, #{successor}.created_at)" - threshold = aggregation_time_seconds - else - difference = "(#{successor}.created_at - #{predecessor}.created_at)" - threshold = "interval '#{aggregation_time_seconds} second'" - end + difference = "(#{successor}.created_at - #{predecessor}.created_at)" + threshold = "interval '#{aggregation_time_seconds} second'" "(#{difference} > #{threshold})" end diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 1586322ca9..2ddbe7ac04 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -31,10 +31,8 @@ class MailHandler < ActionMailer::Base include ActionView::Helpers::SanitizeHelper include Redmine::I18n - class UnauthorizedAction < StandardError; - end - class MissingInformation < StandardError; - end + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end attr_reader :email, :user diff --git a/app/models/member_role.rb b/app/models/member_role.rb index 0db975945b..8561d2a302 100644 --- a/app/models/member_role.rb +++ b/app/models/member_role.rb @@ -98,7 +98,7 @@ class MemberRole < ActiveRecord::Base end end - users = inherited_roles_by_member.keys.map(&:user) + users = inherited_roles_by_member.keys.map(&:principal) Watcher.prune(user: users, project_id: member.project_id) unless users.empty? end diff --git a/app/models/mixins/changed_by_system.rb b/app/models/mixins/changed_by_system.rb new file mode 100644 index 0000000000..110ca432ce --- /dev/null +++ b/app/models/mixins/changed_by_system.rb @@ -0,0 +1,65 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Mixins + module ChangedBySystem + extend ActiveSupport::Concern + + def changed_by_system(attributes = nil) + @changed_by_system ||= [] + + if attributes + @changed_by_system += Array(attributes) + end + + @changed_by_system + end + + def change_by_system + prior_changes = non_no_op_changes + + ret = yield + + changed_by_system(changed_compared_to(prior_changes)) + + ret + end + + private + + def non_no_op_changes + changes.reject { |_, (old, new)| old == 0 && new.nil? } + end + + def changed_compared_to(prior_changes) + changed.select { |c| !prior_changes[c] || prior_changes[c].last != changes[c].last } + end + end +end diff --git a/app/models/principal.rb b/app/models/principal.rb index 7cb4393990..60dbf07078 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -82,12 +82,6 @@ class Principal < ActiveRecord::Base firstnamelastname = "((firstname || ' ') || lastname)" lastnamefirstname = "((lastname || ' ') || firstname)" - # special concat for mysql - if OpenProject::Database.mysql? - firstnamelastname = "CONCAT(CONCAT(firstname, ' '), lastname)" - lastnamefirstname = "CONCAT(CONCAT(lastname, ' '), firstname)" - end - s = "%#{q.to_s.downcase.strip.tr(',', '')}%" where(['LOWER(login) LIKE :s OR ' + diff --git a/app/models/project.rb b/app/models/project.rb index 8d8c367c70..e6d2dac29f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -715,13 +715,13 @@ class Project < ActiveRecord::Base @allowed_permissions ||= begin names = enabled_modules.loaded? ? enabled_module_names : enabled_modules.pluck(:name) - Redmine::AccessControl.modules_permissions(names).map(&:name) + OpenProject::AccessControl.modules_permissions(names).map(&:name) end end def allowed_actions @actions_allowed ||= allowed_permissions - .map { |permission| Redmine::AccessControl.allowed_actions(permission) } + .map { |permission| OpenProject::AccessControl.allowed_actions(permission) } .flatten end diff --git a/app/models/project/copy.rb b/app/models/project/copy.rb index f9436da86f..a569a23fb3 100644 --- a/app/models/project/copy.rb +++ b/app/models/project/copy.rb @@ -43,6 +43,8 @@ module Project::Copy def copy_attributes(project) super with_model(project) do |project| + # Clear enabled modules + self.enabled_modules = [] self.enabled_module_names = project.enabled_module_names self.types = project.types self.work_package_custom_fields = project.work_package_custom_fields diff --git a/config/initializers/rabl_hack.rb b/app/models/queries/filters/strategies/relation.rb similarity index 62% rename from config/initializers/rabl_hack.rb rename to app/models/queries/filters/strategies/relation.rb index 646eb0d161..e6f64a345a 100644 --- a/config/initializers/rabl_hack.rb +++ b/app/models/queries/filters/strategies/relation.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) @@ -27,32 +28,34 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -## -# Hack against rabl 0.13.0 which applies config.include_child_root to -# #collection as well as to #child calls as you would expect. -# -module Rabl - class Engine - def to_hash_with_hack(options = {}) - if is_collection?(@_data_object) - options[:building_collection] = true - end - to_hash_without_hack(options) - end +module Queries::Filters::Strategies + class Relation < BaseStrategy + delegate :allowed_values_subset, + to: :filter - alias_method :to_hash_without_hack, :to_hash - alias_method :to_hash, :to_hash_with_hack - end + self.supported_operators = ::Relation::TYPES.keys + %w(parent children) + self.default_operator = ::Relation::TYPE_RELATES + + def validate + unique_values = values.uniq + allowed_and_desired_values = allowed_values_subset & unique_values - class Builder - def to_hash_with_hack(object = nil, settings = nil, options = nil) - if @options[:building_collection] && !@options[:child_root] - @options[:root_name] = false + if allowed_and_desired_values.sort != unique_values.sort + errors.add(:values, :inclusion) end - to_hash_without_hack(object, settings, options) + if too_many_values + errors.add(:values, "only one value allowed") + end + end + + def valid_values! + filter.values &= allowed_values.map(&:last).map(&:to_s) end - alias_method :to_hash_without_hack, :to_hash - alias_method :to_hash, :to_hash_with_hack + private + + def too_many_values + values.reject(&:blank?).length > 1 + end end end diff --git a/app/models/queries/not_existing_filter.rb b/app/models/queries/not_existing_filter.rb index 11ba25ef8a..26ee32d1e6 100644 --- a/app/models/queries/not_existing_filter.rb +++ b/app/models/queries/not_existing_filter.rb @@ -62,6 +62,18 @@ class Queries::NotExistingFilter < Queries::Filters::Base } end + def scope + # TODO: remove switch once the WP query is a + # subclass of Queries::Base + model = if context.respond_to?(:model) + context.model + else + WorkPackage + end + + model.unscoped + end + def attributes_hash nil end diff --git a/app/models/queries/operators.rb b/app/models/queries/operators.rb index 1fa47c1c17..cd6dd17d81 100644 --- a/app/models/queries/operators.rb +++ b/app/models/queries/operators.rb @@ -47,7 +47,20 @@ module Queries::Operators Queries::Operators::Ago, Queries::Operators::OnDate, Queries::Operators::BetweenDate, - Queries::Operators::Everywhere + Queries::Operators::Everywhere, + Queries::Operators::Relates, + Queries::Operators::Duplicates, + Queries::Operators::Duplicated, + Queries::Operators::Blocks, + Queries::Operators::Blocked, + Queries::Operators::Follows, + Queries::Operators::Precedes, + Queries::Operators::Includes, + Queries::Operators::PartOf, + Queries::Operators::Requires, + Queries::Operators::Required, + Queries::Operators::Parent, + Queries::Operators::Children ] OPERATORS = Hash[*(operators.map { |o| [o.symbol.to_s, o] }).flatten].freeze diff --git a/app/models/queries/operators/blocked.rb b/app/models/queries/operators/blocked.rb new file mode 100644 index 0000000000..803e7c2ae0 --- /dev/null +++ b/app/models/queries/operators/blocked.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Blocked < Base + label ::Relation::TYPE_BLOCKED + set_symbol ::Relation::TYPE_BLOCKED + end +end diff --git a/app/models/queries/operators/blocks.rb b/app/models/queries/operators/blocks.rb new file mode 100644 index 0000000000..96e839c7a6 --- /dev/null +++ b/app/models/queries/operators/blocks.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Blocks < Base + label ::Relation::TYPE_BLOCKS + set_symbol ::Relation::TYPE_BLOCKS + end +end diff --git a/app/models/queries/operators/children.rb b/app/models/queries/operators/children.rb new file mode 100644 index 0000000000..874032cf4a --- /dev/null +++ b/app/models/queries/operators/children.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Children < Base + label 'children' + set_symbol 'children' + end +end diff --git a/app/models/queries/operators/duplicated.rb b/app/models/queries/operators/duplicated.rb new file mode 100644 index 0000000000..f02595d103 --- /dev/null +++ b/app/models/queries/operators/duplicated.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Duplicated < Base + label ::Relation::TYPE_DUPLICATED + set_symbol ::Relation::TYPE_DUPLICATED + end +end diff --git a/app/models/queries/operators/duplicates.rb b/app/models/queries/operators/duplicates.rb new file mode 100644 index 0000000000..55ae7bcc81 --- /dev/null +++ b/app/models/queries/operators/duplicates.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Duplicates < Base + label ::Relation::TYPE_DUPLICATES + set_symbol ::Relation::TYPE_DUPLICATES + end +end diff --git a/app/models/queries/operators/follows.rb b/app/models/queries/operators/follows.rb new file mode 100644 index 0000000000..11e75713b1 --- /dev/null +++ b/app/models/queries/operators/follows.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Follows < Base + label 'follows' + set_symbol ::Relation::TYPE_FOLLOWS + end +end diff --git a/app/models/queries/operators/includes.rb b/app/models/queries/operators/includes.rb new file mode 100644 index 0000000000..bf200a8d48 --- /dev/null +++ b/app/models/queries/operators/includes.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Includes < Base + label ::Relation::TYPE_INCLUDES + set_symbol ::Relation::TYPE_INCLUDES + end +end diff --git a/app/models/queries/operators/parent.rb b/app/models/queries/operators/parent.rb new file mode 100644 index 0000000000..9897291e27 --- /dev/null +++ b/app/models/queries/operators/parent.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Parent < Base + label 'parent' + set_symbol 'parent' + end +end diff --git a/app/models/queries/operators/part_of.rb b/app/models/queries/operators/part_of.rb new file mode 100644 index 0000000000..c80f56b44b --- /dev/null +++ b/app/models/queries/operators/part_of.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class PartOf < Base + label ::Relation::TYPE_PARTOF + set_symbol ::Relation::TYPE_PARTOF + end +end diff --git a/app/models/queries/operators/precedes.rb b/app/models/queries/operators/precedes.rb new file mode 100644 index 0000000000..269272319f --- /dev/null +++ b/app/models/queries/operators/precedes.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Precedes < Base + label ::Relation::TYPE_PRECEDES + set_symbol ::Relation::TYPE_PRECEDES + end +end diff --git a/app/models/queries/operators/relates.rb b/app/models/queries/operators/relates.rb new file mode 100644 index 0000000000..a385a19f35 --- /dev/null +++ b/app/models/queries/operators/relates.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Relates < Base + label 'relates' + set_symbol ::Relation::TYPE_RELATES + end +end diff --git a/app/models/queries/operators/required.rb b/app/models/queries/operators/required.rb new file mode 100644 index 0000000000..4d5ed3f629 --- /dev/null +++ b/app/models/queries/operators/required.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Required < Base + label ::Relation::TYPE_REQUIRED + set_symbol ::Relation::TYPE_REQUIRED + end +end diff --git a/app/models/queries/operators/requires.rb b/app/models/queries/operators/requires.rb new file mode 100644 index 0000000000..81ca0b2880 --- /dev/null +++ b/app/models/queries/operators/requires.rb @@ -0,0 +1,36 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Operators + class Requires < Base + label ::Relation::TYPE_REQUIRES + set_symbol ::Relation::TYPE_REQUIRES + end +end diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index 190f7a9dc3..3cac632cd5 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -35,6 +35,7 @@ module Queries::Projects query = ::Queries::Projects::ProjectQuery register.filter query, filters::AncestorFilter + register.filter query, filters::TypeFilter register.filter query, filters::ActiveOrArchivedFilter register.filter query, filters::NameAndIdentifierFilter register.filter query, filters::CustomFieldFilter diff --git a/app/models/queries/projects/filters/type_filter.rb b/app/models/queries/projects/filters/type_filter.rb new file mode 100644 index 0000000000..344f40ac02 --- /dev/null +++ b/app/models/queries/projects/filters/type_filter.rb @@ -0,0 +1,66 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module Queries + module Projects + module Filters + class TypeFilter < ::Queries::Projects::Filters::ProjectFilter + def allowed_values + @allowed_values ||= Type.pluck(:name, :id) + end + + def joins + :types + end + + def where + operator_strategy.sql_for_field(values, Type.table_name, :id) + end + + def type + :list + end + + def self.key + :type_id + end + + private + + def type_strategy + # Instead of getting the IDs of all the projects a user is allowed + # to see we only check that the value is an integer. Non valid ids + # will then simply create an empty result but will not cause any + # harm. + @type_strategy ||= ::Queries::Filters::Strategies::IntegerList.new(self) + end + end + end + end +end diff --git a/app/models/queries/work_packages.rb b/app/models/queries/work_packages.rb index 85b36aaa69..9c0b19f87c 100644 --- a/app/models/queries/work_packages.rb +++ b/app/models/queries/work_packages.rb @@ -75,6 +75,7 @@ module Queries::WorkPackages register.filter Query, filters_module::CommentFilter register.filter Query, filters_module::SubjectOrIdFilter register.filter Query, filters_module::ManualSortFilter + register.filter Query, filters_module::RelatableFilter columns_module = Queries::WorkPackages::Columns diff --git a/app/models/queries/work_packages/filter/filter_for_wp_mixin.rb b/app/models/queries/work_packages/filter/filter_for_wp_mixin.rb index 81f1078d12..9a1fd251cc 100644 --- a/app/models/queries/work_packages/filter/filter_for_wp_mixin.rb +++ b/app/models/queries/work_packages/filter/filter_for_wp_mixin.rb @@ -38,7 +38,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin end def value_objects - objects = scope.find(no_templated_values) + objects = visible_scope.find(no_templated_values) if has_templated_value? objects << ::Queries::Filters::TemplatedValue.new(WorkPackage) @@ -52,7 +52,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin end def available? - scope.exists? + visible_scope.exists? end def ar_object_filter? @@ -60,7 +60,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin end def allowed_values_subset - id_values = scope.where(id: no_templated_values).pluck(:id).map(&:to_s) + id_values = visible_scope.where(id: no_templated_values).pluck(:id).map(&:to_s) if has_templated_value? id_values + templated_value_keys @@ -71,7 +71,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin private - def scope + def visible_scope if context.project WorkPackage .visible diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index 01c95fb9c2..51ab5833af 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) diff --git a/spec_legacy/unit/lib/redmine/access_control_spec.rb b/app/models/queries/work_packages/filter/relatable_filter.rb similarity index 55% rename from spec_legacy/unit/lib/redmine/access_control_spec.rb rename to app/models/queries/work_packages/filter/relatable_filter.rb index 92586f2058..966ab9205b 100644 --- a/spec_legacy/unit/lib/redmine/access_control_spec.rb +++ b/app/models/queries/work_packages/filter/relatable_filter.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) @@ -26,34 +27,56 @@ # # See docs/COPYRIGHT.rdoc for more details. #++ -require 'legacy_spec_helper' -describe Redmine::AccessControl do - before do - @access_module = Redmine::AccessControl +class Queries::WorkPackages::Filter::RelatableFilter < Queries::WorkPackages::Filter::WorkPackageFilter + include Queries::WorkPackages::Filter::FilterForWpMixin + + def available? + User.current.allowed_to?(:manage_work_package_relations, nil, global: true) + end + + def type + :relation + end + + def type_strategy + @type_strategy ||= Queries::Filters::Strategies::Relation.new(self) + end + + def where + # all of the filter logic is handled by #scope + "(1 = 1)" + end + + def scope + if operator == Relation::TYPE_RELATES + relateable_from_or_to + elsif operator != 'parent' && canonical_operator == operator + relateable_to + else + relateable_from + end + end + + private + + def relateable_from_or_to + relateable_to.or(relateable_from) + end + + def relateable_from + WorkPackage.relateable_from(from) end - it 'should permissions' do - perms = @access_module.permissions - assert perms.is_a?(Array) - assert perms.first.is_a?(Redmine::AccessControl::Permission) + def relateable_to + WorkPackage.relateable_to(from) end - it 'should module permission' do - perm = @access_module.permission(:view_work_packages) - assert perm.is_a?(Redmine::AccessControl::Permission) - assert_equal :view_work_packages, perm.name - assert_equal :work_package_tracking, perm.project_module - assert perm.actions.is_a?(Array) - assert perm.actions.include?('issues/index') + def from + WorkPackage.find(values.first) end - it 'should no module permission' do - perm = @access_module.permission(:edit_project) - assert perm.is_a?(Redmine::AccessControl::Permission) - assert_equal :edit_project, perm.name - assert_nil perm.project_module - assert perm.actions.is_a?(Array) - assert perm.actions.include?('project_settings/show') + def canonical_operator + Relation.canonical_type(operator) end end diff --git a/app/models/queries/work_packages/filter/relates_filter.rb b/app/models/queries/work_packages/filter/relates_filter.rb index e7d5d7869c..a63aabd235 100644 --- a/app/models/queries/work_packages/filter/relates_filter.rb +++ b/app/models/queries/work_packages/filter/relates_filter.rb @@ -32,7 +32,6 @@ class Queries::WorkPackages::Filter::RelatesFilter < Queries::WorkPackages::Filter::WorkPackageFilter - include ::Queries::WorkPackages::Filter::FilterOnUndirectedRelationsMixin def relation_type diff --git a/app/models/queries/work_packages/filter/subject_or_id_filter.rb b/app/models/queries/work_packages/filter/subject_or_id_filter.rb index 00b5382bff..fcc00573b2 100644 --- a/app/models/queries/work_packages/filter/subject_or_id_filter.rb +++ b/app/models/queries/work_packages/filter/subject_or_id_filter.rb @@ -30,7 +30,6 @@ class Queries::WorkPackages::Filter::SubjectOrIdFilter < Queries::WorkPackages::Filter::WorkPackageFilter - include Queries::WorkPackages::Filter::OrFilterForWpMixin CONTAINS_OPERATOR = '~'.freeze diff --git a/app/models/queries/work_packages/filter/work_package_filter.rb b/app/models/queries/work_packages/filter/work_package_filter.rb index 6a705c202e..81bdfd3782 100644 --- a/app/models/queries/work_packages/filter/work_package_filter.rb +++ b/app/models/queries/work_packages/filter/work_package_filter.rb @@ -44,4 +44,12 @@ class Queries::WorkPackages::Filter::WorkPackageFilter < ::Queries::Filters::Bas def includes nil end + + def scope + # We only return the WorkPackage base scope for now as most of the filters + # (this one's subclasses) currently do not follow the base filter approach of using the scope. + # The intend is to have more and more wp filters use the scope method just like the + # rest of the queries (e.g. project) + WorkPackage.unscoped + end end diff --git a/app/models/query/results.rb b/app/models/query/results.rb index d2acfd51d7..7a51d79580 100644 --- a/app/models/query/results.rb +++ b/app/models/query/results.rb @@ -47,19 +47,18 @@ class ::Query::Results # Returns the work package count def work_package_count - WorkPackage.visible - .joins(all_filter_joins) - .includes(:status, :project) - .where(query.statement) - .references(:statuses, :projects) - .count + work_package_scope + .joins(all_filter_joins) + .includes(:status, :project) + .where(query.statement) + .references(:statuses, :projects) + .count rescue ::ActiveRecord::StatementInvalid => e raise ::Query::StatementInvalid.new(e.message) end def work_packages - WorkPackage - .visible + work_package_scope .where(query.statement) .where(options[:conditions]) .includes(all_includes) @@ -100,6 +99,12 @@ class ::Query::Results private + def work_package_scope + WorkPackage + .visible + .merge(filter_merges) + end + def all_includes (%i(status project) + includes_for_columns(include_columns) + @@ -210,6 +215,13 @@ class ::Query::Results query.filters.map(&:joins).flatten.compact end + def filter_merges + query.filters.inject(::WorkPackage.unscoped) do |scope, filter| + scope = scope.merge(filter.scope) + scope + end + end + def clean_symbol_list(list) list.flatten.compact.uniq.map(&:to_sym) end diff --git a/app/models/role.rb b/app/models/role.rb index 4671642365..9a3cd6b9f0 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -63,7 +63,9 @@ class Role < ActiveRecord::Base validates_length_of :name, maximum: 30 def self.givable - where(builtin: NON_BUILTIN).order(Arel.sql('position')) + where(builtin: NON_BUILTIN) + .where(type: 'Role') + .order(Arel.sql('position')) end def permissions @@ -132,14 +134,6 @@ class Role < ActiveRecord::Base end end - # Return all the permissions that can be given to the role - def setable_permissions - setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions - setable_permissions -= Redmine::AccessControl.members_only_permissions if builtin == BUILTIN_NON_MEMBER - setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if builtin == BUILTIN_ANONYMOUS - setable_permissions - end - # Return the builtin 'non member' role. If the role doesn't exist, # it will be created on the fly. def self.non_member @@ -179,12 +173,12 @@ class Role < ActiveRecord::Base private def allowed_permissions - @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.map(&:name) + @allowed_permissions ||= permissions + OpenProject::AccessControl.public_permissions.map(&:name) end def allowed_actions @actions_allowed ||= allowed_permissions.map do |permission| - Redmine::AccessControl.allowed_actions(permission) + OpenProject::AccessControl.allowed_actions(permission) end.flatten end diff --git a/app/models/user.rb b/app/models/user.rb index fee69f37e2..bcd0fa3f0b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -238,7 +238,7 @@ class User < Principal def self.activate_user!(user, session) if session[:invitation_token] token = Token::Invitation.find_by_plaintext_value session[:invitation_token] - invited_id = token && token.user.id + invited_id = token&.user&.id if user.id == invited_id user.activate! @@ -476,28 +476,28 @@ class User < Principal # Find a user account by matching the exact login and then a case-insensitive # version. Exact matches will be given priority. def self.find_by_login(login) - # force string comparison to be case sensitive on MySQL - type_cast = (OpenProject::Database.mysql?) ? 'BINARY' : '' # First look for an exact match - user = where(["#{type_cast} login = ?", login]).first + user = find_by(login: login) # Fail over to case-insensitive if none was found - user ||= where(["#{type_cast} LOWER(login) = ?", login.to_s.downcase]).first + user || where(["LOWER(login) = ?", login.to_s.downcase]).first end def self.find_by_rss_key(key) return nil unless Setting.feeds_enabled? + token = Token::Rss.find_by(value: key) - if token && token.user.active? + if token&.user&.active? token.user end end def self.find_by_api_key(key) return nil unless Setting.rest_api_enabled? + token = Token::Api.find_by_plaintext_value(key) - if token && token.user.active? + if token&.user&.active? token.user end end @@ -513,16 +513,11 @@ class User < Principal skip_suffix_check, regexp = mail_regexp(mail) # If the recipient part already contains a suffix, don't expand - return where("LOWER(mail) = ?", mail) if skip_suffix_check - - command = - if OpenProject::Database.mysql? - 'REGEXP' - else - '~*' - end - - where("LOWER(mail) #{command} ?", regexp) + if skip_suffix_check + where("LOWER(mail) = ?", mail) + else + where("LOWER(mail) ~* ?", regexp) + end end ## @@ -563,7 +558,7 @@ class User < Principal roles = [] # No role on archived projects - return roles unless project && project.active? + return roles unless project&.active? # Return all roles if user is admin return Role.givable.to_a if admin? diff --git a/app/models/user/project_authorization_cache.rb b/app/models/user/project_authorization_cache.rb index 4b8cc9930d..b57fead87e 100644 --- a/app/models/user/project_authorization_cache.rb +++ b/app/models/user/project_authorization_cache.rb @@ -59,7 +59,7 @@ class User::ProjectAuthorizationCache private def normalized_permission_name(action) - Redmine::AccessControl.permission(action) + OpenProject::AccessControl.permission(action) end def projects_by_actions diff --git a/app/models/work_package.rb b/app/models/work_package.rb index 38f4b801e9..8773209c7b 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -260,7 +260,7 @@ class WorkPackage < ActiveRecord::Base def assignable_versions @assignable_versions ||= begin current_version = fixed_version_id_changed? ? Version.find_by(id: fixed_version_id_was) : fixed_version - (project.assignable_versions + [current_version]).compact.uniq.sort + ((project&.assignable_versions || []) + [current_version]).compact.uniq.sort end end diff --git a/app/policies/work_package_policy.rb b/app/policies/work_package_policy.rb index 6e574ca936..ebffb4cdfe 100644 --- a/app/policies/work_package_policy.rb +++ b/app/policies/work_package_policy.rb @@ -51,7 +51,8 @@ class WorkPackagePolicy < BasePolicy duplicate: copy_allowed?(work_package), # duplicating is another form of copying delete: delete_allowed?(work_package), manage_subtasks: manage_subtasks_allowed?(work_package), - comment: comment_allowed?(work_package) + comment: comment_allowed?(work_package), + assign_version: assign_version_allowed?(work_package) } end @@ -125,4 +126,12 @@ class WorkPackagePolicy < BasePolicy @comment_cache[work_package.project] end + + def assign_version_allowed?(work_package) + @assign_version_cache ||= Hash.new do |hash, project| + hash[project] = user.allowed_to?(:assign_versions, work_package.project) + end + + @assign_version_cache[work_package.project] + end end diff --git a/app/seeders/basic_data/role_seeder.rb b/app/seeders/basic_data/role_seeder.rb index 74327693e8..3035388057 100644 --- a/app/seeders/basic_data/role_seeder.rb +++ b/app/seeders/basic_data/role_seeder.rb @@ -66,6 +66,7 @@ module BasicData add_work_packages move_work_packages edit_work_packages + assign_versions add_work_package_notes edit_own_work_package_notes manage_work_package_relations @@ -141,7 +142,11 @@ module BasicData end def project_admin - { name: I18n.t(:default_role_project_admin), position: 5, permissions: Role.new.setable_permissions.map(&:name) } + { + name: I18n.t(:default_role_project_admin), + position: 5, + permissions: Roles::CreateContract.new(Role.new, nil).assignable_permissions.map(&:name) + } end def non_member diff --git a/app/services/api/v3/parse_query_params_service.rb b/app/services/api/v3/parse_query_params_service.rb index 7d8427f311..349acd9c51 100644 --- a/app/services/api/v3/parse_query_params_service.rb +++ b/app/services/api/v3/parse_query_params_service.rb @@ -127,9 +127,9 @@ module API end def highlighted_attributes_from_params(params) - highlighted_attributes = params[:highlightedAttributes] + highlighted_attributes = Array(params[:highlightedAttributes].presence) - return unless highlighted_attributes + return unless highlighted_attributes.present? highlighted_attributes.map do |attr| convert_attribute(attr) diff --git a/app/services/api/v3/update_query_from_v3_params_service.rb b/app/services/api/v3/update_query_from_v3_params_service.rb index bc6505b3ab..18dbb199d1 100644 --- a/app/services/api/v3/update_query_from_v3_params_service.rb +++ b/app/services/api/v3/update_query_from_v3_params_service.rb @@ -34,7 +34,7 @@ module API self.current_user = user end - def call(params) + def call(params, valid_subset: false) parsed = ::API::V3::ParseQueryParamsService .new .call(params) @@ -42,7 +42,7 @@ module API if parsed.success? ::UpdateQueryFromParamsService .new(query, current_user) - .call(parsed.result) + .call(parsed.result, valid_subset: valid_subset) else parsed end diff --git a/app/services/api/v3/work_package_collection_from_query_service.rb b/app/services/api/v3/work_package_collection_from_query_service.rb index f83b71d5eb..2500a2fec3 100644 --- a/app/services/api/v3/work_package_collection_from_query_service.rb +++ b/app/services/api/v3/work_package_collection_from_query_service.rb @@ -37,10 +37,10 @@ module API self.current_user = user end - def call(params = {}) + def call(params = {}, valid_subset: false) update = UpdateQueryFromV3ParamsService .new(query, current_user) - .call(params) + .call(params, valid_subset: valid_subset) if update.success? representer = results_to_representer(params) diff --git a/app/services/authorization/project_query.rb b/app/services/authorization/project_query.rb index aa998e2d9e..8912338f0d 100644 --- a/app/services/authorization/project_query.rb +++ b/app/services/authorization/project_query.rb @@ -132,9 +132,9 @@ class Authorization::ProjectQuery < Authorization::AbstractQuery def self.permissions(action) if action.is_a?(Hash) - Redmine::AccessControl.allow_actions(action) + OpenProject::AccessControl.allow_actions(action) else - [Redmine::AccessControl.permission(action)].compact + [OpenProject::AccessControl.permission(action)].compact end end diff --git a/app/services/authorization/user_allowed_query.rb b/app/services/authorization/user_allowed_query.rb index 0a3333b38f..347d888002 100644 --- a/app/services/authorization/user_allowed_query.rb +++ b/app/services/authorization/user_allowed_query.rb @@ -46,7 +46,7 @@ class Authorization::UserAllowedQuery < Authorization::AbstractUserQuery has_role = roles_table[:id].not_eq(nil) has_permission = role_permissions_table[:id].not_eq(nil) - has_role_and_permission = if Redmine::AccessControl.permission(action).public? + has_role_and_permission = if OpenProject::AccessControl.permission(action).public? has_role else has_role.and(has_permission) @@ -75,7 +75,7 @@ class Authorization::UserAllowedQuery < Authorization::AbstractUserQuery transformations.register :all, :role_permissions_join, after: [:roles_join] do |statement, action| - if Redmine::AccessControl.permission(action).public? + if OpenProject::AccessControl.permission(action).public? statement else statement.outer_join(role_permissions_table) diff --git a/app/services/base_services/create.rb b/app/services/base_services/create.rb index 02f40ca990..01d1948520 100644 --- a/app/services/base_services/create.rb +++ b/app/services/base_services/create.rb @@ -65,16 +65,16 @@ module BaseServices def set_attributes(params) attributes_service_class .new(user: user, - model: new_instance, + model: new_instance(params), contract_class: contract_class) .call(params) end - def after_save(attributes_call) + def after_save(_attributes_call) # nothing for now but subclasses can override end - def new_instance + def new_instance(_params) instance_class.new end diff --git a/app/services/base_services/set_attributes.rb b/app/services/base_services/set_attributes.rb index 60c3307055..47c81330a0 100644 --- a/app/services/base_services/set_attributes.rb +++ b/app/services/base_services/set_attributes.rb @@ -35,6 +35,13 @@ module BaseServices def initialize(user:, model:, contract_class:) self.user = user self.model = model + + # Allow tracking changes caused by a user but done for him by the system. + # E.g. fixed_version of a work package might need to be changed as the user changed the project. + # This is currently used for permission checks where the changed project is checked but the fixed_version + # is not if it is done by the system. + model.extend(Mixins::ChangedBySystem) + self.contract_class = contract_class end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index afedad2d8d..be96898bb9 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -28,14 +28,4 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Members::CreateService < ::BaseServices::Create - private - - def after_save(attributes_call) - super - - # Because of the way roles are assigned further down the stack, - # the roles association is empty after assign_roles has been called. - attributes_call.result.roles.reload - end -end +class Members::CreateService < ::BaseServices::Create; end diff --git a/app/views/timelog/index.rabl b/app/services/members/delete_service.rb similarity index 87% rename from app/views/timelog/index.rabl rename to app/services/members/delete_service.rb index b0c42c8c95..7a286729d8 100644 --- a/app/views/timelog/index.rabl +++ b/app/services/members/delete_service.rb @@ -1,12 +1,12 @@ #-- copyright # OpenProject is a project management system. -# Copyright (C) 2012-2013 the OpenProject Foundation (OPF) +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or @@ -26,5 +26,4 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -collection @entries => "timeEntries" -extends 'timelog/show' +class Members::DeleteService < ::BaseServices::Delete; end diff --git a/app/views/users/show.rabl b/app/services/members/update_service.rb similarity index 84% rename from app/views/users/show.rabl rename to app/services/members/update_service.rb index 34924187a0..c0187a3887 100644 --- a/app/views/users/show.rabl +++ b/app/services/members/update_service.rb @@ -1,12 +1,14 @@ +#-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. -# Copyright (C) 2012-2013 the OpenProject Foundation (OPF) +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or @@ -26,9 +28,4 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -object @user -attributes :id, - :name, - :firstname, - :lastname - +class Members::UpdateService < ::BaseServices::Update; end diff --git a/app/views/projects/level_list.api.rabl b/app/services/roles/create_service.rb similarity index 58% rename from app/views/projects/level_list.api.rabl rename to app/services/roles/create_service.rb index abb0f57298..b54f2fcba6 100644 --- a/app/views/projects/level_list.api.rabl +++ b/app/services/roles/create_service.rb @@ -1,12 +1,14 @@ +#-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. -# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or @@ -26,24 +28,36 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -collection @projects => "projects" +class Roles::CreateService < ::BaseServices::Create + include Roles::NotifyMixin -# This is a bit verbose as Project.level_list produces an array of hashes with the form: -# [ -# { :project => , -# :level => } -# ] + private -node(:id) { |p| p[:project].id } -node(:name) { |p| p[:project].name } -node(:identifier) { |p| p[:project].identifier } -node(:has_children) { |p| !p[:project].leaf? } -node(:level) { |p| p[:level] } + def create(params) + copy_workflow_id = params.delete(:copy_workflow_from) -node(:created_on, if: lambda { |p| p[:project].created_on }) do |p| - p[:project].created_on.utc -end + super_call = super + + if super_call.success? + copy_workflows(copy_workflow_id, super_call.result) + + notify_changed_roles(:added, super_call.result) + end + + super_call + end + + def new_instance(params) + if params.delete(:global_role) + GlobalRole.new + else + super + end + end -node(:updated_on, if: lambda { |p| p[:project].updated_on }) do |p| - p[:project].updated_on.utc + def copy_workflows(copy_workflow_id, role) + if copy_workflow_id.present? && (copy_from = Role.find_by(id: copy_workflow_id)) + role.workflows.copy_from_role(copy_from) + end + end end diff --git a/app/services/roles/notify_mixin.rb b/app/services/roles/notify_mixin.rb new file mode 100644 index 0000000000..3fd72952cc --- /dev/null +++ b/app/services/roles/notify_mixin.rb @@ -0,0 +1,37 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Roles::NotifyMixin + private + + def notify_changed_roles(action, changed_role) + OpenProject::Notifications.send(:roles_changed, action: action, role: changed_role) + end +end diff --git a/app/services/roles/set_attributes_service.rb b/app/services/roles/set_attributes_service.rb new file mode 100644 index 0000000000..3d7f997320 --- /dev/null +++ b/app/services/roles/set_attributes_service.rb @@ -0,0 +1,37 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module Roles + class SetAttributesService < ::BaseServices::SetAttributes + def set_default_attributes + model.permissions = Role.non_member.permissions if model.permissions.nil? || model.permissions.empty? + end + end +end diff --git a/app/services/roles/update_service.rb b/app/services/roles/update_service.rb new file mode 100644 index 0000000000..7d10571ce0 --- /dev/null +++ b/app/services/roles/update_service.rb @@ -0,0 +1,39 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +class Roles::UpdateService < ::BaseServices::Update + include Roles::NotifyMixin + + private + + def after_safe + notify_changed_roles(:updated, model) + end +end diff --git a/app/services/update_query_from_params_service.rb b/app/services/update_query_from_params_service.rb index 821ffea652..ba6aa9babc 100644 --- a/app/services/update_query_from_params_service.rb +++ b/app/services/update_query_from_params_service.rb @@ -32,7 +32,7 @@ class UpdateQueryFromParamsService self.current_user = user end - def call(params) + def call(params, valid_subset: false) apply_group_by(params) apply_sort_by(params) @@ -51,6 +51,10 @@ class UpdateQueryFromParamsService disable_hierarchy_when_only_grouped_by(params) + if valid_subset + query.valid_subset! + end + if query.valid? ServiceResult.new(success: true, result: query) @@ -71,6 +75,7 @@ class UpdateQueryFromParamsService def apply_filters(params) return unless params[:filters] + query.filters = [] params[:filters].each do |filter| diff --git a/app/services/work_packages/copy_service.rb b/app/services/work_packages/copy_service.rb index c14dd96deb..97a05c0292 100644 --- a/app/services/work_packages/copy_service.rb +++ b/app/services/work_packages/copy_service.rb @@ -75,8 +75,7 @@ class WorkPackages::CopyService wp .attributes .slice(*writable_work_package_attributes(wp)) - .merge('author_id' => user.id, - 'parent_id' => wp.parent_id, + .merge('parent_id' => wp.parent_id, 'custom_field_values' => wp.custom_value_attributes) .merge(override) end diff --git a/app/services/work_packages/set_attributes_service.rb b/app/services/work_packages/set_attributes_service.rb index ced352e268..9802bded27 100644 --- a/app/services/work_packages/set_attributes_service.rb +++ b/app/services/work_packages/set_attributes_service.rb @@ -32,19 +32,27 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes private def set_attributes(attributes) - if attributes.key?(:attachment_ids) - work_package.attachments_replacements = Attachment.where(id: attributes[:attachment_ids]) + set_attachments_attributes(attributes) + set_static_attributes(attributes) + + work_package.change_by_system do + set_default_attributes + update_project_dependent_attributes end - set_static_attributes(attributes) - set_default_attributes - unify_dates - update_project_dependent_attributes set_custom_attributes(attributes) - update_dates - reset_custom_values - reassign_invalid_status_if_type_changed - set_templated_description + + work_package.change_by_system do + update_dates + reassign_invalid_status_if_type_changed + set_templated_description + end + end + + def set_attachments_attributes(attributes) + return unless attributes.key?(:attachment_ids) + + work_package.attachments_replacements = Attachment.where(id: attributes[:attachment_ids]) end def set_static_attributes(attributes) @@ -104,6 +112,8 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes end work_package.attributes = assignable_attributes + + initialize_unset_custom_values end def unify_dates @@ -124,13 +134,17 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes def update_project_dependent_attributes return unless work_package.project_id_changed? && work_package.project_id - set_fixed_version_to_nil - reassign_category + work_package.change_by_system do + set_fixed_version_to_nil + reassign_category - reassign_type unless work_package.type_id_changed? + reassign_type unless work_package.type_id_changed? + end end def update_dates + unify_dates + min_start = new_start_date if min_start @@ -181,9 +195,9 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes end end - def reset_custom_values - # Take over any default custom values - # for new custom fields + # Take over any default custom values + # for new custom fields + def initialize_unset_custom_values work_package.set_default_values! if custom_field_context_changed? end diff --git a/app/services/work_packages/shared/update_attributes.rb b/app/services/work_packages/shared/update_attributes.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/project_mailer/copy_project_succeeded.html.erb b/app/views/project_mailer/copy_project_succeeded.html.erb index 9d2dac5cce..c1570714be 100644 --- a/app/views/project_mailer/copy_project_succeeded.html.erb +++ b/app/views/project_mailer/copy_project_succeeded.html.erb @@ -33,5 +33,5 @@ See docs/COPYRIGHT.rdoc for more details. <% unless @errors.empty? %>
- <%= render partial: 'errors', locals: { errors: @errors } %> + <%= render partial: 'user_mailer/errors', locals: { errors: @errors } %> <% end %> diff --git a/app/views/project_mailer/copy_project_succeeded.text.erb b/app/views/project_mailer/copy_project_succeeded.text.erb index a62eb5eb6f..c84c815f2e 100644 --- a/app/views/project_mailer/copy_project_succeeded.text.erb +++ b/app/views/project_mailer/copy_project_succeeded.text.erb @@ -31,5 +31,5 @@ See docs/COPYRIGHT.rdoc for more details. <%= t('copy_project.text.succeeded', source_project_name: @source_project.name, target_project_name: @target_project.name) %> <% unless @errors.empty? %> ---------------------------------------- - <%= render partial: 'errors', locals: { errors: @errors } %> + <%= render partial: 'user_mailer/errors', locals: { errors: @errors } %> <% end %> diff --git a/app/views/projects/form/_modules.html.erb b/app/views/projects/form/_modules.html.erb index 15b4036a78..56857ed39b 100644 --- a/app/views/projects/form/_modules.html.erb +++ b/app/views/projects/form/_modules.html.erb @@ -40,7 +40,7 @@ See docs/COPYRIGHT.rdoc for more details. - <% sorted_modules = Redmine::AccessControl.available_project_modules.sort_by { |name| l_or_humanize(name, prefix: "project_module_") } %> + <% sorted_modules = OpenProject::AccessControl.available_project_modules.sort_by { |name| l_or_humanize(name, prefix: "project_module_") } %> <% sorted_modules.each do |name| %>
<%= form.collection_check_box :enabled_module_names, diff --git a/app/views/roles/_form.html.erb b/app/views/roles/_form.html.erb index 832d7cc34c..9539c61017 100644 --- a/app/views/roles/_form.html.erb +++ b/app/views/roles/_form.html.erb @@ -1,18 +1,11 @@ <%#-- copyright -OpenProject is a project management system. -Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +OpenProject Global Roles Plugin -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2017 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team +Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. +version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -23,57 +16,51 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -See docs/COPYRIGHT.rdoc for more details. - ++#%> -<%= error_messages_for 'role' %> +<%= render :partial => 'shared/global_roles_header' %> + +<% roles ||= nil %> -<% unless @role.builtin? %> -
<%= f.text_field :name, required: true, container_class: '-middle' %>
-
<%= f.check_box :assignable %>
- <% if @role.new_record? && @roles.any? %> -
-
- <%= styled_label_tag 'copy_workflow_from', t(:label_copy_workflow_from) %> -
- <%= styled_select_tag(:copy_workflow_from, - options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from]), - include_blank: true, - container_class: '-middle') %> -
-
+<%= error_messages_for_contract(role, @call.errors) if @call %> + +
+ <%= f.text_field :name, required: true, container_class: '-slim' %> +
+
+ <% if role.new_record? %> + <%= styled_label_tag :global_role, t(:label_global_role) %> +
+ <%= styled_check_box_tag("global_role", "1", role.is_a?(GlobalRole))%> +
+ <% else %> + <%= styled_label_tag :unchangeable, "#{t(:label_role_type)} #{t(:label_not_changeable)}" %> +
+ <%= (role.is_a?(GlobalRole) ? t(:label_global_role) : t(:label_member_role))%>
<% end %> +
+ +<% if role.new_record? || role.is_a?(GlobalRole) %> + +<% end %> + +<% if role.new_record? || !role.is_a?(GlobalRole) %> +
+ <%= render partial: "member_attributes", locals: { f: f, role: role, roles: roles&.select {|r| !r.is_a?(GlobalRole)} }%> +
+<% end %> + +<% if role.new_record? || role.is_a?(GlobalRole) %> +
> + <%= render partial: "permissions", locals: {permissions: grouped_setable_permissions(GlobalRole.new), role: role, showGlobalRole: true }%> +
+<% end %> +<% if role.new_record? || !role.is_a?(GlobalRole) %> +
> + <%= render partial: "permissions", locals: {permissions: grouped_setable_permissions(role), role: role, showGlobalRole: false }%> +
<% end %> -
- <% @permissions.each do |mod, mod_permissions| %> - <% module_name = mod.blank? ? 'fieldset--' + Project.model_name.human.downcase.gsub(' ', '_') : 'fieldset--' + l_or_humanize(mod, prefix: 'project_module_').downcase.gsub(' ', '_') %> -
- <% module_name = mod.blank? ? "form--" + I18n.t('attributes.project') : "form--" + l_or_humanize(mod, prefix: 'project_module_').gsub(' ','_') %> -
-
- <%= mod.blank? ? Project.model_name.human : l_or_humanize(mod, prefix: 'project_module_') %> -
- - (<%= check_all_links module_name %>) - -
-
- <% mod_permissions.each do |permission| %> -
-
- -
-
- <% end %> -
-
-
-
- <% end %> -
diff --git a/modules/global_roles/app/views/roles/_member_attributes.html.erb b/app/views/roles/_member_attributes.html.erb similarity index 100% rename from modules/global_roles/app/views/roles/_member_attributes.html.erb rename to app/views/roles/_member_attributes.html.erb diff --git a/modules/global_roles/app/views/roles/_member_form.html.erb b/app/views/roles/_member_form.html.erb similarity index 100% rename from modules/global_roles/app/views/roles/_member_form.html.erb rename to app/views/roles/_member_form.html.erb diff --git a/modules/global_roles/app/views/roles/_permissions.html.erb b/app/views/roles/_permissions.html.erb similarity index 97% rename from modules/global_roles/app/views/roles/_permissions.html.erb rename to app/views/roles/_permissions.html.erb index 41f9c6d611..656bebc709 100644 --- a/modules/global_roles/app/views/roles/_permissions.html.erb +++ b/app/views/roles/_permissions.html.erb @@ -31,7 +31,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- <% mod_permissions.each do |permission| %> + <% Array(mod_permissions).each do |permission| %>
diff --git a/frontend/src/app/components/wp-query-select/wp-query-select-dropdown.component.ts b/frontend/src/app/components/wp-query-select/wp-query-select-dropdown.component.ts index 2d47b3d306..248bded468 100644 --- a/frontend/src/app/components/wp-query-select/wp-query-select-dropdown.component.ts +++ b/frontend/src/app/components/wp-query-select/wp-query-select-dropdown.component.ts @@ -73,8 +73,8 @@ interface IQueryAutocompleteJQuery extends JQuery { templateUrl: './wp-query-select.template.html', }) export class WorkPackageQuerySelectDropdownComponent implements OnInit, OnDestroy { - @ViewChild('wpQueryMenuSearchInput') _wpQueryMenuSearchInput:ElementRef; - @ViewChild('queryResultsContainer') _queryResultsContainerElement:ElementRef; + @ViewChild('wpQueryMenuSearchInput', { static: true }) _wpQueryMenuSearchInput:ElementRef; + @ViewChild('queryResultsContainer', { static: true }) _queryResultsContainerElement:ElementRef; public loading = false; public noResults = false; diff --git a/frontend/src/app/components/wp-query/url-params-helper.ts b/frontend/src/app/components/wp-query/url-params-helper.ts index 455b3ea9c1..f5ba6410ad 100644 --- a/frontend/src/app/components/wp-query/url-params-helper.ts +++ b/frontend/src/app/components/wp-query/url-params-helper.ts @@ -144,11 +144,6 @@ export class UrlParamsHelperService { return paramsData; } - private encode(paramsData:any, query:QueryResource) { - - return paramsData; - } - private encodeTimelineVisible(paramsData:any, query:QueryResource) { if (!!query.timelineVisible) { paramsData.tv = query.timelineVisible; @@ -243,7 +238,7 @@ export class UrlParamsHelperService { return queryData; } - public buildV3GetQueryFromQueryResource(query:QueryResource, additionalParams:any = {}) { + public buildV3GetQueryFromQueryResource(query:QueryResource, additionalParams:any = {}, contextual:any = {}) { var queryData:any = {}; queryData["columns[]"] = this.buildV3GetColumnsFromQueryResource(query); @@ -267,7 +262,7 @@ export class UrlParamsHelperService { queryData.groupBy = _.get(query.groupBy, 'id', ''); // Filters - queryData.filters = this.buildV3GetFiltersAsJson(query.filters); + queryData.filters = this.buildV3GetFiltersAsJson(query.filters, contextual); // Sortation queryData.sortBy = this.buildV3GetSortByFromQuery(query); @@ -295,7 +290,7 @@ export class UrlParamsHelperService { private buildV3GetColumnsFromQueryResource(query:QueryResource) { if (query.columns) { - return query.columns.map((column:any) => column.id); + return query.columns.map((column:any) => column.id || column.idFromLink); } else if (query._links.columns) { return query._links.columns.map((column:HalLink) => { let id = column.href!; @@ -305,11 +300,17 @@ export class UrlParamsHelperService { } } - public buildV3GetFilters(filters:QueryFilterInstanceResource[]):ApiV3Filter[] { + public buildV3GetFilters(filters:QueryFilterInstanceResource[], replacements = {}):ApiV3Filter[] { let newFilters = filters.map((filter:QueryFilterInstanceResource) => { let id = this.buildV3GetFilterIdFromFilter(filter); let operator = this.buildV3GetOperatorIdFromFilter(filter); - let values = this.buildV3GetValuesFromFilter(filter); + let values = this.buildV3GetValuesFromFilter(filter).map(value => { + _.each(replacements, (val:string, key:string) => { + value = value.replace(`{${key}}`, val); + }); + + return value; + }); const filterHash:ApiV3Filter = {}; filterHash[id] = { operator: operator as FilterOperator, values: values }; @@ -320,11 +321,11 @@ export class UrlParamsHelperService { return newFilters; } - public buildV3GetFiltersAsJson(filter:QueryFilterInstanceResource[]) { - return JSON.stringify(this.buildV3GetFilters(filter)); + public buildV3GetFiltersAsJson(filter:QueryFilterInstanceResource[], contextual = {}) { + return JSON.stringify(this.buildV3GetFilters(filter, contextual)); } - private buildV3GetFilterIdFromFilter(filter:QueryFilterInstanceResource) { + public buildV3GetFilterIdFromFilter(filter:QueryFilterInstanceResource) { let href = filter.filter ? filter.filter.$href : filter._links.filter.href; return this.idFromHref(href); diff --git a/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts b/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts index bdca4d63f4..0419f364f0 100644 --- a/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts +++ b/frontend/src/app/components/wp-relations/embedded/children/wp-children-query.component.ts @@ -40,6 +40,9 @@ import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; import {WorkPackageRelationQueryBase} from "core-components/wp-relations/embedded/wp-relation-query.base"; import {WpChildrenInlineCreateService} from "core-components/wp-relations/embedded/children/wp-children-inline-create.service"; import {WorkPackageCacheService} from "core-components/work-packages/work-package-cache.service"; +import {filter} from "rxjs/operators"; +import {QueryResource} from "core-app/modules/hal/resources/query-resource"; +import {GroupDescriptor} from "core-components/work-packages/wp-single-view/wp-single-view.component"; @Component({ selector: 'wp-children-query', @@ -50,16 +53,18 @@ import {WorkPackageCacheService} from "core-components/work-packages/work-packag }) export class WorkPackageChildrenQueryComponent extends WorkPackageRelationQueryBase implements OnInit, OnDestroy { @Input() public workPackage:WorkPackageResource; - @Input() public query:any; + @Input() public query:QueryResource; + + /** An optional group descriptor if we're rendering on the single view */ + @Input() public group?:GroupDescriptor; @Input() public addExistingChildEnabled:boolean = false; - @ViewChild('childrenEmbeddedTable') private childrenEmbeddedTable:WorkPackageEmbeddedTableComponent; public tableActions:OpTableActionFactory[] = [ OpUnlinkTableAction.factoryFor( 'remove-child-action', this.I18n.t('js.relation_buttons.remove_child'), (child:WorkPackageResource) => { - this.childrenEmbeddedTable.loadingIndicator = this.wpRelationsHierarchyService.removeChild(child); + this.embeddedTable.loadingIndicator = this.wpRelationsHierarchyService.removeChild(child); }, (child:WorkPackageResource) => !!child.changeParent ) @@ -79,12 +84,13 @@ export class WorkPackageChildrenQueryComponent extends WorkPackageRelationQueryB this.wpInlineCreate.referenceTarget = this.workPackage; // Set up the query props - this.buildQueryProps(); + this.queryProps = this.buildQueryProps(); // Refresh table when work package is refreshed this.wpCacheService .observe(this.workPackage.id!) .pipe( + filter(() => this.embeddedTable && this.embeddedTable.isInitialized), untilComponentDestroyed(this) ) .subscribe(() => this.refreshTable()); diff --git a/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.html b/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.html index 95098e10fe..ba86e21614 100644 --- a/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.html +++ b/frontend/src/app/components/wp-relations/embedded/inline/add-existing/wp-relation-inline-add-existing.component.html @@ -1,14 +1,15 @@
-
-
+
+
-
+
{ + let id = this.urlParamsHelper.buildV3GetFilterIdFromFilter(filter); + return relationTypes.indexOf(id) === -1; + }); + + return this.urlParamsHelper.buildV3GetFilters(filters); + } } diff --git a/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts b/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts index 8c21e68f38..2163ac595b 100644 --- a/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts +++ b/frontend/src/app/components/wp-relations/embedded/relations/wp-relation-query.component.ts @@ -41,6 +41,9 @@ import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notific import {forkJoin, merge} from "rxjs"; import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service"; import {WorkPackageTableRefreshService} from "core-components/wp-table/wp-table-refresh-request.service"; +import {filter, skip} from "rxjs/operators"; +import {QueryResource} from "core-app/modules/hal/resources/query-resource"; +import {GroupDescriptor} from "core-components/work-packages/wp-single-view/wp-single-view.component"; @Component({ selector: 'wp-relation-query', @@ -52,8 +55,8 @@ import {WorkPackageTableRefreshService} from "core-components/wp-table/wp-table- export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryBase implements OnInit, OnDestroy { @Input() public workPackage:WorkPackageResource; - @Input() public query:any; - @Input() public group:any; + @Input() public query:QueryResource; + @Input() public group:GroupDescriptor; public tableActions:OpTableActionFactory[] = [ OpUnlinkTableAction.factoryFor( @@ -87,7 +90,7 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB this.wpInlineCreate.relationType = relationType; // Set up the query props - this.buildQueryProps(); + this.queryProps = this.buildQueryProps(); // Wire the successful saving of a new addition to refreshing the embedded table this.wpInlineCreate.newInlineWorkPackageCreated @@ -95,9 +98,11 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB .subscribe((toId:string) => this.addRelation(toId)); // When relations have changed, refresh this table - this.wpRelations.observe(this.workPackage.id!) - .pipe(untilComponentDestroyed(this)) + .pipe( + filter(val => !_.isEmpty(val)), + untilComponentDestroyed(this) + ) .subscribe(() => this.refreshTable()); } @@ -115,6 +120,6 @@ export class WorkPackageRelationQueryComponent extends WorkPackageRelationQueryB } private getRelationTypeFromQuery() { - return this.group.relationType; + return this.group.relationType!; } } diff --git a/frontend/src/app/components/wp-relations/embedded/wp-relation-query.base.ts b/frontend/src/app/components/wp-relations/embedded/wp-relation-query.base.ts index 6593956072..f94aae3e48 100644 --- a/frontend/src/app/components/wp-relations/embedded/wp-relation-query.base.ts +++ b/frontend/src/app/components/wp-relations/embedded/wp-relation-query.base.ts @@ -31,18 +31,24 @@ import {ViewChild} from "@angular/core"; import {WorkPackageEmbeddedTableComponent} from "core-components/wp-table/embedded/wp-embedded-table.component"; import {QueryResource} from "core-app/modules/hal/resources/query-resource"; import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper"; +import {ErrorResource} from "core-app/modules/hal/resources/error-resource"; +import {query} from "@angular/animations"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; export abstract class WorkPackageRelationQueryBase { public workPackage:WorkPackageResource; /** Input is either a query resource, or directly query props */ - public query:Object; + public query:QueryResource|Object; /** Query props are derived from the query resource, if any */ public queryProps:Object; + /** Whether this section should be hidden completely (due to missing permissions e.g.) */ + public hidden:boolean = false; + /** Reference to the embedded table instance */ - @ViewChild('embeddedTable') protected embeddedTable:WorkPackageEmbeddedTableComponent; + @ViewChild('embeddedTable', { static: false }) protected embeddedTable:WorkPackageEmbeddedTableComponent; constructor(protected queryUrlParamsHelper:UrlParamsHelperService) { } @@ -55,30 +61,53 @@ export abstract class WorkPackageRelationQueryBase { } /** - * Create a contextualized copy of the query where all - * references to the templated value are replaced with the actual work package ID. + * Special handling for query loading when a project filter is involved. + * + * Ensure that at least one project was visible to the user or otherwise, + * hide the creation from them. + * cf. OP#30106 + * @param query */ - protected contextualizedQuery(query:QueryResource) { - let duppedQuery = _.cloneDeep(query); + public handleQueryLoaded(loaded:QueryResource) { + // We only handle loaded queries + if (!(this.query instanceof QueryResource)) { + return; + } - _.each(duppedQuery.filters, (filter) => { - if (filter._links.values[0] && filter._links.values[0].templated) { - filter._links.values[0].href = filter._links.values[0].href.replace('{id}', this.workPackage.id!); - } - }); + const filtersLength = this.projectValuesCount(this.query); + const loadedFiltersLength = this.projectValuesCount(loaded); - return duppedQuery; + // Does the default have a project filter, but the other does not? + if (filtersLength !== null && loadedFiltersLength === null) { + this.hidden = true; + } + + // Has a project filter been reduced to zero elements? + if (filtersLength && loadedFiltersLength && filtersLength > 0 && loadedFiltersLength === 0) { + this.hidden = true; + } + } + + /** + * Get the filters of the query props + */ + protected projectValuesCount(query:QueryResource):number|null { + const project = query.filters.find(f => f.id === 'project'); + return project ? project.values.length : null; } /** * Set up the query props from input */ protected buildQueryProps() { - if (this.query && (this.query as any)._type === 'Query') { - let query = this.contextualizedQuery(this.query as QueryResource); - this.queryProps = this.queryUrlParamsHelper.buildV3GetQueryFromQueryResource(query, {}); + if (this.query instanceof QueryResource) { + return this.queryUrlParamsHelper.buildV3GetQueryFromQueryResource( + this.query, + { valid_subset: true }, + { id: this.workPackage.id! } + ); } else { - this.queryProps = this.query; + return this.query; } } } diff --git a/frontend/src/app/components/wp-relations/embedded/wp-relation-query.html b/frontend/src/app/components/wp-relations/embedded/wp-relation-query.html index 016742c199..9ee2144eb2 100644 --- a/frontend/src/app/components/wp-relations/embedded/wp-relation-query.html +++ b/frontend/src/app/components/wp-relations/embedded/wp-relation-query.html @@ -1,13 +1,23 @@ - - + +
+
+

+
+
+ + + +
diff --git a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts index ac9beb6cee..df440fc3fb 100644 --- a/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relation-row/wp-relation-row.component.ts @@ -19,7 +19,7 @@ export class WorkPackageRelationRowComponent implements OnInit, OnDestroy { @Input() public relatedWorkPackage:WorkPackageResource; @Input() public groupByWorkPackageType:boolean; - @ViewChild('relationDescriptionTextarea') readonly relationDescriptionTextarea:ElementRef; + @ViewChild('relationDescriptionTextarea', { static: false }) readonly relationDescriptionTextarea:ElementRef; public relationType:string; public showRelationInfo:boolean = false; diff --git a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts index 52643b2916..11bd647869 100644 --- a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts @@ -46,7 +46,7 @@ import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/iso import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {WorkPackageCollectionResource} from "core-app/modules/hal/resources/wp-collection-resource"; import {CurrentProjectService} from "core-components/projects/current-project.service"; -import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; +import {ApiV3Filter, ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service"; import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; @@ -68,8 +68,11 @@ export class WorkPackageRelationsAutocomplete implements AfterContentInit { @Input() selectedRelationType:string; @Input() filterCandidatesFor:string; + /** Do we take the current query filters into account? */ + @Input() additionalFilters:ApiV3Filter[] = []; + @Input() appendToContainer:string = 'body'; - @ViewChild(NgSelectComponent) public ngSelectComponent:NgSelectComponent; + @ViewChild(NgSelectComponent, { static: true }) public ngSelectComponent:NgSelectComponent; @Output() onCancel = new EventEmitter(); @Output() onReferenced = new EventEmitter(); @@ -103,7 +106,6 @@ export class WorkPackageRelationsAutocomplete implements AfterContentInit { if (!this.ngSelectComponent) { return; } - this.ngSelectComponent.open(); setTimeout(() => { this.ngSelectComponent.focus(); @@ -138,6 +140,7 @@ export class WorkPackageRelationsAutocomplete implements AfterContentInit { return from( this.workPackage.availableRelationCandidates.$link.$fetch({ query: query, + filters: JSON.stringify(this.additionalFilters), type: this.filterCandidatesFor || this.selectedRelationType }) as Promise ) diff --git a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts index b8b351e9ae..60a66181fa 100644 --- a/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relations-create/wp-relations-create.component.ts @@ -13,7 +13,7 @@ import {WorkPackageTableRefreshService} from "core-components/wp-table/wp-table- }) export class WorkPackageRelationsCreateComponent { @Input() readonly workPackage:WorkPackageResource; - @ViewChild('focusAfterSave') readonly focusAfterSave:ElementRef; + @ViewChild('focusAfterSave', { static: false }) readonly focusAfterSave:ElementRef; public showRelationsCreateForm:boolean = false; public selectedRelationType:string = RelationResource.DEFAULT(); diff --git a/frontend/src/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts b/frontend/src/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts index 5f2aafcb34..c9d9ad2ea4 100644 --- a/frontend/src/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts +++ b/frontend/src/app/components/wp-relations/wp-relations-group/wp-relations-group.component.ts @@ -45,7 +45,7 @@ export class WorkPackageRelationsGroupComponent { @Output() public onToggleGroupBy = new EventEmitter(); - @ViewChild('wpRelationGroupByToggler') readonly toggleElement:ElementRef; + @ViewChild('wpRelationGroupByToggler', { static: false }) readonly toggleElement:ElementRef; public text = { groupByType: this.I18n.t('js.relation_buttons.group_by_wp_type'), diff --git a/frontend/src/app/components/wp-table/configuration-modal/tabs/columns-tab.component.ts b/frontend/src/app/components/wp-table/configuration-modal/tabs/columns-tab.component.ts index 1beb63e664..fb06bdfbab 100644 --- a/frontend/src/app/components/wp-table/configuration-modal/tabs/columns-tab.component.ts +++ b/frontend/src/app/components/wp-table/configuration-modal/tabs/columns-tab.component.ts @@ -33,7 +33,7 @@ export class WpTableConfigurationColumnsTab implements TabComponent, AfterViewIn upsaleCheckOutLink: this.I18n.t('js.work_packages.table_configuration.upsale.check_out_link') }; - @ViewChild('select2Columns') select2Columns:ElementRef; + @ViewChild('select2Columns', { static: true }) select2Columns:ElementRef; constructor(readonly injector:Injector, readonly I18n:I18nService, diff --git a/frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.html b/frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.html index 0258a5fe52..68f325e590 100644 --- a/frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.html +++ b/frontend/src/app/components/wp-table/configuration-modal/tabs/highlighting-tab.component.html @@ -45,9 +45,6 @@ - diff --git a/frontend/src/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts b/frontend/src/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts index e199a3fecc..5d8b1ce190 100644 --- a/frontend/src/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts +++ b/frontend/src/app/components/wp-table/configuration-modal/wp-table-configuration.modal.ts @@ -62,7 +62,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme public selectedColumnMap:{ [id:string]:boolean } = {}; // Get the view child we'll use as the portal host - @ViewChild('tabContentOutlet') tabContentOutlet:ElementRef; + @ViewChild('tabContentOutlet', { static: true }) tabContentOutlet:ElementRef; // And a reference to the actual portal host interface public tabPortalHost:TabPortalOutlet; diff --git a/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts index 8ea47a04bd..875035004b 100644 --- a/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/src/app/components/wp-table/embedded/wp-embedded-table.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service'; import {WorkPackageTablePaginationService} from 'core-components/wp-fast-table/state/wp-table-pagination.service'; import {OpTableActionFactory} from 'core-components/wp-table/table-actions/table-action'; @@ -24,6 +24,12 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo @Input() public tableActions:OpTableActionFactory[] = []; @Input() public externalHeight:boolean = false; + /** Inform about loading errors */ + @Output() public onError = new EventEmitter(); + + /** Inform about loaded query */ + @Output() public onQueryLoaded = new EventEmitter(); + readonly QueryDm:QueryDmService = this.injector.get(QueryDmService); readonly opModalService:OpModalService = this.injector.get(OpModalService); readonly tableActionsService:OpTableActionsService = this.injector.get(OpTableActionsService); @@ -127,7 +133,6 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo return Promise.resolve(this.loadedQuery!); } - // HACK: Decrease loading time of queries when results are not needed. // We should allow the backend to disable results embedding instead. if (!this.configuration.tableVisible) { @@ -151,6 +156,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo ) .then((query:QueryResource) => { this.initializeStates(query, query.results); + this.onQueryLoaded.emit(query); return query; }) .catch((error) => { @@ -158,6 +164,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo 'js.error.embedded_table_loading', { message: _.get(error, 'message', error) } ); + this.onError.emit(error); }); if (visible) { diff --git a/frontend/src/app/components/wp-table/external-configuration/external-query-configuration.component.ts b/frontend/src/app/components/wp-table/external-configuration/external-query-configuration.component.ts index 2e6fa1a30b..0ad9715de5 100644 --- a/frontend/src/app/components/wp-table/external-configuration/external-query-configuration.component.ts +++ b/frontend/src/app/components/wp-table/external-configuration/external-query-configuration.component.ts @@ -17,7 +17,7 @@ export interface QueryConfigurationLocals { }) export class ExternalQueryConfigurationComponent implements AfterViewInit { - @ViewChild('embeddedTableForConfiguration') private embeddedTable:WorkPackageEmbeddedTableComponent; + @ViewChild('embeddedTableForConfiguration', { static: true }) private embeddedTable:WorkPackageEmbeddedTableComponent; constructor(@Inject(OpQueryConfigurationLocalsToken) readonly locals:QueryConfigurationLocals, readonly cdRef:ChangeDetectorRef) { diff --git a/frontend/src/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts b/frontend/src/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts index 51b87b324c..e0f04f3240 100644 --- a/frontend/src/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts +++ b/frontend/src/app/components/wp-table/timeline/cells/timeline-cell-renderer.ts @@ -252,6 +252,7 @@ export class TimelineCellRenderer { this.checkForActiveSelectionMode(renderInfo, bar); this.checkForSpecialDisplaySituations(renderInfo, bar); + this.applyTypeColor(renderInfo, bar); return true; } @@ -327,7 +328,7 @@ export class TimelineCellRenderer { // create center label const labelCenter = document.createElement('div'); labelCenter.classList.add(classNameBarLabel); - this.applyTypeColor(renderInfo.workPackage, labelCenter); + this.applyTypeColor(renderInfo, labelCenter); element.appendChild(labelCenter); // create left label @@ -366,15 +367,25 @@ export class TimelineCellRenderer { return labels; } - protected applyTypeColor(wp:WorkPackageResource, bg:HTMLElement):void { + protected applyTypeColor(renderInfo:RenderInfo, bg:HTMLElement):void { + let wp = renderInfo.workPackage; let type = wp.type; + let selectionMode = renderInfo.viewParams.activeSelectionMode; - if (!type) { + if (!type && !selectionMode) { bg.style.backgroundColor = this.fallbackColor; } + bg.style.backgroundColor = ''; + + // Don't apply the class in selection mode const id = type.id; - bg.classList.add(Highlighting.backgroundClass('type', id!)); + if (renderInfo.viewParams.activeSelectionMode) { + bg.classList.remove(Highlighting.backgroundClass('type', id!)); + return; + } else { + bg.classList.add(Highlighting.backgroundClass('type', id!)); + } } protected assignDate(changeset:WorkPackageChangeset, attributeName:string, value:moment.Moment) { @@ -408,7 +419,7 @@ export class TimelineCellRenderer { bar.style.background = 'none'; } else { // Apply the background color - this.applyTypeColor(renderInfo.workPackage, bar); + this.applyTypeColor(renderInfo, bar); } } diff --git a/frontend/src/app/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts b/frontend/src/app/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts index 32bef5562e..e20a502ee2 100644 --- a/frontend/src/app/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts +++ b/frontend/src/app/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts @@ -47,7 +47,6 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { const diamond = document.createElement('div'); diamond.className = 'diamond'; - diamond.style.backgroundColor = '#DDDDDD'; diamond.style.left = '0.5em'; diamond.style.height = '1em'; diamond.style.width = '1em'; @@ -130,7 +129,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer { diamond.style.width = 15 + 'px'; diamond.style.height = 15 + 'px'; diamond.style.marginLeft = -(15 / 2) + (renderInfo.viewParams.pixelPerDay / 2) + 'px'; - this.applyTypeColor(renderInfo.workPackage, diamond); + this.applyTypeColor(renderInfo, diamond); // offset left const offsetStart = date.diff(viewParams.dateDisplayStart, 'days'); diff --git a/frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts b/frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts index cbdaedb644..1fffda08ae 100644 --- a/frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts +++ b/frontend/src/app/components/wp-table/timeline/container/wp-timeline-container.directive.ts @@ -347,6 +347,7 @@ export class WorkPackageTimelineTableController implements AfterViewInit, OnDest this.selectionParams.notification = this.NotificationsService.addNotice(this.text.selectionMode); this.$element.addClass('active-selection-mode'); + this.refreshView(); } diff --git a/frontend/src/app/globals/global-listeners.ts b/frontend/src/app/globals/global-listeners.ts index 3cf478adc6..f71c6b348e 100644 --- a/frontend/src/app/globals/global-listeners.ts +++ b/frontend/src/app/globals/global-listeners.ts @@ -35,6 +35,7 @@ import {registerRequestForConfirmation} from "core-app/globals/global-listeners/ * A set of listeners that are relevant on every page to set sensible defaults */ (function($:JQueryStatic) { + $(function() { $(document.documentElement!) .on('click', (evt:JQueryEventObject) => { @@ -50,6 +51,13 @@ import {registerRequestForConfirmation} from "core-app/globals/global-listeners/ return true; }); + // Jump to the element given by location.hash, if present + const hash = window.location.hash; + if (hash && hash.startsWith('#')) { + const el = document.querySelector(hash); + el && el.scrollIntoView(); + } + // Disable global drag & drop handling, which results in the browser loading the image and losing the page $(document.documentElement!) .on('dragover drop', (evt:JQueryEventObject) => { diff --git a/frontend/src/app/helpers/selection-helpers.ts b/frontend/src/app/helpers/selection-helpers.ts index 942e2ff2ae..e400174947 100644 --- a/frontend/src/app/helpers/selection-helpers.ts +++ b/frontend/src/app/helpers/selection-helpers.ts @@ -7,9 +7,9 @@ export namespace SelectionHelpers { */ export function hasSelectionWithin(target:Element):boolean { try { - const selection = window.getSelection(); + const selection = window.getSelection()!; const hasSelection = selection.toString().length > 0; - const isWithin = target.contains(getSelection().anchorNode); + const isWithin = target.contains(selection.anchorNode); return hasSelection && isWithin; } catch (e) { diff --git a/frontend/src/app/modules/a11y/accessible-by-keyboard.component.spec.ts b/frontend/src/app/modules/a11y/accessible-by-keyboard.component.spec.ts index 63c134a23a..172e4507e3 100644 --- a/frontend/src/app/modules/a11y/accessible-by-keyboard.component.spec.ts +++ b/frontend/src/app/modules/a11y/accessible-by-keyboard.component.spec.ts @@ -28,8 +28,7 @@ import {AccessibleByKeyboardComponent} from "core-app/modules/a11y/accessible-by-keyboard.component"; -import {TestBed} from '@angular/core/testing'; -import {ComponentFixture} from '@angular/core/testing/src/component_fixture'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; describe('accessibleByKeyboard component', () => { diff --git a/frontend/src/app/modules/a11y/accessible-click.directive.spec.ts b/frontend/src/app/modules/a11y/accessible-click.directive.spec.ts index 528e384161..d27cf139ae 100644 --- a/frontend/src/app/modules/a11y/accessible-click.directive.spec.ts +++ b/frontend/src/app/modules/a11y/accessible-click.directive.spec.ts @@ -28,8 +28,7 @@ import {Component, DebugElement} from "@angular/core"; -import {fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {ComponentFixture} from '@angular/core/testing/src/component_fixture'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {By} from "@angular/platform-browser"; import {AccessibleClickDirective} from "core-app/modules/a11y/accessible-click.directive"; diff --git a/frontend/src/app/modules/a11y/double-click-or-tap.directive.ts b/frontend/src/app/modules/a11y/double-click-or-tap.directive.ts index 59b5d3b618..aee7252e9a 100644 --- a/frontend/src/app/modules/a11y/double-click-or-tap.directive.ts +++ b/frontend/src/app/modules/a11y/double-click-or-tap.directive.ts @@ -27,8 +27,6 @@ //++ import {Directive, EventEmitter, HostListener, Input, Output} from '@angular/core'; -import {keyCodes} from 'core-app/modules/common/keyCodes.enum'; -import {HammerInstance} from "@angular/platform-browser/src/dom/events/hammer_gestures"; @Directive({ selector: '[doubleClickOrTap]', diff --git a/frontend/src/app/modules/attachments/attachments-upload/attachments-upload.component.ts b/frontend/src/app/modules/attachments/attachments-upload/attachments-upload.component.ts index 45f73548f7..4480fe2c92 100644 --- a/frontend/src/app/modules/attachments/attachments-upload/attachments-upload.component.ts +++ b/frontend/src/app/modules/attachments/attachments-upload/attachments-upload.component.ts @@ -42,7 +42,7 @@ import {NotificationsService} from "core-app/modules/common/notifications/notifi export class AttachmentsUploadComponent implements OnInit { @Input() public resource:HalResource; - @ViewChild('hiddenFileInput') public filePicker:ElementRef; + @ViewChild('hiddenFileInput', { static: false }) public filePicker:ElementRef; public draggingOver:boolean = false; public text:any; diff --git a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts index 21c2ef5626..d3640458d9 100644 --- a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts @@ -4,6 +4,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {Component} from "@angular/compiler/src/core"; import {ComponentType} from "@angular/cdk/portal"; import {OpContextMenuItem} from "core-components/op-context-menu/op-context-menu.types"; +import {DisabledButtonPlaceholder} from "core-app/modules/boards/board/board-list/board-list.component"; export interface BoardActionService { @@ -58,6 +59,12 @@ export interface BoardActionService { */ dragIntoAllowed(query:QueryResource, value:HalResource|undefined):boolean; + /** + * Determine whether we can create new items for this action attribute + */ + canAddToQuery(query:QueryResource):Promise; + + /** * Get the specific component for the autocompleter (e.g versionAutocompleter) * @returns {Component} @@ -69,4 +76,10 @@ export interface BoardActionService { * @returns {Component} */ headerComponent():ComponentType|undefined; + + /** + * Get icon and text to show on the add button when it is disabled + * @returns {the icon class or nothing} + */ + disabledAddButtonPlaceholder(resource?:HalResource):DisabledButtonPlaceholder|undefined; } diff --git a/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts index fc4e427f8c..7ee05bce23 100644 --- a/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts @@ -56,6 +56,10 @@ export class BoardStatusActionService implements BoardActionService { } } + public canAddToQuery(query:QueryResource):Promise { + return Promise.resolve(true); + } + public addActionQueries(board:Board):Promise { return this.getStatuses() .then((results) => @@ -120,6 +124,10 @@ export class BoardStatusActionService implements BoardActionService { return undefined; } + public disabledAddButtonPlaceholder(status:StatusResource) { + return undefined; + } + private getStatuses():Promise { return this.statusDm .list() diff --git a/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts index 737cbc835b..ab35c424ed 100644 --- a/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts @@ -15,9 +15,11 @@ import {OpContextMenuItem} from "core-components/op-context-menu/op-context-menu import {LinkHandling} from "core-app/modules/common/link-handling/link-handling"; import {StateService} from "@uirouter/core"; import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; -import {StatusResource} from "core-app/modules/hal/resources/status-resource"; import {VersionCacheService} from "core-components/versions/version-cache.service"; import {VersionBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/version/version-board-header.component"; +import {FormResource} from "core-app/modules/hal/resources/form-resource"; +import {FormsCacheService} from "core-components/forms/forms-cache.service"; +import {CallableHalLink} from "core-app/modules/hal/hal-link/hal-link"; @Injectable() export class BoardVersionActionService implements BoardActionService { @@ -29,6 +31,7 @@ export class BoardVersionActionService implements BoardActionService { protected currentProject:CurrentProjectService, protected wpNotifications:WorkPackageNotificationService, protected state:StateService, + protected formCache:FormsCacheService, protected pathHelper:PathHelperService) { } @@ -67,12 +70,25 @@ export class BoardVersionActionService implements BoardActionService { } } + public canAddToQuery(query:QueryResource):Promise { + const formLink = _.get(query, 'results.createWorkPackage.href', null) ; + + if (!formLink) { + return Promise.resolve(false); + } + + return this.formCache + .require(formLink) + .then((form:FormResource) => form.schema.version.writable); + } + public addActionQueries(board:Board):Promise { return this.getVersions() .then((results) => { return Promise.all( results.map((version:VersionResource) => { - if (version.isOpen() && version.definingProject.name === this.currentProject.name) { + const definingName = _.get(version, 'definingProject.name', null); + if (version.isOpen() && definingName && definingName === this.currentProject.name) { return this.addActionQuery(board, version); } @@ -141,6 +157,16 @@ export class BoardVersionActionService implements BoardActionService { return VersionBoardHeaderComponent; } + public disabledAddButtonPlaceholder(version:VersionResource) { + if (version.isLocked()) { + return { icon: 'locked', text: this.I18n.t('js.boards.version.locked') }; + } else if (version.isClosed()) { + return { icon: 'not-supported', text: this.I18n.t('js.boards.version.closed') }; + } else { + return undefined; + } + } + public dragIntoAllowed(query:QueryResource, value:HalResource|undefined) { return value instanceof VersionResource && value.isOpen(); } @@ -170,7 +196,7 @@ export class BoardVersionActionService implements BoardActionService { return [ { // Lock version - hidden: !version.isOpen(), + hidden: !version.isOpen() || (version.isLocked() && !version.$links.update), linkText: this.I18n.t('js.boards.version.lock_version'), onClick: () => { this.patchVersionStatus(version, 'locked'); @@ -179,7 +205,7 @@ export class BoardVersionActionService implements BoardActionService { }, { // Unlock version - hidden: !version.isLocked(), + hidden: !version.isLocked() || (version.isOpen() && !version.$links.update), linkText: this.I18n.t('js.boards.version.unlock_version'), onClick: () => { this.patchVersionStatus(version, 'open'); @@ -188,7 +214,7 @@ export class BoardVersionActionService implements BoardActionService { }, { // Close version - hidden: version.isClosed(), + hidden: version.isClosed() || (!version.isClosed() && !version.$links.update), linkText: this.I18n.t('js.boards.version.close_version'), onClick: () => { this.patchVersionStatus(version, 'closed'); @@ -197,13 +223,26 @@ export class BoardVersionActionService implements BoardActionService { }, { // Open version - hidden: !version.isClosed(), + hidden: !version.isClosed() || (version.isClosed() && !version.$links.update), linkText: this.I18n.t('js.boards.version.open_version'), onClick: () => { this.patchVersionStatus(version, 'open'); return true; } }, + { + // Show link + linkText: this.I18n.t('js.boards.version.show_version'), + href: this.pathHelper.versionShowPath(id), + onClick: (evt:JQuery.Event) => { + if (!LinkHandling.isClickedWithModifier(evt)) { + window.open(this.pathHelper.versionShowPath(id), '_blank'); + return true; + } + + return false; + } + }, { // Edit link hidden: !version.$links.update, diff --git a/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.html b/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.html index 8b6daf113e..0e15f955fd 100644 --- a/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.html +++ b/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.html @@ -1,17 +1,8 @@
- - - - - -

+ {{version.endDate}}
diff --git a/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.sass b/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.sass index 9b3f3d1f3a..15cd68fac3 100644 --- a/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.sass +++ b/frontend/src/app/modules/boards/board/board-actions/version/version-board-header.sass @@ -1,3 +1,3 @@ .version-board-header display: flex - align-items: center + flex-direction: column diff --git a/frontend/src/app/modules/boards/board/board-list/board-list.component.html b/frontend/src/app/modules/boards/board/board-list/board-list.component.html index cafe85ace1..c516cf9330 100644 --- a/frontend/src/app/modules/boards/board/board-list/board-list.component.html +++ b/frontend/src/app/modules/boards/board/board-list/board-list.component.html @@ -32,21 +32,25 @@
- - + [ngClass]="{ '-with-create-button': board.isAction || showAddButton }"> +
+ + +
-
+<% resource = ::API::V3::MeetingContents::MeetingContentRepresenter.new(content, current_user: current_user, embed_links: true) %> +<%= list_attachments(resource) %> + <%= render :partial => 'shared/meeting_header' %> diff --git a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb new file mode 100644 index 0000000000..f821a90213 --- /dev/null +++ b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb @@ -0,0 +1,52 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + module Attachments + class AttachmentsByMeetingContentAPI < ::API::OpenProjectAPI + resources :attachments do + helpers API::V3::Attachments::AttachmentsByContainerAPI::Helpers + + helpers do + def container + meeting_content + end + + def get_attachment_self_path + api_v3_paths.attachments_by_meeting_content container.id + end + end + + get &API::V3::Attachments::AttachmentsByContainerAPI.read + post &API::V3::Attachments::AttachmentsByContainerAPI.create + end + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb b/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb new file mode 100644 index 0000000000..13a7ddbd64 --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb @@ -0,0 +1,38 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + module MeetingAgendas + class MeetingAgendaRepresenter < API::V3::MeetingContents::MeetingContentRepresenter + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb b/modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb new file mode 100644 index 0000000000..0f58dbdc87 --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb @@ -0,0 +1,58 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + module MeetingContents + class MeetingContentRepresenter < ::API::Decorators::Single + include API::Decorators::LinkedResource + include API::Caching::CachedRepresenter + include ::API::V3::Attachments::AttachableRepresenterMixin + + self_link title_getter: ->(*) { nil } + + property :id + + associated_resource :project, + link: ->(*) do + next unless represented.project.present? + { + href: api_v3_paths.project(represented.project.id), + title: represented.project.name + } + end + + def _type + 'MeetingContent' + end + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb b/modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb new file mode 100644 index 0000000000..86fd430260 --- /dev/null +++ b/modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb @@ -0,0 +1,38 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + module MeetingMinutes + class MeetingMinutesRepresenter < API::V3::MeetingContents::MeetingContentRepresenter + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb b/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb new file mode 100644 index 0000000000..6476528aea --- /dev/null +++ b/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb @@ -0,0 +1,53 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + module Meetings + class MeetingContentsAPI < ::API::OpenProjectAPI + resources :meeting_contents do + helpers do + def meeting_content + MeetingContent.find params[:id] + end + end + + route_param :id do + get do + ::API::V3::MeetingContents::MeetingContentRepresenter.new( + meeting_content, current_user: current_user, embed_links: true + ) + end + + mount ::API::V3::Attachments::AttachmentsByMeetingContentAPI + end + end + end + end + end +end diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 1a108c06c1..6890139a24 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -69,6 +69,10 @@ module OpenProject::Meeting patch_with_namespace :OpenProject, :TextFormatting, :Formats, :Markdown, :TextileConverter + add_api_endpoint 'API::V3::Root' do + mount ::API::V3::Meetings::MeetingContentsAPI + end + initializer 'meeting.precompile_assets' do Rails.application.config.assets.precompile += %w(meeting/meeting.css meeting/meeting.js) end @@ -91,5 +95,29 @@ module OpenProject::Meeting PermittedParams.permit(:search, :meetings) end + + add_api_path :meeting_content do |id| + "#{root}/meeting_contents/#{id}" + end + + add_api_path :meeting_agenda do |id| + meeting_content(id) + end + + add_api_path :meeting_minutes do |id| + meeting_content(id) + end + + add_api_path :attachments_by_meeting_content do |id| + "#{meeting_content(id)}/attachments" + end + + add_api_path :attachments_by_meeting_agenda do |id| + attachments_by_meeting_content id + end + + add_api_path :attachments_by_meeting_minutes do |id| + attachments_by_meeting_content id + end end end diff --git a/modules/meeting/spec/features/meetings_attachments_spec.rb b/modules/meeting/spec/features/meetings_attachments_spec.rb new file mode 100644 index 0000000000..7eb7a5b68b --- /dev/null +++ b/modules/meeting/spec/features/meetings_attachments_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'features/page_objects/notification' + +describe 'Add an attachment to a meeting (agenda)', js: true do + let(:role) do + FactoryBot.create :role, permissions: %i[view_meetings edit_meetings create_meeting_agendas] + end + + let(:dev) do + FactoryBot.create :user, member_in_project: project, member_through_role: role + end + + let(:project) { FactoryBot.create(:project) } + + let(:meeting) do + FactoryBot.create( + :meeting, + project: project, + title: "Versammlung", + agenda: FactoryBot.create(:meeting_agenda, text: "Versammlung") + ) + end + + let(:attachments) { ::Components::Attachments.new } + let(:image_fixture) { Rails.root.join('spec/fixtures/files/image.png') } + let(:editor) { Components::WysiwygEditor.new } + + before do + login_as(dev) + + visit "/meetings/#{meeting.id}" + + within "#tab-content-agenda .toolbar" do + click_button "Edit" + end + end + + describe 'wysiwyg editor' do + context 'on an existing page' do + it 'can upload an image via drag & drop' do + target = find('.ck-content') + + editor.expect_button 'Insert image' + + editor.drag_attachment image_fixture, 'Some image caption' + + click_on "Save" + + content = find("div.meeting_content.meeting_agenda") + + expect(content).to have_selector('img') + expect(content).to have_content('Some image caption') + end + end + end + + describe 'attachment dropzone' do + it 'can upload an image via attaching and drag & drop' do + # called the same for all Wysiwyg dditors no matter if for work packages + # or not + container = page.find('.wp-attachment-upload') + scroll_to_element(container) + + ## + # Attach file manually + expect(page).to have_no_selector('.work-package--attachments--filename') + attachments.attach_file_on_input(image_fixture) + expect(page).not_to have_selector('notification-upload-progress') + expect(page).to have_selector('.work-package--attachments--filename', text: 'image.png', wait: 5) + + ## + # and via drag & drop + attachments.drag_and_drop_file(container, Rails.root.join('spec/fixtures/files/image.png')) + expect(page).not_to have_selector('notification-upload-progress') + expect(page).to have_selector('.work-package--attachments--filename', text: 'image.png', count: 2, wait: 5) + end + end +end diff --git a/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb b/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb new file mode 100644 index 0000000000..7ae3b43fa8 --- /dev/null +++ b/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'requests/api/v3/attachments/attachment_resource_shared_examples' + +describe "meeting agenda attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :meeting_content } + + let(:create_permission) { :create_meetings } + let(:read_permission) { :view_meetings } + let(:update_permission) { :edit_meetings } + + let(:meeting_content) { FactoryBot.create :meeting_agenda, meeting: meeting } + let(:meeting) { FactoryBot.create :meeting, project: project } + end +end diff --git a/modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb b/modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb new file mode 100644 index 0000000000..64185a7af7 --- /dev/null +++ b/modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'requests/api/v3/attachments/attachment_resource_shared_examples' + +describe "meeting minutes attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :meeting_content } + + let(:create_permission) { :create_meetings } + let(:read_permission) { :view_meetings } + let(:update_permission) { :edit_meetings } + + let(:meeting_content) { FactoryBot.create :meeting_minutes, meeting: meeting } + let(:meeting) { FactoryBot.create :meeting, project: project } + end +end diff --git a/modules/my_project_page/lib/open_project/my_project_page/engine.rb b/modules/my_project_page/lib/open_project/my_project_page/engine.rb index e0e9cdb976..14348b0dc9 100644 --- a/modules/my_project_page/lib/open_project/my_project_page/engine.rb +++ b/modules/my_project_page/lib/open_project/my_project_page/engine.rb @@ -38,11 +38,11 @@ module OpenProject::MyProjectPage project_module :my_project_page do view_actions.each do |action| - Redmine::AccessControl.permission(:view_project).actions << "my_projects_overviews/#{action}" + OpenProject::AccessControl.permission(:view_project).actions << "my_projects_overviews/#{action}" end edit_actions.each do |action| - Redmine::AccessControl.permission(:edit_project).actions << "my_projects_overviews/#{action}" + OpenProject::AccessControl.permission(:edit_project).actions << "my_projects_overviews/#{action}" end end end diff --git a/modules/my_project_page/spec/lib/redmine/access_control_spec.rb b/modules/my_project_page/spec/lib/redmine/access_control_spec.rb index 1c2f9853dd..6794d434d8 100644 --- a/modules/my_project_page/spec/lib/redmine/access_control_spec.rb +++ b/modules/my_project_page/spec/lib/redmine/access_control_spec.rb @@ -20,9 +20,9 @@ require 'spec_helper' -describe Redmine::AccessControl do - let(:view_project_permission) { Redmine::AccessControl.permission(:view_project) } - let(:edit_project_permission) { Redmine::AccessControl.permission(:edit_project) } +describe OpenProject::AccessControl do + let(:view_project_permission) { OpenProject::AccessControl.permission(:view_project) } + let(:edit_project_permission) { OpenProject::AccessControl.permission(:edit_project) } describe '#view_project' do it { expect(view_project_permission.actions).to be_include("my_projects_overviews/index") } diff --git a/modules/reporting/app/assets/stylesheets/reporting/_reporting.sass b/modules/reporting/app/assets/stylesheets/reporting/_reporting.sass new file mode 100644 index 0000000000..0b5190e0a6 --- /dev/null +++ b/modules/reporting/app/assets/stylesheets/reporting/_reporting.sass @@ -0,0 +1,112 @@ +@import "fonts/openproject_icon_definitions" + +@mixin sort-icons + font-family: "openproject-icon-font" !important + font-weight: normal !important + speak: none + margin-left: 5px + font-size: 1.2em + line-height: 1 + vertical-align: text-bottom + +// -------------------------- Generics -------------------------- +.tablesorter-header + cursor: pointer + +.tablesorter-headerDesc .generic-table--sort-header span + &:after + @include sort-icons + @include icon-mixin-sort-descending + +.tablesorter-headerAsc .generic-table--sort-header span + &:after + @include sort-icons + @include icon-mixin-sort-ascending + +.cost_types + padding-bottom: 3px + +.cost_types a.active + color: #000 + font-weight: bold + +/* Details view*/ +.detail-report td + text-align: left + vertical-align: top + +#query_form fieldset.header_collapsible.collapsible + padding-bottom: 10px + + +// -------------------------- Save and Delete Reports -------------------------- +#save_as_form, #delete_form + z-index: 999 + +// -------------------------- Buttons -------------------------- +.form--buttons.-with-button-form + position: relative + +.form--buttons.-with-button-form .button + margin-bottom: 0 + +div.button_form + background-color: white + border: 1px solid gray + -moz-border-radius: 3px + border-radius: 3px + left: 100px + position: absolute + padding: 1.0rem + width: 400px + +// -------------------------- Filter -------------------------- +.filter + -webkit-border-radius: 5px + -moz-border-radius: 5px + border-radius: 5px + +.inactive-filter + background-color: #FCE29A !important + +.advanced-filters--filter-value + white-space: nowrap + +.filter_radio_option + padding-left: 5px + padding-right: 5px + +#add_filter_block + margin-top: 6px + +#add_filter_select + margin-bottom: 10px + +.advanced-filters--filter-value.-binary + display: flex + + +// -------------------------- Mobile -------------------------- +@media only screen and (max-width: 679px) + .group-by--control + margin-top: 10px + + #group-by--rows .group-by--caption, + #group-by--columns .group-by--caption + padding: 0 7px + + .group-by--selected-element + display: inline-block + padding-left: 7px + + .group-by--selected-element:before, + .group-by--selected-element:after + border-width: 18px 0px 18px 14px + margin-top: -18px + + .group-by--selected-element:first-of-type + margin: 0 + + .group-by--selected-element:first-of-type:before + border: none + left: 0 diff --git a/modules/reporting/app/assets/stylesheets/reporting/_reporting_group_by.sass b/modules/reporting/app/assets/stylesheets/reporting/_reporting_group_by.sass new file mode 100644 index 0000000000..2215ad7231 --- /dev/null +++ b/modules/reporting/app/assets/stylesheets/reporting/_reporting_group_by.sass @@ -0,0 +1,121 @@ + +// -------------------------- Group by -------------------------- + +#group-by--area + margin: 5px 0 10px 0 + +#group-by--area fieldset + border: none + padding: 0px + margin-bottom: 1em + +.in_row + display: inline-block + list-style: none + border-width: 0px + +.group-by--selected-element + cursor: move + position: relative + background-color: #767676 + padding-left: 14px + margin-left: 18px + min-width: 145px + +fieldset.collapsible.header_collapsible legend.in_row + width: inherit + background-image: inherit + +.group-by--container + overflow: hidden + +.group-by--label + margin: 0px + padding: 0px 18px 0 0 + min-width: 60px + text-align: center + white-space: nowrap + font-weight: bold + color: #fff + height: 36px + line-height: 36px + cursor: move + +.group-by--selected-element:after, .group-by--selected-element:before + border: solid transparent + content: " " + height: 0 + width: 0 + position: absolute + pointer-events: none + top: 50% + border-width: 30px 0px 30px 14px + margin-top: -30px + +.group-by--selected-element:after + border-color: rgba(118, 118, 118, 0) + border-left-color: rgba(118, 118, 118, 1) + left: 100% + +.group-by--selected-element:before + border-color: rgba(118, 118, 118, 1) + border-left-color: rgba(118, 118, 118, 0) + left: -14px + +.group-by--selected-element:hover:after + border-color: rgba(52, 147, 179, 0) + border-left-color: rgba(52, 147, 179, 1) + +.group-by--selected-element:hover:before + border-color: rgba(52, 147, 179, 1) + border-left-color: rgba(52, 147, 179, 0) + +.group-by--selected-element:hover + background-color: #3493B3 + +.group-by--remove + line-height: normal + cursor: pointer + color: #FFFFFF + +.group-by--remove:hover + background-color: #3493B3 !important + text-decoration: none + color: #FFFFFF + +.group-by--control + margin: 0 + padding: 0 + +.group-by--selected-elements + background-color: #EEE + overflow: hidden + +.group-by--caption + position: relative + color: #FFFFFF + background-color: #4B4B4B + font-weight: bold + padding: 0 7px + margin: 0 + height: inherit + line-height: 36px + min-width: 55px + overflow: visible + +.group-by--caption:after + left: 100% + top: 50% + border: solid transparent + content: " " + height: 0 + width: 0 + position: absolute + pointer-events: none + +.group-by--caption:after + border-color: rgba(75, 75, 75, 0) + border-left-color: #4B4B4B + border-width: 30px 0px 30px 14px + margin-top: -30px + z-index: 10 diff --git a/modules/reporting/app/assets/stylesheets/reporting/_reporting_table.sass b/modules/reporting/app/assets/stylesheets/reporting/_reporting_table.sass new file mode 100644 index 0000000000..008f05f986 --- /dev/null +++ b/modules/reporting/app/assets/stylesheets/reporting/_reporting_table.sass @@ -0,0 +1,112 @@ +#result-table + margin: 10px 0 + overflow-x: auto + + // -------------------------- Normal (generic) table -------------------------- + .generic-table--results-container tfoot + td + width: 150px + .generic-table--footer-outer + width: inherit + + // -------------------------- Special table -------------------------- + .report + text-align: center + border-collapse: collapse + border: solid 1px #ccc !important + width: auto !important + + td, th + min-width: 90px + white-space: nowrap + + td + border: dotted 1px #ddd + color: #666 + text-align: center + padding: 0 8px 0 8px + line-height: 2rem + vertical-align: middle + + tbody th, tbody td, .inner + max-width: 300px + white-space: normal !important + + td:hover + color: #000 + outline: #ccc 1px solid + outline-offset: 1px + + td.empty:hover + outline: none + + th + border: solid 1px #ccc + background-color: #e3e3e3 + text-align: center + font-size: 0.875rem + line-height: 34px + padding: 0 8px 0 8px + + .odd th.inner + background-color: #e8e8e8 + + .even th.inner + background-color: #e3e3e3 + + th.inner + border: solid 1px #ccc + background-color: #efefef + text-align: right + + .odd td.inner + background-color: #e8e8e8 + + .even td.inner + background-color: #e3e3e3 + + tr:hover .inner, + tr:hover .bottom, + tr:hover .empty, + tr:hover .right + background-color: #f5f5c5 !important + + .top + border-top-style: solid + border-top-color: #ccc + + .bottom + border-bottom-style: solid + border-bottom-color: #ccc + + td.penultimate + border-right-style: solid + + thead .inner, tfoot .inner + text-align: right + padding-right: 5px + + .result + font-size: 120% + text-align: right + + .thead tr:hover .inner, tfoot tr:hover .inner + background-color: #efefef + + .left + text-align: left !important + padding-left: 5px + + .right + text-align: right !important + padding-right: 5px + +td .drill_down, th .drill_down + font-size: 8px + display: block + float: right + font-weight: bold + visibility: hidden + +td:hover .drill_down, th:hover .drill_down + visibility: visible diff --git a/modules/reporting/app/assets/stylesheets/reporting/_reporting_table_print.sass b/modules/reporting/app/assets/stylesheets/reporting/_reporting_table_print.sass new file mode 100644 index 0000000000..1cc8938e86 --- /dev/null +++ b/modules/reporting/app/assets/stylesheets/reporting/_reporting_table_print.sass @@ -0,0 +1,7 @@ +@media print + #query_form + .form--fieldset:not(#filters) + display: none + + #main + overflow: visible diff --git a/modules/reporting/app/assets/stylesheets/reporting/reporting.sass b/modules/reporting/app/assets/stylesheets/reporting/reporting.sass deleted file mode 100644 index 1e5e76ee0b..0000000000 --- a/modules/reporting/app/assets/stylesheets/reporting/reporting.sass +++ /dev/null @@ -1,29 +0,0 @@ -@import "fonts/openproject_icon_definitions" - -@mixin sort-icons - font-family: "openproject-icon-font" !important - font-weight: normal !important - speak: none - margin-left: 5px - font-size: 1.2em - line-height: 1 - vertical-align: text-bottom - -.tablesorter-header - cursor: pointer - -.tablesorter-headerDesc .generic-table--sort-header span - &:after - @include sort-icons - @include icon-mixin-sort-descending - -.tablesorter-headerAsc .generic-table--sort-header span - &:after - @include sort-icons - @include icon-mixin-sort-ascending - -#result-table .generic-table--results-container tfoot - td - width: 150px - .generic-table--footer-outer - width: inherit \ No newline at end of file diff --git a/modules/reporting/app/assets/stylesheets/reporting/reporting_styles.sass b/modules/reporting/app/assets/stylesheets/reporting/reporting_styles.sass new file mode 100644 index 0000000000..cd66bdd31c --- /dev/null +++ b/modules/reporting/app/assets/stylesheets/reporting/reporting_styles.sass @@ -0,0 +1,4 @@ +@import _reporting +@import _reporting_group_by +@import _reporting_table +@import _reporting_table_print diff --git a/modules/reporting/app/models/cost_query/custom_field_mixin.rb b/modules/reporting/app/models/cost_query/custom_field_mixin.rb index ab12761199..32103b8de1 100644 --- a/modules/reporting/app/models/cost_query/custom_field_mixin.rb +++ b/modules/reporting/app/models/cost_query/custom_field_mixin.rb @@ -22,13 +22,14 @@ module CostQuery::CustomFieldMixin attr_reader :custom_field SQL_TYPES = { - 'string' => mysql? ? 'char' : 'varchar', - 'list' => mysql? ? 'char' : 'varchar', - 'text' => mysql? ? 'char' : 'text', - 'bool' => mysql? ? 'unsigned' : 'boolean', - 'date' => 'date', - 'int' => 'decimal(60,3)', - 'float' => 'decimal(60,3)' } + 'string' => 'varchar', + 'list' => 'varchar', + 'text' => 'text', + 'bool' => 'boolean', + 'date' => 'date', + 'int' => 'decimal(60,3)', + 'float' => 'decimal(60,3)' + }.freeze def self.extended(base) base.inherited_attribute :factory @@ -101,7 +102,7 @@ module CostQuery::CustomFieldMixin # contained invalid values. def all_values_int?(field) field.custom_values.pluck(:value).all? { |val| val.to_i > 0 } - rescue + rescue StandardError false end diff --git a/modules/reporting/app/views/cost_reports/_reporting_header.html.erb b/modules/reporting/app/views/cost_reports/_reporting_header.html.erb index 122af3f001..8a9b2f7c41 100644 --- a/modules/reporting/app/views/cost_reports/_reporting_header.html.erb +++ b/modules/reporting/app/views/cost_reports/_reporting_header.html.erb @@ -21,6 +21,5 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. <% content_for :header_tags do %> <%= javascript_include_tag "reporting_engine/reporting_engine" %> <%= javascript_include_tag "reporting/reporting" %> - <%= stylesheet_link_tag 'reporting_engine/reporting_engine' %> - <%= stylesheet_link_tag "reporting/reporting" %> + <%= stylesheet_link_tag "reporting/reporting_styles", media: 'all' %> <% end %> diff --git a/modules/reporting/lib/open_project/reporting/engine.rb b/modules/reporting/lib/open_project/reporting/engine.rb index 8a4f274466..b427757c53 100644 --- a/modules/reporting/lib/open_project/reporting/engine.rb +++ b/modules/reporting/lib/open_project/reporting/engine.rb @@ -24,7 +24,7 @@ module OpenProject::Reporting include OpenProject::Plugins::ActsAsOpEngine register 'openproject-reporting', - author_url: 'http://finn.de', + author_url: 'https://www.openproject.org', bundled: true do view_actions = [:index, :show, :drill_down, :available_values, :display_report_list] @@ -38,17 +38,17 @@ module OpenProject::Reporting #register additional permissions for viewing time and cost entries through the CostReportsController view_actions.each do |action| - Redmine::AccessControl.permission(:view_time_entries).actions << "cost_reports/#{action}" - Redmine::AccessControl.permission(:view_own_time_entries).actions << "cost_reports/#{action}" - Redmine::AccessControl.permission(:view_cost_entries).actions << "cost_reports/#{action}" - Redmine::AccessControl.permission(:view_own_cost_entries).actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_time_entries).actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_own_time_entries).actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_cost_entries).actions << "cost_reports/#{action}" + OpenProject::AccessControl.permission(:view_own_cost_entries).actions << "cost_reports/#{action}" end # register additional permissions for the work package costlog controller - Redmine::AccessControl.permission(:view_time_entries).actions << "work_package_costlog/index" - Redmine::AccessControl.permission(:view_own_time_entries).actions << "work_package_costlog/index" - Redmine::AccessControl.permission(:view_cost_entries).actions << "work_package_costlog/index" - Redmine::AccessControl.permission(:view_own_cost_entries).actions << "work_package_costlog/index" + OpenProject::AccessControl.permission(:view_time_entries).actions << "work_package_costlog/index" + OpenProject::AccessControl.permission(:view_own_time_entries).actions << "work_package_costlog/index" + OpenProject::AccessControl.permission(:view_cost_entries).actions << "work_package_costlog/index" + OpenProject::AccessControl.permission(:view_own_cost_entries).actions << "work_package_costlog/index" #menu extensions menu :top_menu, :cost_reports_global, { controller: '/cost_reports', action: 'index', project_id: nil }, @@ -87,7 +87,6 @@ module OpenProject::Reporting initializer 'reporting.precompile_assets' do Rails.application.config.assets.precompile += %w( - reporting_engine/reporting_engine.css reporting_engine/reporting_engine.js ) @@ -104,7 +103,7 @@ module OpenProject::Reporting require_dependency 'cost_query/group_by' end - assets %w(reporting/reporting.css + assets %w(reporting/reporting_styles.css reporting/reporting.js) patches %i[TimelogController CustomFieldsController OpenProject::Configuration] diff --git a/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb b/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb deleted file mode 100644 index 5f1bbf52a4..0000000000 --- a/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb +++ /dev/null @@ -1,433 +0,0 @@ -/*-- copyright -ReportingEngine - -Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF) - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -version 3. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -++*/ - -.cost_types { - padding-bottom: 3px; -} - -.cost_types a.active { - color: #000; - font-weight: bold; -} - -.report { - text-align: center; - border-collapse: collapse; - border: solid 1px #ccc !important; - width: auto !important; -} - -.report td, .report th { - min-width: 90px; - white-space: nowrap; -} - -.report td { - border: dotted 1px #ddd; - color: #666; - text-align: center; - padding: 0 8px 0 8px; - line-height: 2rem; - vertical-align: middle; - -} - -.report tbody th, .report tbody td, .inner { - max-width: 300px; - white-space: normal !important; -} - -.report td:hover { - color: #000; - outline: #ccc 1px solid; - outline-offset: 1px; -} - -.report td.empty:hover { - outline: none; -} - -.report th { - border: solid 1px #ccc; - background-color: #e3e3e3; - text-align: center; - font-size: 0.875rem; - line-height: 34px; - padding: 0 8px 0 8px; - -} - -.report .odd th.inner { - background-color: #e8e8e8; -} - -.report .even th.inner { - background-color: #e3e3e3; -} - -.report th.inner { - border: solid 1px #ccc; - background-color: #efefef; - text-align: right; -} - -.report .odd td.inner { - background-color: #e8e8e8; -} - -.report .even td.inner { - background-color: #e3e3e3; -} - -.report tr.even:hover .inner, -.report tr.even:hover .bottom, -.report tr.even:hover .empty, -.report tr.even:hover .right { - background-color: #f5f5c5 !important; -} -/* IE7 made me do it! */ -.report tr.odd:hover .inner, -.report tr.odd:hover .bottom, -.report tr.odd:hover .empty, -.report tr.odd:hover .right { - background-color: #f5f5c5 !important; -} - -.report .top { - border-top-style: solid; - border-top-color: #ccc; - /* border-top: 2px solid #ccc !important; */ -} - -.report .bottom { - border-bottom-style: solid; - border-bottom-color: #ccc; - /* border-bottom: 2px solid #ccc !important; */ -} - -.report td.penultimate { - border-right-style: solid; -} - -.report thead .inner, .report tfoot .inner { - text-align: right; - padding-right: 5px; -} - -.report .result { - font-size: 120%; - text-align: right; -} - -#result-table { - margin: 10px 0; - overflow-x: auto; -} - -.report thead tr:hover .inner, .report tfoot tr:hover .inner { - background-color: #efefef; -} - -.report .left { - text-align: left !important; - padding-left: 5px; -} - -.report .right { - text-align: right !important; - padding-right: 5px; -} - -/* Details view*/ - -.detail-report td { - text-align: left; - vertical-align: top; -} - -#query_form fieldset.header_collapsible.collapsible { - padding-bottom: 10px; -} - -/* Overwriting styling for headlines within the query. */ -/* TODO: Font-size seems to be a bit odd. Needs some love. */ -.new_report fieldset h3 { - font-size: 1.17em; - border: none; -} - -.filter { - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} - -.inactive-filter { - background-color: #FCE29A !important; -} - -.advanced-filters--filter-value { - white-space: nowrap; -} - -.filter_radio_option { - padding-left: 5px; - padding-right: 5px; -} - -#add_filter_block { - margin-top: 6px; -} - -#add_filter_select { - margin-bottom: 10px; -} - -/* ----- group by --- */ -#group-by--area { - margin: 5px 0 10px 0; -} - -#group-by--area fieldset { - border: none; - padding: 0px; - margin-bottom: 1em; -} - -.in_row { - display: inline-block; - list-style: none; - border-width: 0px; -} - -.group-by--selected-element { - cursor: move; - position: relative; - background-color: #767676; - padding-left: 14px; - margin-left: 18px; - min-width: 145px; -} - -fieldset.collapsible.header_collapsible legend.in_row { - width: inherit; - background-image: inherit; -} - -.group-by--container { - overflow: hidden; -} - -.group-by--label { - margin: 0px; - padding: 0px 18px 0 0; - min-width: 60px; - text-align: center; - white-space: nowrap; - font-weight: bold; - color: #fff; - height: 36px; - line-height: 36px; - cursor: move; -} - -.group-by--selected-element:after, .group-by--selected-element:before { - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - top: 50%; - border-width: 30px 0px 30px 14px; - margin-top: -30px; -} - -.group-by--selected-element:after { - border-color: rgba(118, 118, 118, 0); - border-left-color: rgba(118, 118, 118, 1); - left: 100%; -} - -.group-by--selected-element:before { - border-color: rgba(118, 118, 118, 1); - border-left-color: rgba(118, 118, 118, 0); - left: -14px; -} - -.group-by--selected-element:hover:after { - border-color: rgba(52, 147, 179, 0); - border-left-color: rgba(52, 147, 179, 1); -} -.group-by--selected-element:hover:before { - border-color: rgba(52, 147, 179, 1); - border-left-color: rgba(52, 147, 179, 0); -} - -.group-by--selected-element:hover { - background-color: #3493B3; -} - -.group-by--remove { - line-height: normal; - cursor: pointer; - color: #FFFFFF; -} - -.group-by--remove:hover { - background-color: #3493B3 !important; - text-decoration: none; - color: #FFFFFF; -} - -.group-by--control { - margin: 0; - padding: 0; -} - -.group-by--selected-elements { - background-color: #EEE; - overflow: hidden; -} - -.group-by--caption { - position: relative; - color: #FFFFFF; - background-color: #4B4B4B; - font-weight: bold; - padding: 0 7px; - margin: 0; - height: inherit; - line-height: 36px; - min-width: 55px; - overflow: visible; -} - -.group-by--caption:after { - left: 100%; - top: 50%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} - -.group-by--caption:after { - border-color: rgba(75, 75, 75, 0); - border-left-color: #4B4B4B; - border-width: 30px 0px 30px 14px; - margin-top: -30px; - z-index: 10; -} - -/* -- end group-by -- */ - -td .drill_down, th .drill_down { - font-size: 8px; - display: block; - float: right; - font-weight: bold; - visibility: hidden; -} - -td:hover .drill_down, th:hover .drill_down { - visibility: visible; -} - -/*Buttons*/ - -.form--buttons.-with-button-form { - position: relative; -} - -.form--buttons.-with-button-form .button { - margin-bottom: 0; -} - -div.button_form { - /* TODO IE Compatibility! */ - background-color: white; - border: 1px solid gray; - -moz-border-radius: 3px; - border-radius: 3px; - left: 100px; - position: absolute; - padding: 1.0rem; - width: 400px; -} - -/***** Save and Delete Reports ****/ -#save_as_form, #delete_form { - z-index: 999; -} - -/* Calendar Fixes */ -div.calendar /* let calendar expand properly */ -{ - font-size: medium; - line-height: normal; -} - -div.calendar table div { /* make sure the nested divs are large enough, too */ - font-size: medium; - line-height: normal; -} - -.calendar .combo .label, .calendar .combo .label-IEfix { /* set the proper size for the combo boxes with months and years */ - font-size: 10px; - line-height: normal; -} - -.calendar tbody .day { /* avoid jitter during quick mouse-overs over days */ - border: 1px dotted transparent; - padding: 1px 3px 1px 1px; -} - -.advanced-filters--filter-value.-binary { - display: flex; -} - -@media only screen and (max-width: 679px) { - .group-by--control { - margin-top: 10px; - } - #group-by--rows .group-by--caption, - #group-by--columns .group-by--caption { - padding: 0 7px; - } - .group-by--selected-element { - display: inline-block; - padding-left: 7px; - } - .group-by--selected-element:before, - .group-by--selected-element:after { - border-width: 18px 0px 18px 14px; - margin-top: -18px; - } - .group-by--selected-element:first-of-type { - margin: 0; - } - .group-by--selected-element:first-of-type:before { - border: none; - left: 0; - } -} diff --git a/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css b/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css deleted file mode 100644 index f07c23b302..0000000000 --- a/modules/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css +++ /dev/null @@ -1,23 +0,0 @@ -/*-- copyright -ReportingEngine - -Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF) - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -version 3. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -++*/ - -/* * - *= require reporting_engine/reporting - */ diff --git a/modules/reporting_engine/lib/report/query_utils.rb b/modules/reporting_engine/lib/report/query_utils.rb index 3d86243b9d..578ce41bd2 100644 --- a/modules/reporting_engine/lib/report/query_utils.rb +++ b/modules/reporting_engine/lib/report/query_utils.rb @@ -189,11 +189,6 @@ module Report::QueryUtils }.join(', ')}\n\t\tELSE #{field_name_for else_part}\n\tEND" end - def iso_year_week(field, default_table = nil) - field = field_name_for(field, default_table) - "-- code specific for #{adapter_name}\n\t" << super(field) - end - ## # Converts value with a given behavior, but treats nil differently. # Params @@ -248,85 +243,19 @@ module Report::QueryUtils second.size > first.size ? -1 : 0 end - def mysql? - [:mysql, :mysql2].include? adapter_name.to_s.downcase.to_sym - end - - def sqlite? - adapter_name == :sqlite - end - - def postgresql? - adapter_name == :postgresql - end - - module SQL - def typed(_type, value, escape = true) - escape ? "'#{quote_string value}'" : value - end + def typed(type, value, escape = true) + safe_value = escape ? "'#{quote_string value}'" : value + "#{safe_value}::#{type}" end - module MySql - include SQL - def iso_year_week(field) - "yearweek(#{field}, 1)" - end - end - - module Sqlite - include SQL - def iso_year_week(field) - # enjoy - <<-EOS - case - when strftime('%W', strftime('%Y-01-04', #{field})) = '00' then - -- 01/01 is in week 1 of the current year => %W == week - 1 - case - when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-04') = '00' then - -- we are at the end of the year, and it's the first week of the next year - (strftime('%Y', #{field}) + 1) || '01' - when strftime('%W', #{field}) < '08' then - -- we are in week 1 to 9 - strftime('%Y0', #{field}) || (strftime('%W', #{field}) + 1) - else - -- we are in week 10 or later - strftime('%Y', #{field}) || (strftime('%W', #{field}) + 1) - end - else - -- 01/01 is in week 53 of the last year - case - when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-01') = '00' then - -- we are at the end of the year, and it's the first week of the next year - (strftime('%Y', #{field}) + 1) || '01' - when strftime('%W', #{field}) = '00' then - -- we are in the week belonging to last year - (strftime('%Y', #{field}) - 1) || '53' - else - -- everything is fine - strftime('%Y%W', #{field}) - end - end - EOS - end - end - - module Postres - include SQL - def typed(type, value, escape = true) - "#{super}::#{type}" - end + def iso_year_week(field, default_table = nil) + field_name = field_name_for(field, default_table) - def iso_year_week(field) - "(EXTRACT(isoyear from #{field})*100 + \n\t\t" \ - "EXTRACT(week from #{field} - \n\t\t" \ - "(EXTRACT(dow FROM #{field})::int+6)%7))" - end + "(EXTRACT(isoyear from #{field_name})*100 + \n\t\t" \ + "EXTRACT(week from #{field_name} - \n\t\t" \ + "(EXTRACT(dow FROM #{field_name})::int+6)%7))" end - include MySql if mysql? - include Sqlite if sqlite? - include Postres if postgresql? - def self.cache @cache ||= Hash.new { |h, k| h[k] = {} } end diff --git a/modules/reporting_engine/lib/report/sql_statement.rb b/modules/reporting_engine/lib/report/sql_statement.rb index f85d6aef2e..550f520530 100644 --- a/modules/reporting_engine/lib/report/sql_statement.rb +++ b/modules/reporting_engine/lib/report/sql_statement.rb @@ -100,7 +100,6 @@ class Report::SqlStatement "\nWHERE #{where.join ' AND '}\n" sql << "GROUP BY #{group_by.join ', '}\nORDER BY #{group_by.join ', '}\n" if group_by? sql << "-- END #{desc}\n" - sql.gsub!('--', '#') if mysql? sql # << " LIMIT 100" end end @@ -115,6 +114,7 @@ class Report::SqlStatement # @param [#to_s] From part def from(table = nil) return @from unless table + @sql = nil @from = table end diff --git a/modules/reporting_engine/lib/reporting_engine/engine.rb b/modules/reporting_engine/lib/reporting_engine/engine.rb index 00482e0e7d..2b8c74b955 100644 --- a/modules/reporting_engine/lib/reporting_engine/engine.rb +++ b/modules/reporting_engine/lib/reporting_engine/engine.rb @@ -26,29 +26,7 @@ module ReportingEngine config.autoload_paths += Dir["#{config.root}/lib/"] initializer 'reportingengine.precompile_assets' do - Rails.application.config.assets.precompile += %w(reporting_engine.css reporting_engine.js) - end - - initializer 'check mysql version' do - connection = ActiveRecord::Base.connection - adapter_name = connection.adapter_name.to_s.downcase.to_sym - if [:mysql, :mysql2].include?(adapter_name) - # The reporting engine is incompatible with the - # following mysql versions due to a bug in MySQL itself: - # 5.6.0 - 5.6.12 - # 5.7.0 - 5.7.1 - # see https://www.openproject.org/issues/967 for details. - required_patch_levels = { '5.6' => 13, '5.7' => 2 } - - mysql_version = connection.show_variable('VERSION') - release_version, patch_level = mysql_version.match(/(\d*\.\d*)\.(\d*)/).captures - required_patch_level = required_patch_levels[release_version] - - if required_patch_level && (patch_level.to_i < required_patch_level) - raise "MySQL #{mysql_version} is not supported. Version #{release_version} \ - requires patch level >= #{required_patch_level}." - end - end + Rails.application.config.assets.precompile += %w(reporting_engine.js) end config.to_prepare do diff --git a/modules/reporting_engine/lib/widget/filters.rb b/modules/reporting_engine/lib/widget/filters.rb index 94daec1615..f2098ba5a4 100644 --- a/modules/reporting_engine/lib/widget/filters.rb +++ b/modules/reporting_engine/lib/widget/filters.rb @@ -20,9 +20,9 @@ require_dependency 'widget/base' class Widget::Filters < ::Widget::Base def render - spacer = content_tag :li, '', class: 'advanced-filters--spacer' + spacer = content_tag :li, '', class: 'advanced-filters--spacer hide-when-print' - add_filter = content_tag :li, id: 'add_filter_block', class: 'advanced-filters--add-filter' do + add_filter = content_tag :li, id: 'add_filter_block', class: 'advanced-filters--add-filter hide-when-print' do add_filter_label = label_tag 'add_filter_select', l(:label_filter_add), class: 'advanced-filters--add-filter-label' add_filter_label += label_tag 'add_filter_select', I18n.t('js.filter.description.text_open_filter') + ' ' + diff --git a/modules/reporting_engine/lib/widget/settings.rb b/modules/reporting_engine/lib/widget/settings.rb index f2775a9d61..13d5dfa73a 100644 --- a/modules/reporting_engine/lib/widget/settings.rb +++ b/modules/reporting_engine/lib/widget/settings.rb @@ -37,7 +37,7 @@ class Widget::Settings < Widget::Base end def render_controls_settings - content_tag :div, class: 'form--buttons -with-button-form' do + content_tag :div, class: 'form--buttons -with-button-form hide-when-print' do widgets = ''.html_safe render_widget(Widget::Controls::Apply, @subject, to: widgets) render_widget(Widget::Controls::Save, @subject, to: widgets, diff --git a/modules/xls_export/spec/lib/work_package_xls_export_spec.rb b/modules/xls_export/spec/lib/work_package_xls_export_spec.rb index 9dc649ea54..961634e6f0 100644 --- a/modules/xls_export/spec/lib/work_package_xls_export_spec.rb +++ b/modules/xls_export/spec/lib/work_package_xls_export_spec.rb @@ -83,27 +83,27 @@ describe "WorkPackageXlsExport" do CHILD_2 = 5 SINGLE = 8 FOLLOWED = 9 - RELATION = 7 - RELATION_DESCRIPTION = 9 - RELATED_SUBJECT = 12 + RELATION = 8 + RELATION_DESCRIPTION = 10 + RELATED_SUBJECT = 13 it 'produces the correct result' do - expect(query.columns.map(&:name)).to eq [:id, :subject, :type, :status, :assigned_to] + expect(query.columns.map(&:name)).to eq [:type, :id, :subject, :status, :assigned_to, :priority] # the first header row devides the sheet into work packages and relation columns - expect(sheet.rows.first.take(7)).to eq ['Work packages', nil, nil, nil, nil, nil, 'Relations'] + expect(sheet.rows.first.take(8)).to eq ['Work packages', nil, nil, nil, nil, nil, nil, 'Relations'] # the second header row includes the column names for work packages and relations expect(sheet.rows[1]) .to eq [ - nil, 'ID', 'Subject', 'Type', 'Status', 'Assignee', + nil, 'Type', 'ID', 'Subject', 'Status', 'Assignee', 'Priority', nil, 'Relation type', 'Delay', 'Description', 'ID', 'Type', 'Subject', nil ] # duplicates rows for each relation c2id = child_2.id - expect(sheet.column(1).drop(2)) + expect(sheet.column(2).drop(2)) .to eq [parent.id, parent.id, child_1.id, c2id, c2id, c2id, single.id, followed.id, child_2_child.id] # marks Parent as parent of Child 1 and 2 @@ -130,7 +130,7 @@ describe "WorkPackageXlsExport" do expect(sheet.row(CHILD_2 + 2)[RELATED_SUBJECT]).to eq 'Followed' # shows no relation information for Single - expect(sheet.row(SINGLE).drop(6).compact).to eq [] + expect(sheet.row(SINGLE).drop(7).compact).to eq [] # shows Followed as preceding Child 2' expect(sheet.row(FOLLOWED)[RELATION]).to eq 'precedes' @@ -140,20 +140,20 @@ describe "WorkPackageXlsExport" do # exports the correct data (examples) expect(sheet.row(PARENT)) .to eq [ - nil, parent.id, parent.subject, parent.type.name, parent.status.name, parent.assigned_to, + nil, parent.type.name, parent.id, parent.subject, parent.status.name, parent.assigned_to, parent.priority.name, nil, 'parent of', nil, nil, child_1.id, child_1.type.name, child_1.subject ] # delay nil as this is a parent-child relation not represented by an actual Relation record expect(sheet.row(SINGLE)) .to eq [ - nil, single.id, single.subject, single.type.name, single.status.name, single.assigned_to + nil, single.type.name, single.id, single.subject, single.status.name, single.assigned_to, single.priority.name ] expect(sheet.row(FOLLOWED)) .to eq [ - nil, followed.id, followed.subject, followed.type.name, followed.status.name, - followed.assigned_to, - nil, 'precedes', 0, relation.description, child_2.id, child_2.type.name, child_2.subject + nil, followed.type.name, followed.id, followed.subject, followed.status.name, + followed.assigned_to, followed.priority.name, + nil, 'precedes', 0, relation.description, child_2.id, child_2.type.name, child_2.subject ] end diff --git a/package.json b/package.json index cb151674fc..10735cbd61 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,6 @@ }, "private": true, "engines": { - "node": "~8.12.0" - }, - "dependencies": { - "webfonts-generator": "^0.4.0" + "node": "~10.15.3" } } diff --git a/script/ci/setup.sh b/script/ci/setup.sh index f8a445850e..ae281fe90e 100644 --- a/script/ci/setup.sh +++ b/script/ci/setup.sh @@ -33,8 +33,7 @@ set -e # script/ci/setup.sh # $1 = TEST_SUITE -# $2 = DB -# $3 = OPENPROJECT_EDITION +# $2 = OPENPROJECT_EDITION run() { echo $1; @@ -44,17 +43,11 @@ run() { eval $2; } -if [ $2 = "mysql" ]; then - run "mysql -u root -e \"CREATE DATABASE IF NOT EXISTS travis_ci_test DEFAULT CHARACTER SET = 'utf8' DEFAULT COLLATE 'utf8_general_ci';\"" - run "mysql -u root -e \"GRANT ALL ON travis_ci_test.* TO 'travis'@'localhost';\"" - run "cp script/templates/database.travis.mysql.yml config/database.yml" -elif [ $2 = "postgres" ]; then - run "psql -c 'create database travis_ci_test;' -U postgres" - run "cp script/templates/database.travis.postgres.yml config/database.yml" -fi +run "psql -c 'create database travis_ci_test;' -U postgres" +run "cp script/templates/database.travis.postgres.yml config/database.yml" -if [ "$3" = "bim" ]; then - export OPENPROJECT_EDITION="$3"; +if [ "$2" = "bim" ]; then + export OPENPROJECT_EDITION="$2"; else unset OPENPROJECT_EDITION fi diff --git a/spec/contracts/members/create_contract_spec.rb b/spec/contracts/members/create_contract_spec.rb index 3c3a79822d..65c491fba1 100644 --- a/spec/contracts/members/create_contract_spec.rb +++ b/spec/contracts/members/create_contract_spec.rb @@ -38,5 +38,39 @@ describe Members::CreateContract do end subject(:contract) { described_class.new(member, current_user) } + + describe '#validation' do + context 'if the principal is nil' do + let(:member_principal) { nil } + + it 'is invalid' do + expect_valid(false, principal: %i(blank)) + end + end + + context 'if the project is nil' do + let(:member_project) { nil } + + it 'is invalid' do + expect_valid(false, project: %i(blank)) + end + end + + context 'if the principal is a builtin user' do + let(:member_principal) { FactoryBot.build_stubbed(:anonymous) } + + it 'is invalid' do + expect_valid(false, principal: %i(unassignable)) + end + end + + context 'if the principal is a locked user' do + let(:member_principal) { FactoryBot.build_stubbed(:locked_user) } + + it 'is invalid' do + expect_valid(false, principal: %i(unassignable)) + end + end + end end end diff --git a/spec/contracts/members/shared_contract_examples.rb b/spec/contracts/members/shared_contract_examples.rb index a077e74840..5e03792f93 100644 --- a/spec/contracts/members/shared_contract_examples.rb +++ b/spec/contracts/members/shared_contract_examples.rb @@ -48,15 +48,15 @@ shared_examples_for 'member contract' do end let(:permissions) { [:manage_members] } - describe 'validation' do - def expect_valid(valid, symbols = {}) - expect(contract.validate).to eq(valid) + def expect_valid(valid, symbols = {}) + expect(contract.validate).to eq(valid) - symbols.each do |key, arr| - expect(contract.errors.symbols_for(key)).to match_array arr - end + symbols.each do |key, arr| + expect(contract.errors.symbols_for(key)).to match_array arr end + end + describe 'validation' do shared_examples 'is valid' do it 'is valid' do expect_valid(true) @@ -65,38 +65,6 @@ shared_examples_for 'member contract' do it_behaves_like 'is valid' - context 'if the project is nil' do - let(:member_project) { nil } - - it 'is invalid' do - expect_valid(false, project: %i(blank)) - end - end - - context 'if the principal is nil' do - let(:member_principal) { nil } - - it 'is invalid' do - expect_valid(false, principal: %i(blank)) - end - end - - context 'if the principal is a builtin user' do - let(:member_principal) { FactoryBot.build_stubbed(:anonymous) } - - it 'is invalid' do - expect_valid(false, principal: %i(unassignable)) - end - end - - context 'if the principal is a locked user' do - let(:member_principal) { FactoryBot.build_stubbed(:locked_user) } - - it 'is invalid' do - expect_valid(false, principal: %i(unassignable)) - end - end - context 'if the roles are nil' do let(:member_roles) { [] } diff --git a/spec/contracts/members/update_contract_spec.rb b/spec/contracts/members/update_contract_spec.rb new file mode 100644 index 0000000000..b30a80b9d1 --- /dev/null +++ b/spec/contracts/members/update_contract_spec.rb @@ -0,0 +1,73 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './shared_contract_examples' + +describe Members::UpdateContract do + it_behaves_like 'member contract' do + let(:member) do + FactoryBot.build_stubbed(:member, + project: member_project, + roles: member_roles, + principal: member_principal) + end + + subject(:contract) { described_class.new(member, current_user) } + + describe 'validation' do + context 'if the principal is changed' do + before do + member.principal = FactoryBot.build_stubbed(:user) + end + + it 'is invalid' do + expect_valid(false, user_id: %i(error_readonly)) + end + end + + context 'if the project is changed' do + before do + member.project = FactoryBot.build_stubbed(:project) + end + + it 'is invalid' do + expect_valid(false, project_id: %i(error_readonly)) + end + end + + context 'if the principal is a locked user' do + let(:member_principal) { FactoryBot.build_stubbed(:locked_user) } + + it 'is valid' do + expect_valid(true) + end + end + end + end +end diff --git a/spec/contracts/roles/create_contract_spec.rb b/spec/contracts/roles/create_contract_spec.rb new file mode 100644 index 0000000000..2f1437be53 --- /dev/null +++ b/spec/contracts/roles/create_contract_spec.rb @@ -0,0 +1,71 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './shared_contract_examples' + +describe Roles::CreateContract do + it_behaves_like 'roles contract' do + let(:role) do + Role.new.tap do |r| + r.name = role_name + r.assignable = role_assignable + r.permissions = role_permissions + end + end + + let(:global_role) do + GlobalRole.new.tap do |r| + r.name = role_name + r.permissions = role_permissions + end + end + + subject(:contract) { described_class.new(role, current_user) } + + describe 'validation' do + context 'with the type set manually' do + before do + role.type = 'GlobalRole' + end + + it_behaves_like 'is valid' + end + + context 'with the type set manually to something other than Role or GlobalRole' do + before do + role.type = 'MyRole' + end + + it 'is invalid' do + expect_valid(false, type: %i(inclusion)) + end + end + end + end +end diff --git a/spec/contracts/roles/shared_contract_examples.rb b/spec/contracts/roles/shared_contract_examples.rb new file mode 100644 index 0000000000..2f06be8927 --- /dev/null +++ b/spec/contracts/roles/shared_contract_examples.rb @@ -0,0 +1,114 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +shared_examples_for 'roles contract' do + let(:current_user) do + FactoryBot.build_stubbed(:admin) + end + let(:role_instance) { Role.new } + let(:role_name) { 'A role name' } + let(:role_assignable) { true } + let(:role_permissions) { [:view_work_packages] } + + def expect_valid(valid, symbols = {}) + expect(contract.validate).to eq(valid) + + symbols.each do |key, arr| + expect(contract.errors.symbols_for(key)).to match_array arr + end + end + + shared_examples 'is valid' do + it 'is valid' do + expect_valid(true) + end + end + + describe 'validation' do + it_behaves_like 'is valid' + + context 'if the name is nil' do + let(:role_name) { nil } + + it 'is invalid' do + expect_valid(false, name: %i(blank)) + end + end + + context 'if the permissions do not include their dependency' do + let(:role_permissions) { [:manage_members] } + + it 'is invalid' do + expect_valid(false, permissions: %i(dependency_missing)) + end + end + end + + describe '#assignable_permissions' do + let(:all_permissions) { %i[perm1 perm2 perm3] } + + context 'for a standard role' do + let(:public_permissions) { [:perm1] } + let(:global_permissions) { [:perm3] } + + before do + allow(OpenProject::AccessControl) + .to receive(:permissions) + .and_return(all_permissions) + allow(OpenProject::AccessControl) + .to receive(:global_permissions) + .and_return(global_permissions) + allow(OpenProject::AccessControl) + .to receive(:public_permissions) + .and_return(public_permissions) + end + + it 'is all non public, non global permissions' do + expect(contract.assignable_permissions) + .to eql [:perm2] + end + end + + context 'for a global role' do + let(:role) { global_role } + + before do + allow(OpenProject::AccessControl) + .to receive(:global_permissions) + .and_return(all_permissions) + end + + it 'is all the global permissions' do + expect(contract.assignable_permissions) + .to eql all_permissions + end + end + end +end diff --git a/spec/contracts/roles/update_contract_spec.rb b/spec/contracts/roles/update_contract_spec.rb new file mode 100644 index 0000000000..3ab1747185 --- /dev/null +++ b/spec/contracts/roles/update_contract_spec.rb @@ -0,0 +1,66 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './shared_contract_examples' + +describe Roles::UpdateContract do + it_behaves_like 'roles contract' do + let(:role) do + FactoryBot.build_stubbed(:role, + name: 'Some name', + assignable: !role_assignable).tap do |r| + r.name = role_name + r.assignable = role_assignable + r.permissions = role_permissions + end + end + + let(:global_role) do + FactoryBot.build_stubbed(:global_role, + name: 'Some name').tap do |r| + r.name = role_name + r.permissions = role_permissions + end + end + + subject(:contract) { described_class.new(role, current_user) } + + describe 'validation' do + context 'with the type set manually' do + before do + role.type = 'GlobalRole' + end + + it 'is invalid' do + expect_valid(false, type: %i(error_readonly)) + end + end + end + end +end diff --git a/spec/contracts/work_packages/base_contract_spec.rb b/spec/contracts/work_packages/base_contract_spec.rb index 6cf5c64aaa..0257b15bd9 100644 --- a/spec/contracts/work_packages/base_contract_spec.rb +++ b/spec/contracts/work_packages/base_contract_spec.rb @@ -47,7 +47,7 @@ describe WorkPackages::BaseContract do permissions.each do |permission| allow(u) .to receive(:allowed_to?) - .with(permission, project) + .with(permission, project, global: project.nil?) .and_return(true) end @@ -64,6 +64,7 @@ describe WorkPackages::BaseContract do delete_work_package_watchers manage_work_package_relations add_work_package_notes + assign_versions ) end let(:changed_values) { [] } diff --git a/spec/contracts/work_packages/create_contract_spec.rb b/spec/contracts/work_packages/create_contract_spec.rb index 9ce1ac4951..a7fcbc72dc 100644 --- a/spec/contracts/work_packages/create_contract_spec.rb +++ b/spec/contracts/work_packages/create_contract_spec.rb @@ -36,27 +36,23 @@ describe WorkPackages::CreateContract do let(:user) { FactoryBot.build_stubbed(:user) } subject(:contract) { described_class.new(work_package, user) } - let(:validated_contract) { + let(:validated_contract) do contract = subject contract.validate contract - } + end it_behaves_like 'work package contract' - describe 'authorization' do - def add_work_packages_allowed(in_project: true, in_global: true) - allow(user) - .to receive(:allowed_to?) - .with(:add_work_packages, project) - .and_return(in_project) - - allow(user) - .to receive(:allowed_to?) - .with(:add_work_packages, nil, global: true) - .and_return(in_global) + def add_work_packages_allowed(in_project: true, in_global: true) + allow(user) + .to receive(:allowed_to?) do |permission, permission_project, global: false| + (in_project && project == permission_project && permission == :add_work_packages) || + (in_global && global && permission == :add_work_packages) end + end + describe 'authorization' do context 'user allowed in project and project specified' do before do add_work_packages_allowed(in_project: true, in_global: true) @@ -118,8 +114,17 @@ describe WorkPackages::CreateContract do end describe 'author_id' do - it 'is valid if the user is the user is the user the contract is evaluated for' do - work_package.author = user + before do + add_work_packages_allowed(in_project: true, in_global: true) + work_package.project = project + end + + it 'is valid if the user is set by the sytem and the user is the user the contract is evaluated for' do + work_package.extend(Mixins::ChangedBySystem) + + work_package.change_by_system do + work_package.author = user + end expect(validated_contract.errors[:author_id]).to be_empty end @@ -128,7 +133,7 @@ describe WorkPackages::CreateContract do work_package.author = FactoryBot.build_stubbed(:user) expect(validated_contract.errors.symbols_for(:author_id)) - .to match_array [:invalid] + .to match_array %i[invalid error_readonly] end end end diff --git a/spec/contracts/work_packages/update_contract_spec.rb b/spec/contracts/work_packages/update_contract_spec.rb index 4c1fa3bb91..b8c3f45859 100644 --- a/spec/contracts/work_packages/update_contract_spec.rb +++ b/spec/contracts/work_packages/update_contract_spec.rb @@ -30,11 +30,42 @@ require 'spec_helper' require 'contracts/work_packages/shared_base_contract' describe WorkPackages::UpdateContract do - let(:project) { FactoryBot.create(:project, is_public: false) } - let(:work_package) { FactoryBot.create(:work_package, project: project) } - let(:user) { FactoryBot.create(:user, member_in_project: project, member_through_role: role) } - let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { %i[view_work_packages edit_work_packages] } + let(:project) do + FactoryBot.build_stubbed(:project, is_public: false).tap do |p| + allow(Project) + .to receive(:find) + .with(p.id) + .and_return(p) + end + end + let(:work_package) do + FactoryBot.build_stubbed(:work_package, + project: project, + type: type).tap do |wp| + + wp_scope = double('wp scope') + + allow(WorkPackage) + .to receive(:visible) + .with(user) + .and_return(wp_scope) + + allow(wp_scope) + .to receive(:exists?) do |id| + permissions.include?(:view_work_packages) && id == wp.id + end + end + end + let(:user) { FactoryBot.build_stubbed(:user) } + let(:type) { FactoryBot.build_stubbed(:type) } + let(:permissions) { %i[view_work_packages edit_work_packages assign_versions] } + + before do + allow(user) + .to receive(:allowed_to?) do |permission, context| + permissions.include?(permission) && context == project + end + end subject(:contract) { described_class.new(work_package, user) } @@ -78,7 +109,9 @@ describe WorkPackages::UpdateContract do end context 'full access' do - it { expect(contract.errors).to be_empty } + it 'is valid' do + expect(contract.errors).to be_empty + end end context 'no read access' do @@ -112,17 +145,35 @@ describe WorkPackages::UpdateContract do end end end + + context 'only assign_versions permission' do + let(:permissions) { %i[view_work_packages assign_versions] } + + it 'is valid' do + expect(contract.errors).to be_empty + end + end end describe 'project_id' do - let(:target_project) { FactoryBot.create(:project) } + let(:target_project) { FactoryBot.create(:project, types: [type]) } let(:target_permissions) { [:move_work_packages] } before do - FactoryBot.create :member, - user: user, - project: target_project, - roles: [FactoryBot.create(:role, permissions: target_permissions)] + allow(user) + .to receive(:allowed_to?) do |permission, context| + permissions.include?(permission) && context == project || + target_permissions.include?(permission) && context == target_project + end + + allow(work_package) + .to receive(:project) do + if work_package.project_id == target_project.id + target_project + else + project + end + end work_package.project = target_project @@ -136,7 +187,53 @@ describe WorkPackages::UpdateContract do context 'if the user lacks the permissions' do let(:target_permissions) { [] } it 'is invalid' do - expect(contract.errors.symbols_for(:project)).to match_array([:error_unauthorized]) + expect(contract.errors.symbols_for(:project_id)).to match_array([:error_readonly]) + end + end + end + + describe 'fixed_version' do + let(:version) { FactoryBot.build_stubbed(:version) } + + before do + allow(work_package) + .to receive(:assignable_versions) + .and_return([version]) + + work_package.attributes = attributes + + contract.validate + end + + context 'having full access' do + context 'with an assignable_version' do + let(:attributes) { { fixed_version_id: version.id } } + + it 'is valid' do + expect(contract.errors).to be_empty + end + end + + context 'with an unassignable_version' do + let(:attributes) { { fixed_version_id: version.id + 1 } } + + it 'adds an error' do + expect(contract.errors.symbols_for(:fixed_version_id)) + .to include(:inclusion) + end + end + end + + context 'write access' do + let(:permissions) { %i[view_work_packages edit_work_packages] } + + context 'if assigning a version' do + let(:attributes) { { fixed_version_id: version.id } } + + it 'adds an error' do + expect(contract.errors.symbols_for(:fixed_version_id)) + .to include(:error_readonly) + end end end end @@ -166,7 +263,7 @@ describe WorkPackages::UpdateContract do end context 'if the user has only edit permissions' do - it { expect(contract.errors.symbols_for(:base)).to include(:error_unauthorized) } + it { expect(contract.errors.symbols_for(:parent_id)).to include(:error_readonly) } end context 'if the user has edit and subtasks permissions' do @@ -189,7 +286,7 @@ describe WorkPackages::UpdateContract do context 'no write access' do let(:permissions) { [:view_work_packages] } - it { expect(contract.errors.symbols_for(:base)).to include(:error_unauthorized) } + it { expect(contract.errors.symbols_for(:parent_id)).to include(:error_readonly) } end context 'with manage_subtasks permission' do @@ -205,7 +302,35 @@ describe WorkPackages::UpdateContract do contract.validate end - it { expect(contract.errors.symbols_for(:base)).to include(:error_unauthorized) } + it { expect(contract.errors.symbols_for(:subject)).to include(:error_readonly) } + end + end + end + + describe '#writable_attributes' do + subject { contract.writable_attributes } + + context 'for a user having only the edit_work_packages permission' do + let(:permissions) { %i[edit_work_packages] } + + it 'includes all attributes except fixed_version_id' do + expect(subject) + .to include('subject', 'start_date', 'description') + + expect(subject) + .not_to include('fixed_version_id', 'fixed_version') + end + end + + context 'for a user having only the assign_versions permission' do + let(:permissions) { %i[assign_versions] } + + it 'includes all attributes except fixed_version_id' do + expect(subject) + .to include('fixed_version_id', 'fixed_version') + + expect(subject) + .not_to include('subject', 'start_date', 'description') end end end diff --git a/spec/controllers/roles_controller_spec.rb b/spec/controllers/roles_controller_spec.rb new file mode 100644 index 0000000000..1d16bdb48a --- /dev/null +++ b/spec/controllers/roles_controller_spec.rb @@ -0,0 +1,375 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe RolesController, type: :controller do + let(:user) do + FactoryBot.build_stubbed(:admin) + end + let(:params) do + { + role: { + name: 'A role name', + permissions: ['add_work_packages', 'edit_work_packages', 'log_time', ''], + assignable: '0' + }, + copy_workflow_from: '5' + } + end + + before do + login_as(user) + end + + describe '#create' do + let(:new_role) { double('role double') } + let(:service_call) { ServiceResult.new(success: true, result: new_role) } + let(:create_params) do + cp = ActionController::Parameters.new(params[:role]) + .merge(global_role: nil, copy_workflow_from: '5') + cp.permit! + + cp + end + let(:create_service) do + service_double = double('create service') + + expect(Roles::CreateService) + .to receive(:new) + .with(user: user) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(create_params) + .and_return(service_call) + end + + before do + create_service + + post :create, params: params + end + + context 'success' do + context 'for a member role' do + it 'redirects to roles#index' do + expect(response) + .to redirect_to(roles_path) + end + + it 'has a flash message' do + expect(flash[:notice]) + .to eql I18n.t(:notice_successful_create) + end + end + + context 'for a global role' do + let(:params) do + { + role: { + name: 'A role name', + permissions: ['add_work_packages', 'edit_work_packages', 'log_time', ''], + assignable: '0' + }, + global_role: '1', + copy_workflow_from: '5' + } + end + let(:create_params) do + cp = ActionController::Parameters.new(params[:role]) + .merge(global_role: '1', copy_workflow_from: '5') + cp.permit! + + cp + end + + it 'redirects to roles#index' do + expect(response) + .to redirect_to(roles_path) + end + + it 'has a flash message' do + expect(flash[:notice]) + .to eql I18n.t(:notice_successful_create) + end + end + end + + context 'failure' do + let(:service_call) { ServiceResult.new(success: false, result: new_role) } + + it 'returns a 200 OK' do + expect(response.status) + .to eql(200) + end + + it 'renders the new template' do + expect(response) + .to render_template('roles/new') + end + + it 'has the service call assigned' do + expect(assigns[:call]) + .to eql service_call + end + + it 'has the role assigned' do + expect(assigns[:role]) + .to eql new_role + end + end + end + + describe '#update' do + let(:params) do + { + id: role.id, + role: { + name: 'A role name', + permissions: ['add_work_packages', 'edit_work_packages', 'log_time', ''], + assignable: '0' + } + } + end + let(:role) do + double('role double', id: 6).tap do |d| + allow(Role) + .to receive(:find) + .with(d.id.to_s) + .and_return(d) + end + end + let(:service_call) { ServiceResult.new(success: true, result: role) } + let(:update_params) do + cp = ActionController::Parameters.new(params[:role]) + cp.permit! + + cp + end + let(:update_service) do + service_double = double('update service') + + expect(Roles::UpdateService) + .to receive(:new) + .with(user: user, model: role) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(update_params) + .and_return(service_call) + end + + before do + update_service + + put :update, params: params + end + + context 'success' do + it 'redirects to roles#index' do + expect(response) + .to redirect_to(roles_path) + end + + it 'has a flash message' do + expect(flash[:notice]) + .to eql I18n.t(:notice_successful_update) + end + end + + context 'failure' do + let(:service_call) { ServiceResult.new(success: false, result: role) } + + it 'returns a 200 OK' do + expect(response.status) + .to eql(200) + end + + it 'renders the edit template' do + expect(response) + .to render_template('roles/edit') + end + + it 'has the service call assigned' do + expect(assigns[:call]) + .to eql service_call + end + + it 'has the role assigned' do + expect(assigns[:role]) + .to eql role + end + end + end + + describe '#bulk_update' do + let(:params) do + { + permissions: { '0' => '', '1' => ['edit_work_packages'], '3' => %w(add_work_packages delete_work_packages) } + } + end + let(:role0) do + double('role double', id: 0) + end + let(:role1) do + double('role double', id: 1) + end + let(:role2) do + double('role double', id: 2) + end + let(:role3) do + double('role double', id: 3) + end + let(:roles) do + [role0, role1, role2, role3] + end + + let!(:roles_scope) do + allow(Role) + .to receive(:order) + .and_return(roles) + end + + let(:service_call0) { ServiceResult.new(success: true, result: role0) } + let(:service_call1) { ServiceResult.new(success: true, result: role1) } + let(:service_call2) { ServiceResult.new(success: true, result: role2) } + let(:service_call3) { ServiceResult.new(success: true, result: role3) } + let(:update_params0) do + { permissions: [] } + end + let(:update_service0) do + service_double = double('update service') + + expect(Roles::UpdateService) + .to receive(:new) + .with(user: user, model: role0) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(update_params0) + .and_return(service_call0) + end + let(:update_params1) do + { permissions: params[:permissions]['1'] } + end + let(:update_service1) do + service_double = double('update service') + + expect(Roles::UpdateService) + .to receive(:new) + .with(user: user, model: role1) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(update_params1) + .and_return(service_call1) + end + let(:update_params2) do + { permissions: [] } + end + let(:update_service2) do + service_double = double('update service') + + expect(Roles::UpdateService) + .to receive(:new) + .with(user: user, model: role2) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(update_params2) + .and_return(service_call2) + end + let(:update_params3) do + { permissions: params[:permissions]['3'] } + end + let(:update_service3) do + service_double = double('update service') + + expect(Roles::UpdateService) + .to receive(:new) + .with(user: user, model: role3) + .and_return(service_double) + + expect(service_double) + .to receive(:call) + .with(update_params3) + .and_return(service_call3) + end + + before do + update_service0 + update_service1 + update_service2 + update_service3 + + put :bulk_update, params: params + end + + context 'success' do + it 'redirects to roles#index' do + expect(response) + .to redirect_to(roles_path) + end + + it 'has a flash message' do + expect(flash[:notice]) + .to eql I18n.t(:notice_successful_update) + end + end + + context 'failure' do + let(:service_call2) { ServiceResult.new(success: false, result: role2) } + + it 'returns a 200 OK' do + expect(response.status) + .to eql(200) + end + + it 'renders the report template' do + expect(response) + .to render_template('roles/report') + end + + it 'has the service call assigned' do + expect(assigns[:calls]) + .to match_array [service_call0, service_call1, service_call2, service_call3] + end + + it 'has the roles assigned' do + expect(assigns[:roles]) + .to match_array roles + end + end + end +end diff --git a/spec/controllers/versions_controller_spec.rb b/spec/controllers/versions_controller_spec.rb index 592bbe286a..e89361f26b 100644 --- a/spec/controllers/versions_controller_spec.rb +++ b/spec/controllers/versions_controller_spec.rb @@ -65,6 +65,57 @@ describe VersionsController, type: :controller do end end + context 'with showing selected types' do + let(:type_a) { FactoryBot.create :type } + let(:type_b) { FactoryBot.create :type } + + let(:wp_a) { FactoryBot.create :work_package, type: type_a, project: project, fixed_version: version1 } + let(:wp_b) { FactoryBot.create :work_package, type: type_b, project: project, fixed_version: version1 } + + before do + project.types = [type_a, type_b] + project.save! + + [wp_a, wp_b] # create work packages + + login_as(user) + end + + describe 'with all types' do + before do + get :index, params: { project_id: project, completed: '1' } + end + + it { expect(response).to be_successful } + it { expect(response).to render_template('index') } + + it "shows all work packages" do + issues_by_version = assigns(:issues_by_version) + work_packages = issues_by_version[version1] + + expect(work_packages).to include wp_a + expect(work_packages).to include wp_b + end + end + + describe 'with selected types' do + before do + get :index, params: { project_id: project, completed: '1', type_ids: [type_b.id] } + end + + it { expect(response).to be_successful } + it { expect(response).to render_template('index') } + + it "shows only work packages of the selected type" do + issues_by_version = assigns(:issues_by_version) + work_packages = issues_by_version[version1] + + expect(work_packages).not_to include wp_a + expect(work_packages).to include wp_b + end + end + end + context 'with showing completed versions' do before do login_as(user) diff --git a/spec/controllers/work_packages/bulk_controller_spec.rb b/spec/controllers/work_packages/bulk_controller_spec.rb index 4e12c9a5e3..193f89865a 100644 --- a/spec/controllers/work_packages/bulk_controller_spec.rb +++ b/spec/controllers/work_packages/bulk_controller_spec.rb @@ -32,79 +32,80 @@ describe WorkPackages::BulkController, type: :controller do let(:user) { FactoryBot.create(:user) } let(:user2) { FactoryBot.create(:user) } let(:custom_field_value) { '125' } - let(:custom_field_1) { + let(:custom_field_1) do FactoryBot.create(:work_package_custom_field, - field_format: 'string', - is_for_all: true) - } + field_format: 'string', + is_for_all: true) + end let(:custom_field_2) { FactoryBot.create(:work_package_custom_field) } let(:custom_field_user) { FactoryBot.create(:user_issue_custom_field) } let(:status) { FactoryBot.create(:status) } - let(:type) { + let(:type) do FactoryBot.create(:type_standard, - custom_fields: [custom_field_1, custom_field_2, custom_field_user]) - } - let(:project_1) { + custom_fields: [custom_field_1, custom_field_2, custom_field_user]) + end + let(:project_1) do FactoryBot.create(:project, - types: [type], - work_package_custom_fields: [custom_field_2]) - } - let(:project_2) { + types: [type], + work_package_custom_fields: [custom_field_2]) + end + let(:project_2) do FactoryBot.create(:project, - types: [type]) - } - let(:role) { + types: [type]) + end + let(:role) do FactoryBot.create(:role, - permissions: [:edit_work_packages, - :view_work_packages, - :manage_subtasks]) - } - let(:member1_p1) { + permissions: %i[edit_work_packages + view_work_packages + manage_subtasks + assign_versions]) + end + let(:member1_p1) do FactoryBot.create(:member, - project: project_1, - principal: user, - roles: [role]) - } - let(:member2_p1) { + project: project_1, + principal: user, + roles: [role]) + end + let(:member2_p1) do FactoryBot.create(:member, - project: project_1, - principal: user2, - roles: [role]) - } - let(:member1_p2) { + project: project_1, + principal: user2, + roles: [role]) + end + let(:member1_p2) do FactoryBot.create(:member, - project: project_2, - principal: user, - roles: [role]) - } - let(:work_package_1) { + project: project_2, + principal: user, + roles: [role]) + end + let(:work_package_1) do FactoryBot.create(:work_package, - author: user, - assigned_to: user, - responsible: user2, - type: type, - status: status, - custom_field_values: { custom_field_1.id => custom_field_value }, - project: project_1) - } - let(:work_package_2) { + author: user, + assigned_to: user, + responsible: user2, + type: type, + status: status, + custom_field_values: { custom_field_1.id => custom_field_value }, + project: project_1) + end + let(:work_package_2) do FactoryBot.create(:work_package, - author: user, - assigned_to: user, - responsible: user2, - type: type, - status: status, - custom_field_values: { custom_field_1.id => custom_field_value }, - project: project_1) - } - let(:work_package_3) { + author: user, + assigned_to: user, + responsible: user2, + type: type, + status: status, + custom_field_values: { custom_field_1.id => custom_field_value }, + project: project_1) + end + let(:work_package_3) do FactoryBot.create(:work_package, - author: user, - type: type, - status: status, - custom_field_values: { custom_field_1.id => custom_field_value }, - project: project_2) - } + author: user, + type: type, + status: status, + custom_field_values: { custom_field_1.id => custom_field_value }, + project: project_2) + end let(:stub_work_package) { FactoryBot.build_stubbed(:work_package) } @@ -126,7 +127,7 @@ describe WorkPackages::BulkController, type: :controller do end context 'same project' do - before do get :edit, params: { ids: [work_package_1.id, work_package_2.id] } end + before { get :edit, params: { ids: [work_package_1.id, work_package_2.id] } } it_behaves_like :response @@ -197,7 +198,7 @@ describe WorkPackages::BulkController, type: :controller do context 'in host' do let(:url) { '/work_packages' } - before do put :update, params: { ids: work_package_ids, back_url: url } end + before { put :update, params: { ids: work_package_ids, back_url: url } } subject { response } @@ -209,7 +210,7 @@ describe WorkPackages::BulkController, type: :controller do context 'of host' do let(:url) { 'http://google.com' } - before do put :update, params: { ids: work_package_ids, back_url: url } end + before { put :update, params: { ids: work_package_ids, back_url: url } } subject { response } @@ -225,18 +226,18 @@ describe WorkPackages::BulkController, type: :controller do let!(:role_with_permission_to_add_watchers) { FactoryBot.create(:role, permissions: role.permissions + [:add_work_package_watchers]) } let!(:other_user) { FactoryBot.create :user } - let!(:other_member_1) { + let!(:other_member_1) do FactoryBot.create(:member, - project: project_1, - principal: other_user, - roles: [role_with_permission_to_add_watchers]) - } - let!(:other_member_2) { + project: project_1, + principal: other_user, + roles: [role_with_permission_to_add_watchers]) + end + let!(:other_member_2) do FactoryBot.create(:member, - project: project_2, - principal: other_user, - roles: [role]) - } + project: project_2, + principal: other_user, + roles: [role]) + end let(:description) { 'Text' } let(:work_package_params) do @@ -300,11 +301,11 @@ describe WorkPackages::BulkController, type: :controller do describe '#custom_fields' do let(:result) { [custom_field_value] } - subject { + subject do WorkPackage.where(id: work_package_ids) .map { |w| w.custom_value_for(custom_field_1.id).value } .uniq - } + end it { is_expected.to match_array(result) } end @@ -313,11 +314,11 @@ describe WorkPackages::BulkController, type: :controller do describe '#notes' do let(:result) { ['Bulk editing'] } - subject { + subject do WorkPackage.where(id: work_package_ids) .map { |w| w.last_journal.notes } .uniq - } + end it { is_expected.to match_array(result) } end @@ -325,11 +326,11 @@ describe WorkPackages::BulkController, type: :controller do describe '#details' do let(:result) { [1] } - subject { + subject do WorkPackage.where(id: work_package_ids) .map { |w| w.last_journal.details.size } .uniq - } + end it { is_expected.to match_array(result) } end @@ -350,7 +351,7 @@ describe WorkPackages::BulkController, type: :controller do let(:work_package_ids) { [work_package_1.id, work_package_2.id, work_package_3.id] } context 'with permission' do - before do member1_p2 end + before { member1_p2 } include_context 'update_request' @@ -381,12 +382,12 @@ describe WorkPackages::BulkController, type: :controller do subject { work_packages.map(&:assigned_to_id).uniq } context 'allowed' do - let!(:member_group_p1) { + let!(:member_group_p1) do FactoryBot.create(:member, project: project_1, principal: group, roles: [role]) - } + end include_context 'update_request' it 'does succeed' do @@ -417,13 +418,13 @@ describe WorkPackages::BulkController, type: :controller do describe '#status' do let(:closed_status) { FactoryBot.create(:closed_status) } - let(:workflow) { + let(:workflow) do FactoryBot.create(:workflow, - role: role, - type_id: type.id, - old_status: status, - new_status: closed_status) - } + role: role, + type_id: type.id, + old_status: status, + new_status: closed_status) + end before do workflow @@ -441,11 +442,11 @@ describe WorkPackages::BulkController, type: :controller do end describe '#parent' do - let(:parent) { + let(:parent) do FactoryBot.create(:work_package, - author: user, - project: project_1) - } + author: user, + project: project_1) + end before do put :update, @@ -473,10 +474,10 @@ describe WorkPackages::BulkController, type: :controller do } end - subject { + subject do work_packages.map { |w| w.custom_value_for(custom_field_1.id).value } .uniq - } + end it { is_expected.to match_array [result] } end @@ -511,17 +512,17 @@ describe WorkPackages::BulkController, type: :controller do describe '#version' do describe 'set fixed_version_id attribute to some version' do - let(:version) { + let(:version) do FactoryBot.create(:version, - status: 'open', - sharing: 'tree', - project: subproject) - } - let(:subproject) { + status: 'open', + sharing: 'tree', + project: subproject) + end + let(:subproject) do FactoryBot.create(:project, - parent: project_1, - types: [type]) - } + parent: project_1, + types: [type]) + end before do put :update, diff --git a/spec/factories/member_role_factory.rb b/spec/factories/member_role_factory.rb new file mode 100644 index 0000000000..77f445fde5 --- /dev/null +++ b/spec/factories/member_role_factory.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +FactoryBot.define do + factory :member_role do + member + role + end +end diff --git a/spec/factories/project_factory.rb b/spec/factories/project_factory.rb index 48d2593bea..0a9489a0c0 100644 --- a/spec/factories/project_factory.rb +++ b/spec/factories/project_factory.rb @@ -38,7 +38,7 @@ FactoryBot.define do sequence(:identifier) { |n| "myproject_no_#{n}" } created_on { Time.now } updated_on { Time.now } - enabled_module_names { Redmine::AccessControl.available_project_modules } + enabled_module_names { OpenProject::AccessControl.available_project_modules } callback(:after_build) do |project, evaluator| disabled_modules = Array(evaluator.disable_modules) diff --git a/spec/features/accessibility/work_packages/work_package_query_spec.rb b/spec/features/accessibility/work_packages/work_package_query_spec.rb index ef533bf8d3..e622929546 100644 --- a/spec/features/accessibility/work_packages/work_package_query_spec.rb +++ b/spec/features/accessibility/work_packages/work_package_query_spec.rb @@ -115,7 +115,7 @@ describe 'Work package index accessibility', type: :feature, selenium: true do describe 'id column' do let(:link_caption) { 'ID' } - let(:column_header_selector) { '.work-package-table--container th:nth-of-type(1)' } + let(:column_header_selector) { '.work-package-table--container th:nth-of-type(2)' } let(:column_header_link_selector) { column_header_selector + ' a' } it_behaves_like 'sortable column' @@ -123,7 +123,7 @@ describe 'Work package index accessibility', type: :feature, selenium: true do describe 'subject column' do let(:link_caption) { 'Subject' } - let(:column_header_selector) { '.work-package-table--container th:nth-of-type(2)' } + let(:column_header_selector) { '.work-package-table--container th:nth-of-type(3)' } let(:column_header_link_selector) { column_header_selector + ' #subject' } it_behaves_like 'sortable column' @@ -131,7 +131,7 @@ describe 'Work package index accessibility', type: :feature, selenium: true do describe 'type column' do let(:link_caption) { 'Type' } - let(:column_header_selector) { '.work-package-table--container th:nth-of-type(3)' } + let(:column_header_selector) { '.work-package-table--container th:nth-of-type(1)' } let(:column_header_link_selector) { column_header_selector + ' a' } it_behaves_like 'sortable column' @@ -165,10 +165,10 @@ describe 'Work package index accessibility', type: :feature, selenium: true do context 'focus' do let(:first_link_selector) do - ".wp-row-#{work_package.id} td.id a" + ".wp-row-#{work_package.id} .wp-table--cell-span.type" end let(:second_link_selector) do - ".wp-row-#{another_work_package.id} td.id a" + ".wp-row-#{another_work_package.id} .wp-table--cell-span.type" end it 'navigates with J and K' do diff --git a/spec/features/roles/create_spec.rb b/spec/features/roles/create_spec.rb index fac69f04ed..774b932f76 100644 --- a/spec/features/roles/create_spec.rb +++ b/spec/features/roles/create_spec.rb @@ -59,6 +59,17 @@ describe 'Role creation', type: :feature, js: true do .to have_selector('.errorExplanation', text: 'Name has already been taken') fill_in 'Name', with: 'New role name' + + # This will lead to an error as manage versions requires view versions + check 'Manage members' + + click_button 'Create' + + expect(page) + .to have_selector('.errorExplanation', + text: "Permissions need to also include 'View members' as 'Manage members' is selected.") + + check 'View members' select existing_role.name, from: 'Copy workflow from' click_button 'Create' @@ -78,6 +89,10 @@ describe 'Role creation', type: :feature, js: true do .to have_checked_field('Edit work packages') expect(page) .to have_checked_field('Edit project') + expect(page) + .to have_checked_field('Manage members') + expect(page) + .to have_checked_field('View members') # By default as Non Member has that permissions expect(page) @@ -86,7 +101,9 @@ describe 'Role creation', type: :feature, js: true do .to have_checked_field('View wiki') expect(page) - .to have_unchecked_field('Manage versions') + .to have_unchecked_field('Select types') + expect(page) + .to have_unchecked_field('Delete watchers') # Workflow should be copied over. # Workflow routes are not resource-oriented. diff --git a/spec/features/time_entry/root_time_entries_spec.rb b/spec/features/time_entry/root_time_entries_spec.rb deleted file mode 100644 index 2d625a032c..0000000000 --- a/spec/features/time_entry/root_time_entries_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require "spec_helper" - -describe "/time_entries", type: :feature do - let(:user) { FactoryBot.create :admin } - - describe "sorting time entries", js: true do - let(:projects) { FactoryBot.create_list :project, 3 } - let(:comments) { ["TE 2", "TE 1", "TE 3"] } - let(:hours) { [2, 5, 1] } - - let!(:time_entries) do - comments.zip(projects).zip(hours).map do |comment_and_project, hours| - comment, project = comment_and_project - work_package = FactoryBot.create :work_package, project: project - - FactoryBot.create :time_entry, - comments: comment, - work_package: work_package, - project: project, - hours: hours - end - end - - def shown_comments - page.all("td.comments").map(&:text) - end - - def shown_hours - page.all("td.hours").map(&:text).map(&:to_i) - end - - before do - login_as user - visit time_entries_path - end - - it "should allow sorting the time entries" do - expect(page).to have_selector('td.comments', text: "TE 2") - expect(page).to have_selector('td.comments', text: "TE 1") - expect(page).to have_selector('td.comments', text: "TE 3") - - - click_on "Comment" - expect(page).to have_selector('td.comments', count: 3) - expect(shown_comments).to eq comments.sort - - click_on "Hours" - expect(page).to have_selector('td.comments', count: 3) - expect(shown_hours).to eq hours.sort - - click_on "Hours" - expect(page).to have_selector('td.comments', count: 3) - expect(shown_hours).to eq hours.sort.reverse - end - end -end diff --git a/spec/features/time_entry/time_entry_report_spec.rb b/spec/features/time_entry/time_entry_report_spec.rb index f2567f8144..f58438e7a1 100644 --- a/spec/features/time_entry/time_entry_report_spec.rb +++ b/spec/features/time_entry/time_entry_report_spec.rb @@ -34,19 +34,19 @@ describe 'time entry report', type: :feature, js: true do let(:work_package) { FactoryBot.create(:work_package, project: project) } let!(:project_time_entry) { FactoryBot.create_list(:time_entry, - 2, - project: project, - work_package: work_package, - hours: 2.5) + 2, + project: project, + work_package: work_package, + hours: 2.5) } let(:project2) { FactoryBot.create(:project) } let(:work_package2) { FactoryBot.create(:work_package, project: project2) } let!(:project_time_entry2) { FactoryBot.create(:time_entry, - project: project2, - spent_on: 1.year.ago, - work_package: work_package2, - hours: 5.0) + project: project2, + spent_on: 1.year.ago, + work_package: work_package2, + hours: 5.0) } let(:user) { FactoryBot.create(:admin) } @@ -54,31 +54,6 @@ describe 'time entry report', type: :feature, js: true do login_as(user) end - describe 'details' do - context 'for a single project' do - before do - visit project_time_entries_path(project.identifier) - end - - it 'should list the time entries' do - expect(page).to have_selector('tr.time-entry', count: 2) - expect(page).to have_selector('.time-entry .hours', text: 2.5, count: 2) - end - end - - context 'for all projects' do - before do - visit time_entries_path - end - - it 'should list the time entries' do - expect(page).to have_selector('tr.time-entry', count: 3) - expect(page).to have_selector('.time-entry .hours', text: 2.5, count: 2) - expect(page).to have_selector('.time-entry .hours', text: 5.0) - end - end - end - describe 'reports' do before do visit time_entries_report_path diff --git a/spec/features/types/form_configuration_query_spec.rb b/spec/features/types/form_configuration_query_spec.rb index 4dcc871a95..1375477d32 100644 --- a/spec/features/types/form_configuration_query_spec.rb +++ b/spec/features/types/form_configuration_query_spec.rb @@ -55,6 +55,12 @@ describe 'form query configuration', type: :feature, js: true do type: type_task ) end + let!(:unrelated_task) do + FactoryBot.create :work_package, subject: 'Unrelated task', type: type_task, project: project + end + let!(:unrelated_bug) do + FactoryBot.create :work_package, subject: 'Unrelated bug', type: type_bug, project: project + end let!(:related_task_other_project) do FactoryBot.create :work_package, new_relation.merge( @@ -233,6 +239,21 @@ describe 'form query configuration', type: :feature, js: true do embedded_table.expect_work_package_listed related_task, related_task_other_project embedded_table.ensure_work_package_not_listed! related_bug + # Expect no reference to unrelated bug + autocompleter = embedded_table.click_reference_inline_create + results = embedded_table.search_autocomplete autocompleter, + query: 'Unrelated', + results_selector: '.ng-dropdown-panel-items' + + expect(results).to have_text "Task ##{unrelated_task.id} Unrelated task" + expect(results).to have_no_text "Bug ##{unrelated_task.id} Unrelated bug" + + # Cancel that referencing + page.find('.wp-create-relation--cancel').click + + # Reference the task type + embedded_table.reference_work_package unrelated_task + # Go back to type configuration visit edit_type_tab_path(id: type_bug.id, tab: "form_configuration") @@ -258,7 +279,7 @@ describe 'form query configuration', type: :feature, js: true do wp_page.expect_group('Subtasks') do embedded_table = Pages::EmbeddedWorkPackagesTable.new(find('.work-packages-embedded-view--container')) - embedded_table.expect_work_package_listed related_task, related_bug + embedded_table.expect_work_package_listed related_task, related_bug, unrelated_task end end end diff --git a/spec/features/work_packages/copy_spec.rb b/spec/features/work_packages/copy_spec.rb index 3639ed10b8..61877b9bbf 100644 --- a/spec/features/work_packages/copy_spec.rb +++ b/spec/features/work_packages/copy_spec.rb @@ -47,7 +47,8 @@ RSpec.feature 'Work package copy', js: true, selenium: true do permissions: %i[view_work_packages add_work_packages manage_work_package_relations - edit_work_packages]) + edit_work_packages + assign_versions]) end let(:type) { FactoryBot.create(:type) } let(:project) { FactoryBot.create(:project, types: [type]) } diff --git a/spec/features/work_packages/details/inplace_editor/version_editor_spec.rb b/spec/features/work_packages/details/inplace_editor/version_editor_spec.rb index b1bd795ebb..9dda873061 100644 --- a/spec/features/work_packages/details/inplace_editor/version_editor_spec.rb +++ b/spec/features/work_packages/details/inplace_editor/version_editor_spec.rb @@ -9,33 +9,41 @@ describe 'subject inplace editor', js: true, selenium: true do let(:subproject1) { FactoryBot.create :project_with_types, name: 'Child', parent: project } let(:subproject2) { FactoryBot.create :project_with_types, name: 'Aunt', parent: project } - let!(:version) { + let!(:version) do FactoryBot.create(:version, status: 'open', sharing: 'tree', project: project) - } - let!(:version2) { + end + let!(:version2) do FactoryBot.create(:version, status: 'open', sharing: 'tree', project: subproject1) - } - let!(:version3) { + end + let!(:version3) do FactoryBot.create(:version, status: 'open', sharing: 'tree', project: subproject2) - } + end let(:property_name) { :version } let(:work_package) { FactoryBot.create :work_package, project: project } - let(:user) { FactoryBot.create :admin } - let(:second_user) { FactoryBot.create :user, member_in_project: project, member_through_role: role } - let(:role) { FactoryBot.create(:role, permissions: %i[view_work_packages edit_work_packages]) } + let(:user) do + FactoryBot.create :user, + member_in_project: project, + member_with_permissions: %i[view_work_packages edit_work_packages manage_versions assign_versions] + end + let(:second_user) do + FactoryBot.create :user, + member_in_project: project, + member_with_permissions: %i[view_work_packages edit_work_packages assign_versions] + end + let(:permissions) { %i[view_work_packages edit_work_packages assign_versions] } let(:work_package_page) { Pages::FullWorkPackage.new(work_package) } - context 'with admin permissions' do + context 'with manage permissions' do before do login_as(user) end @@ -84,7 +92,7 @@ describe 'subject inplace editor', js: true, selenium: true do field.activate! field.input_element.find('input').set 'Version that does not exist' - expect(page).not_to have_selector('.ng-option', text: 'Create new: Version that does not exist') + expect(page).not_to have_selector('.ng-option', text: 'Create: Version that does not exist') end end end diff --git a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb index e50412e9ef..c79ec8d7af 100644 --- a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb +++ b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb @@ -32,7 +32,7 @@ describe 'Work package with relation query group', js: true, selenium: true do include_context 'ng-select-autocomplete helpers' let(:user) { FactoryBot.create :admin } - let(:project) { FactoryBot.create :project } + let(:project) { FactoryBot.create :project, types: [type] } let(:relation_type) { :parent } let(:relation_target) { work_package } let(:new_relation) do @@ -64,7 +64,7 @@ describe 'Work package with relation query group', js: true, selenium: true do let(:relations_tab) { find('.tabrow li', text: 'RELATIONS') } let(:embedded_table) { Pages::EmbeddedWorkPackagesTable.new(first('wp-single-view .work-packages-embedded-view--container')) } - # let(:visit) { true } + let(:visit) { true } before do # inline create needs defaults @@ -74,8 +74,11 @@ describe 'Work package with relation query group', js: true, selenium: true do priority.update_attribute :is_default, true login_as user - full_wp.visit! - full_wp.ensure_page_loaded + + if visit + full_wp.visit! + full_wp.ensure_page_loaded + end end context 'children table' do @@ -102,7 +105,82 @@ describe 'Work package with relation query group', js: true, selenium: true do embedded_table.ensure_work_package_not_listed!(related_work_package) end relations.expect_not_child(related_work_package) + end + end + + describe 'follower table with project filters', clear_cache: true do + let(:visit) { false } + let!(:project2) { FactoryBot.create(:project, types: [type]) } + let!(:project3) { FactoryBot.create(:project, types: [type]) } + let(:relation_type) { :follows } + let!(:related_work_package) do + FactoryBot.create :work_package, + project: project2, + type: type, + follows: [work_package] + end + let(:type) do + FactoryBot.create :type_with_relation_query_group, relation_filter: relation_type + end + let(:query_text) { 'Embedded Table for follows'.upcase } + + before do + query = type.attribute_groups.last.query + query.add_filter('project_id', '=', [project2.id, project3.id]) + # User has no permission to save, avoid creating another user to allow it + query.save!(validate: false) + type.save! + end + + context 'with a user who has permission in one project' do + let(:role) { FactoryBot.create(:role, permissions: permissions) } + let(:permissions) { [:view_work_packages, :add_work_packages, :manage_work_package_relations] } + let(:user) do + FactoryBot.create(:user, + member_in_project: project, + member_through_role: role) + end + let!(:project2_member) { + member = FactoryBot.build(:member, user: user, project: project2) + member.roles = [role] + member.save! + } + + it 'can load the query and inline create' do + full_wp.visit! + full_wp.ensure_page_loaded + + expect(page).to have_selector('.attributes-group--header-text', text: query_text, wait: 20) + embedded_table.expect_work_package_listed related_work_package + embedded_table.click_inline_create + + subject_field = embedded_table.edit_field(nil, :subject) + subject_field.expect_active! + subject_field.set_value 'Another subject' + subject_field.save! + + embedded_table.expect_work_package_subject 'Another subject' + end + end + + context 'with a user who has no permission in any project' do + let(:role) { FactoryBot.create(:role, permissions: permissions) } + let(:permissions) { [:view_work_packages] } + let(:user) do + FactoryBot.create(:user, + member_in_project: project, + member_through_role: role) + end + + it 'hides that group automatically without showing an error' do + full_wp.visit! + full_wp.ensure_page_loaded + + # Will first try to load the query, and then hide it. + expect(page).to have_no_selector('.attributes-group--header-text', text: query_text, wait: 20) + expect(page).to have_no_selector('.work-packages-embedded-view--container .notification-box.-error') + end end end @@ -123,6 +201,7 @@ describe 'Work package with relation query group', js: true, selenium: true do it 'creates and removes across all tables' do embedded_table.table_container.find('a', text: I18n.t('js.relation_buttons.create_new')).click subject_field = embedded_table.edit_field(nil, :subject) + subject_field.expect_active! subject_field.set_value("Fresh WP\n") diff --git a/spec/features/work_packages/edit_on_assign_version_permission_spec.rb b/spec/features/work_packages/edit_on_assign_version_permission_spec.rb new file mode 100644 index 0000000000..90341f7a96 --- /dev/null +++ b/spec/features/work_packages/edit_on_assign_version_permission_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' +require 'features/page_objects/notification' + +describe 'edit work package', js: true do + let(:current_user) do + FactoryBot.create :user, + firstname: 'Dev', + lastname: 'Guy', + member_in_project: project, + member_with_permissions: permissions + end + let(:permissions) { %i[view_work_packages assign_versions] } + + let(:cf_all) do + FactoryBot.create :work_package_custom_field, is_for_all: true, field_format: 'text' + end + + let(:type) { FactoryBot.create :type, custom_fields: [cf_all] } + let(:project) { FactoryBot.create(:project, types: [type]) } + let(:work_package) do + FactoryBot.create(:work_package, + author: current_user, + project: project, + type: type, + created_at: 5.days.ago.to_date.to_s(:db)) + end + let(:status) { work_package.status } + + let(:wp_page) { Pages::FullWorkPackage.new(work_package) } + let(:version) { FactoryBot.create :version, project: project } + + def visit! + wp_page.visit! + wp_page.ensure_page_loaded + end + + before do + login_as(current_user) + + visit! + end + + context 'as a user having only the assign_versions permission' do + it 'can only change the version' do + wp_page.update_attributes version: version.name + + wp_page.expect_notification(message: 'Successful update') + wp_page.expect_attributes version: version.name + + subject_field = wp_page.work_package_field('subject') + subject_field.expect_read_only + end + end + + context 'as a user having only the edit_work_packages permission' do + let(:permissions) { %i[view_work_packages edit_work_packages] } + + it 'can not change the version' do + version_field = wp_page.work_package_field('version') + version_field.expect_read_only + end + end +end diff --git a/spec/features/work_packages/navigation_spec.rb b/spec/features/work_packages/navigation_spec.rb index 9226a3470a..ebad8a7f5b 100644 --- a/spec/features/work_packages/navigation_spec.rb +++ b/spec/features/work_packages/navigation_spec.rb @@ -173,4 +173,17 @@ RSpec.feature 'Work package navigation', js: true, selenium: true do full_page.ensure_page_loaded end + + scenario 'double clicking my page (Regression #30343)' do + work_package.author = user + work_package.subject = 'Foobar' + work_package.save! + + visit my_page_path + + page.find('.wp-table--cell-td.id a', text: work_package.id).click + + full_page = ::Pages::FullWorkPackage.new work_package, work_package.project + full_page.ensure_page_loaded + end end diff --git a/spec/features/work_packages/new/new_work_package_spec.rb b/spec/features/work_packages/new/new_work_package_spec.rb index d4b0e510d5..e42cd35338 100644 --- a/spec/features/work_packages/new/new_work_package_spec.rb +++ b/spec/features/work_packages/new/new_work_package_spec.rb @@ -52,7 +52,7 @@ describe 'new work package', js: true do def create_work_package_globally(type, project) loading_indicator_saveguard - wp_page.click_add_wp_button + wp_page.click_create_wp_button(type) loading_indicator_saveguard wp_page.subject_field.set(subject) @@ -60,8 +60,6 @@ describe 'new work package', js: true do project_field.openSelectField project_field.set_value project - type_field.openSelectField - type_field.set_value type sleep 1 end @@ -284,17 +282,31 @@ describe 'new work package', js: true do click_on 'Cancel' - wp_page.click_add_wp_button + wp_page.click_create_wp_button type_bug expect(page).to have_no_selector('.ng-value', text: project.name) project_field.openSelectField project_field.set_value project.name - type_field.openSelectField - type_field.set_value type_bug - click_on 'Cancel' end + + context 'with a project without type_bug' do + let!(:project_without_bug) do + FactoryBot.create(:project, name: 'Unrelated project', types: [type_task]) + end + + it 'will not show that value in the project drop down' do + create_work_package_globally(type_bug, project.name) + + sleep 2 + + project_field.openSelectField + + expect(page).to have_selector('.ng-dropdown-panel .ng-option', text: project.name) + expect(page).to have_no_selector('.ng-dropdown-panel .ng-option', text: project_without_bug.name) + end + end end context 'as a user with no permissions' do diff --git a/spec/features/work_packages/new/work_package_default_description_spec.rb b/spec/features/work_packages/new/work_package_default_description_spec.rb index c89590e583..65f4dd3026 100644 --- a/spec/features/work_packages/new/work_package_default_description_spec.rb +++ b/spec/features/work_packages/new/work_package_default_description_spec.rb @@ -21,10 +21,9 @@ describe 'new work package', js: true do let(:notification) { PageObjects::Notifications.new(page) } let(:wp_page) { Pages::FullWorkPackageCreate.new } - # Changing the type changes the description if it was empty or still the default. # Changes in the description shall not be overridden. - def change_type_and_expect_description + def change_type_and_expect_description(set_project: false) type_field.openSelectField type_field.set_value type_task expect(page).to have_selector('.wp-edit-field.description h1', text: 'New Task template') @@ -37,7 +36,7 @@ describe 'new work package', js: true do type_field.openSelectField type_field.set_value type_task - expect(page).not_to have_selector('.wp-edit-field.description h1', text: 'New Task template') + expect(page).to have_no_selector('.wp-edit-field.description h1', text: 'New Task template', wait: 5) description_field.set_value '' @@ -45,13 +44,18 @@ describe 'new work package', js: true do type_field.set_value type_bug expect(page).to have_selector('.wp-edit-field.description h1', text: 'New Bug template') + if set_project + project_field.openSelectField + project_field.set_value project + sleep 1 + end + scroll_to_and_click find('#work-packages--edit-actions-save') wp_page.expect_notification message: 'Successful creation.' expect(page).to have_selector('.wp-edit-field--display-field.description h1', text: 'New Bug template') end - before do login_as(user) end @@ -61,15 +65,9 @@ describe 'new work package', js: true do visit '/work_packages/new' wp_page.expect_fully_loaded - project_field.openSelectField - project_field.set_value project - subject_field.set_value 'Foobar!' - # Wait until project is set - expect(page).to have_no_selector('.wp-project-context--warning') - - change_type_and_expect_description + change_type_and_expect_description set_project: true end end diff --git a/spec/features/work_packages/table/hierarchy/hierarchy_spec.rb b/spec/features/work_packages/table/hierarchy/hierarchy_spec.rb index 009d5cdb06..c44be4ff94 100644 --- a/spec/features/work_packages/table/hierarchy/hierarchy_spec.rb +++ b/spec/features/work_packages/table/hierarchy/hierarchy_spec.rb @@ -209,26 +209,14 @@ describe 'Work Package table hierarchy', js: true do wp_table.expect_work_package_listed(leaf, inter, root) wp_table.expect_work_package_listed(leaf_assigned, inter_assigned, root_assigned) - if OpenProject::Database.mysql? - # MySQL returns empty first before assigned - wp_table.expect_work_package_order( - root, - inter, - leaf, - root_assigned, - inter_assigned, - leaf_assigned - ) - else - wp_table.expect_work_package_order( - root_assigned, - inter_assigned, - leaf_assigned, - root, - inter, - leaf - ) - end + wp_table.expect_work_package_order( + root_assigned, + inter_assigned, + leaf_assigned, + root, + inter, + leaf + ) # Hierarchy should be disabled hierarchy.expect_no_hierarchies @@ -238,34 +226,21 @@ describe 'Work Package table hierarchy', js: true do hierarchy.expect_hierarchy_at(root_assigned, inter) hierarchy.expect_leaf_at(root, leaf, leaf_assigned, inter_assigned) - # When ascending, psql order should be: - # MySQL orders empty values before assigned ones + # When ascending, order should be: # ├──root_assigned # | ├─ inter_assigned # | ├─ inter # | | ├─ leaf_assigned # | | ├─ leaf # ├──root - if OpenProject::Database.mysql? - # MySQL returns empty first before assigned - wp_table.expect_work_package_order( - root, - root_assigned, - inter, - leaf, - leaf_assigned, - inter_assigned - ) - else - wp_table.expect_work_package_order( - root_assigned, - inter_assigned, - inter, - leaf_assigned, - leaf, - root - ) - end + wp_table.expect_work_package_order( + root_assigned, + inter_assigned, + inter, + leaf_assigned, + leaf, + root + ) # Test collapsing of rows hierarchy.toggle_row(root_assigned) @@ -285,50 +260,26 @@ describe 'Work Package table hierarchy', js: true do # | | ├─ leaf # | | ├─ leaf_assigned # | ├─ inter_assigned - if OpenProject::Database.mysql? - # MySQL returns empty first before assigned - wp_table.expect_work_package_order( - root, - root_assigned, - inter, - leaf, - leaf_assigned, - inter_assigned - ) - else - wp_table.expect_work_package_order( - root_assigned, - inter_assigned, - inter, - leaf_assigned, - leaf, - root - ) - end + wp_table.expect_work_package_order( + root_assigned, + inter_assigned, + inter, + leaf_assigned, + leaf, + root + ) # Disable hierarchy mode hierarchy.disable_hierarchy - if OpenProject::Database.mysql? - # MySQL returns empty first before assigned - wp_table.expect_work_package_order( - root, - inter, - leaf, - root_assigned, - inter_assigned, - leaf_assigned - ) - else - wp_table.expect_work_package_order( - root_assigned, - inter_assigned, - leaf_assigned, - root, - inter, - leaf - ) - end + wp_table.expect_work_package_order( + root_assigned, + inter_assigned, + leaf_assigned, + root, + inter, + leaf + ) end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 0b193e8746..e298f00ad0 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -41,7 +41,7 @@ describe ProjectsHelper, type: :helper do User.current = nil end - let(:test_project) { FactoryBot.create :valid_project } + let(:test_project) { FactoryBot.create :valid_project } describe 'a version' do let(:version) { FactoryBot.create :version, project: test_project } @@ -170,4 +170,45 @@ describe ProjectsHelper, type: :helper do end end end + + describe '#projects_level_list_json' do + subject { helper.projects_level_list_json(projects).to_json } + let(:projects) { [] } + + describe 'with no project available' do + it 'renders an empty projects document' do + is_expected.to have_json_size(0).at_path('projects') + end + end + + describe 'with some projects available' do + let(:projects) do + p1 = FactoryBot.build(:project, name: 'P1') + + # a result from Project.project_level_list + [{ project: p1, + level: 0 }, + { project: FactoryBot.build(:project, name: 'P2', parent: p1), + level: 1 }, + { project: FactoryBot.build(:project, name: 'P3'), + level: 0 }] + end + + it 'renders a projects document with the size of 3 of type array' do + is_expected.to have_json_size(3).at_path('projects') + end + + it 'renders all three projects' do + is_expected.to be_json_eql('P1'.to_json).at_path('projects/0/name') + is_expected.to be_json_eql('P2'.to_json).at_path('projects/1/name') + is_expected.to be_json_eql('P3'.to_json).at_path('projects/2/name') + end + + it 'renders the project levels' do + is_expected.to be_json_eql(0.to_json).at_path('projects/0/level') + is_expected.to be_json_eql(1.to_json).at_path('projects/1/level') + is_expected.to be_json_eql(0.to_json).at_path('projects/2/level') + end + end + end end diff --git a/spec/lib/api/v3/memberships/membership_representer_rendering_spec.rb b/spec/lib/api/v3/memberships/membership_representer_rendering_spec.rb index c5bf594f24..c588eea6e1 100644 --- a/spec/lib/api/v3/memberships/membership_representer_rendering_spec.rb +++ b/spec/lib/api/v3/memberships/membership_representer_rendering_spec.rb @@ -33,7 +33,7 @@ describe ::API::V3::Memberships::MembershipRepresenter, 'rendering' do let(:member) do FactoryBot.build_stubbed(:member, - roles: roles, + member_roles: [member_role1, member_role2, marked_member_role], principal: principal, project: project, created_on: Time.current) @@ -41,7 +41,17 @@ describe ::API::V3::Memberships::MembershipRepresenter, 'rendering' do let(:project) { FactoryBot.build_stubbed(:project) } let(:roles) { [role1, role2] } let(:role1) { FactoryBot.build_stubbed(:role) } + let(:member_role1) { FactoryBot.build_stubbed(:member_role, role: role1) } let(:role2) { FactoryBot.build_stubbed(:role) } + let(:member_role2) { FactoryBot.build_stubbed(:member_role, role: role2) } + let(:marked_role) { FactoryBot.build_stubbed(:role) } + let(:marked_member_role) do + FactoryBot.build_stubbed(:member_role, role: marked_role).tap do |mr| + allow(mr) + .to receive(:marked_for_destruction?) + .and_return(true) + end + end let(:principal) { user } let(:user) { FactoryBot.build_stubbed(:user) } let(:group) { FactoryBot.build_stubbed(:group) } @@ -78,6 +88,40 @@ describe ::API::V3::Memberships::MembershipRepresenter, 'rendering' do end end + describe 'to update' do + context 'if manage members permissions are granted' do + it_behaves_like 'has an untitled link' do + let(:link) { 'update' } + let(:href) { api_v3_paths.membership_form(member.id) } + end + end + + describe 'if manage members permissions are lacking' do + let(:permissions) { [] } + + it_behaves_like 'has no link' do + let(:link) { 'update' } + end + end + end + + describe 'to updateImmediately' do + context 'if manage members permissions are granted' do + it_behaves_like 'has an untitled link' do + let(:link) { 'updateImmediately' } + let(:href) { api_v3_paths.membership(member.id) } + end + end + + describe 'if manage members permissions are lacking' do + let(:permissions) { [] } + + it_behaves_like 'has no link' do + let(:link) { 'updateImmediately' } + end + end + end + describe 'project' do it_behaves_like 'has a titled link' do let(:link) { 'project' } @@ -109,6 +153,7 @@ describe ::API::V3::Memberships::MembershipRepresenter, 'rendering' do describe 'roles' do it_behaves_like 'has a link collection' do let(:link) { 'roles' } + # excludes member_roles marked for destruction let(:hrefs) do [ { @@ -190,7 +235,7 @@ describe ::API::V3::Memberships::MembershipRepresenter, 'rendering' do describe 'roles' do let(:embedded_path) { '_embedded/roles' } - it 'has an array of roles embedded' do + it 'has an array of roles embedded that excludes member_roles marked for destruction' do is_expected .to be_json_eql('Role'.to_json) .at_path("#{embedded_path}/0/_type") diff --git a/spec/lib/api/v3/projects/project_representer_spec.rb b/spec/lib/api/v3/projects/project_representer_spec.rb index a0bac32475..4fb1dcd759 100644 --- a/spec/lib/api/v3/projects/project_representer_spec.rb +++ b/spec/lib/api/v3/projects/project_representer_spec.rb @@ -107,7 +107,7 @@ describe ::API::V3::Projects::ProjectRepresenter do describe 'categories' do it 'has the correct link to its categories' do - is_expected.to be_json_eql(api_v3_paths.categories(project.id).to_json) + is_expected.to be_json_eql(api_v3_paths.categories_by_project(project.id).to_json) .at_path('_links/categories/href') end end @@ -127,6 +127,11 @@ describe ::API::V3::Projects::ProjectRepresenter do is_expected.to be_json_eql(api_v3_paths.types_by_project(project.id).to_json) .at_path('_links/types/href') end + + it 'links to the work packages in the project' do + is_expected.to be_json_eql(api_v3_paths.work_packages_by_project(project.id).to_json) + .at_path('_links/workPackages/href') + end end context 'for a user having the manage_types permission' do @@ -144,6 +149,10 @@ describe ::API::V3::Projects::ProjectRepresenter do it 'has no types link' do is_expected.to_not have_json_path('_links/types/href') end + + it 'has no work packages link' do + is_expected.to_not have_json_path('_links/workPackages/href') + end end end end diff --git a/spec/lib/api/v3/queries/schemas/category_filter_dependency_representer_spec.rb b/spec/lib/api/v3/queries/schemas/category_filter_dependency_representer_spec.rb index a3e949b07f..2007900e5a 100644 --- a/spec/lib/api/v3/queries/schemas/category_filter_dependency_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/category_filter_dependency_representer_spec.rb @@ -49,7 +49,7 @@ describe ::API::V3::Queries::Schemas::CategoryFilterDependencyRepresenter, clear describe 'value' do let(:path) { 'values' } let(:type) { '[]Category' } - let(:href) { api_v3_paths.categories(project.identifier) } + let(:href) { api_v3_paths.categories_by_project(project.identifier) } context "for operator 'Queries::Operators::Equals'" do let(:operator) { Queries::Operators::Equals } diff --git a/spec/lib/api/v3/support/link_examples.rb b/spec/lib/api/v3/support/link_examples.rb index 6f18c8383a..6420d253cc 100644 --- a/spec/lib/api/v3/support/link_examples.rb +++ b/spec/lib/api/v3/support/link_examples.rb @@ -65,7 +65,7 @@ shared_examples_for 'action link' do end shared_context 'action link shared' do - let(:all_permissions) { Redmine::AccessControl.permissions.map(&:name) } + let(:all_permissions) { OpenProject::AccessControl.permissions.map(&:name) } let(:permissions) { all_permissions } let(:action_link_user) do defined?(user) ? user : FactoryBot.build_stubbed(:user) diff --git a/spec/lib/api/v3/utilities/path_helper_spec.rb b/spec/lib/api/v3/utilities/path_helper_spec.rb index c8f3d9f9d2..b5ca40ca46 100644 --- a/spec/lib/api/v3/utilities/path_helper_spec.rb +++ b/spec/lib/api/v3/utilities/path_helper_spec.rb @@ -56,410 +56,293 @@ describe ::API::V3::Utilities::PathHelper do it_behaves_like 'path', "/api/v3#{url}" end - describe '#root' do - subject { helper.root } - - it_behaves_like 'api v3 path' - end + shared_examples_for 'index' do |name| + plural_name = name.to_s.pluralize - describe '#activity' do - subject { helper.activity 1 } + describe "##{plural_name}" do + subject { helper.send(plural_name) } - it_behaves_like 'api v3 path', '/activities/1' + it_behaves_like 'api v3 path', "/#{plural_name}" + end end - describe '#attachment' do - subject { helper.attachment 1 } + shared_examples_for 'show' do |name| + describe "##{name}" do + subject { helper.send(:"#{name}", 42) } - it_behaves_like 'api v3 path', '/attachments/1' + it_behaves_like 'api v3 path', "/#{name.to_s.pluralize}/42" + end end - describe '#attachments' do - subject { helper.attachments } + shared_examples_for 'create form' do |name| + describe "#create_#{name}_form" do + subject { helper.send(:"create_#{name}_form") } - it_behaves_like 'api v3 path', '/attachments' + it_behaves_like 'api v3 path', "/#{name.to_s.pluralize}/form" + end end - describe '#attachment_content' do - subject { helper.attachment_content 1 } + shared_examples_for 'update form' do |name| + describe "##{name}_form" do + subject { helper.send(:"#{name}_form", 42) } - it_behaves_like 'api v3 path', '/attachments/1/content' + it_behaves_like 'api v3 path', "/#{name.to_s.pluralize}/42/form" + end end - describe '#attachments_by_post' do - subject { helper.attachments_by_post 1 } + shared_examples_for 'schema' do |name| + describe "##{name}_schema" do + subject { helper.send(:"#{name}_schema") } - it_behaves_like 'api v3 path', '/posts/1/attachments' + it_behaves_like 'api v3 path', "/#{name.to_s.pluralize}/schema" + end end - describe '#attachments_by_work_package' do - subject { helper.attachments_by_work_package 1 } - - it_behaves_like 'api v3 path', '/work_packages/1/attachments' + shared_examples_for 'resource' do |name, except: []| + it_behaves_like('index', name) unless except.include?(:index) + it_behaves_like('show', name) unless except.include?(:show) + it_behaves_like('update form', name) unless except.include?(:update_form) + it_behaves_like('create form', name) unless except.include?(:create_form) + it_behaves_like('schema', name) unless except.include?(:schema) end - describe '#attachments_by_wiki_page' do - subject { helper.attachments_by_wiki_page 1 } + describe '#root' do + subject { helper.root } - it_behaves_like 'api v3 path', '/wiki_pages/1/attachments' + it_behaves_like 'api v3 path' end - describe '#available_assignees' do - subject { helper.available_assignees 42 } - - it_behaves_like 'api v3 path', '/projects/42/available_assignees' + context 'activities paths' do + it_behaves_like 'show', :activity end - describe '#available_responsibles' do - subject { helper.available_responsibles 42 } + context 'attachments paths' do + it_behaves_like 'index', :attachment + it_behaves_like 'show', :attachment - it_behaves_like 'api v3 path', '/projects/42/available_responsibles' - end + describe '#attachment_content' do + subject { helper.attachment_content 1 } - describe '#available_watchers' do - subject { helper.available_watchers 42 } - - it_behaves_like 'api v3 path', '/work_packages/42/available_watchers' - end + it_behaves_like 'api v3 path', '/attachments/1/content' + end - describe '#available_projects_on_edit' do - subject { helper.available_projects_on_edit 42 } + describe '#attachments_by_post' do + subject { helper.attachments_by_post 1 } - it_behaves_like 'api v3 path', '/work_packages/42/available_projects' - end + it_behaves_like 'api v3 path', '/posts/1/attachments' + end - describe '#available_projects_on_create' do - subject { helper.available_projects_on_create } + describe '#attachments_by_work_package' do + subject { helper.attachments_by_work_package 1 } - it_behaves_like 'api v3 path', '/work_packages/available_projects' - end + it_behaves_like 'api v3 path', '/work_packages/1/attachments' + end - describe '#categories' do - subject { helper.categories 42 } + describe '#attachments_by_wiki_page' do + subject { helper.attachments_by_wiki_page 1 } - it_behaves_like 'api v3 path', '/projects/42/categories' + it_behaves_like 'api v3 path', '/wiki_pages/1/attachments' + end end - describe '#category' do - subject { helper.category 42 } + context 'category paths' do + it_behaves_like 'index', :category + it_behaves_like 'show', :category - it_behaves_like 'api v3 path', '/categories/42' + describe '#categories_by_project' do + subject { helper.categories_by_project 42 } + + it_behaves_like 'api v3 path', '/projects/42/categories' + end end - describe '#configuration' do - subject { helper.configuration } + context 'configuration paths' do + describe '#configuration' do + subject { helper.configuration } - it_behaves_like 'api v3 path', '/configuration' + it_behaves_like 'api v3 path', '/configuration' + end end context 'custom action paths' do - describe '#custom_action' do - subject { helper.custom_action 42 } - - it_behaves_like 'api v3 path', '/custom_actions/42' - end + it_behaves_like 'show', :custom_action describe '#custom_action_execute' do subject { helper.custom_action_execute 42 } it_behaves_like 'api v3 path', '/custom_actions/42/execute' end - end - - describe '#custom_option' do - subject { helper.custom_option 42 } - it_behaves_like 'api v3 path', '/custom_options/42' + it_behaves_like 'show', :custom_option end - describe '#create_work_package_form' do - subject { helper.create_work_package_form } + describe 'memberships paths' do + it_behaves_like 'resource', :membership - it_behaves_like 'api v3 path', '/work_packages/form' - end - - describe '#create_project_work_package_form' do - subject { helper.create_project_work_package_form 42 } - - it_behaves_like 'api v3 path', '/projects/42/work_packages/form' - end - - describe '#grids' do - subject { helper.grids } - - it_behaves_like 'api v3 path', '/grids' - end - - describe '#create_grid_form' do - subject { helper.create_grid_form } + describe '#memberships_available_projects' do + subject { helper.memberships_available_projects } - it_behaves_like 'api v3 path', '/grids/form' - end - - describe '#grid_schema' do - subject { helper.grid_schema } - - it_behaves_like 'api v3 path', '/grids/schema' - end - - describe '#grid' do - subject { helper.grid(42) } - - it_behaves_like 'api v3 path', '/grids/42' + it_behaves_like 'api v3 path', '/memberships/available_projects' + end end - describe '#grid_form' do - subject { helper.grid_form(42) } - - it_behaves_like 'api v3 path', '/grids/42/form' + describe 'messages paths' do + it_behaves_like 'index', :message + it_behaves_like 'show', :message end - describe '#memberships' do - subject { helper.memberships } + describe 'my paths' do + describe '#my_preferences' do + subject { helper.my_preferences } - it_behaves_like 'api v3 path', '/memberships' + it_behaves_like 'api v3 path', '/my_preferences' + end end - describe '#memberships_available_projects' do - subject { helper.memberships_available_projects } - - it_behaves_like 'api v3 path', '/memberships/available_projects' - end + describe 'news paths' do + describe '#newses' do + subject { helper.newses } - describe '#membership' do - subject { helper.membership(42) } + it_behaves_like 'api v3 path', '/news' + end - it_behaves_like 'api v3 path', '/memberships/42' + it_behaves_like 'show', :news end - describe '#membership_schema' do - subject { helper.membership_schema } + describe 'markup paths' do + describe '#render_markup' do + subject { helper.render_markup(link: 'link-ish') } - it_behaves_like 'api v3 path', '/memberships/schema' - end + it_behaves_like 'api v3 path', '/render/markdown?context=link-ish' - describe '#version_memberships_form' do - subject { helper.create_memberships_form } + context 'no link given' do + subject { helper.render_markup } - it_behaves_like 'api v3 path', '/memberships/form' + it { is_expected.to eql('/api/v3/render/markdown') } + end + end end - describe '#message' do - subject { helper.message(42) } - - it_behaves_like 'api v3 path', '/messages/42' + describe 'posts paths' do + it_behaves_like 'index', :post + it_behaves_like 'show', :post end - describe '#my_preferences' do - subject { helper.my_preferences } - - it_behaves_like 'api v3 path', '/my_preferences' + describe 'principals paths' do + it_behaves_like 'index', :principals end - describe '#newses' do - subject { helper.newses } - - it_behaves_like 'api v3 path', '/news' + describe 'priorities paths' do + it_behaves_like 'index', :priority + it_behaves_like 'show', :priority end - describe '#news' do - subject { helper.news(42) } - - it_behaves_like 'api v3 path', '/news/42' + describe 'projects paths' do + it_behaves_like 'index', :project + it_behaves_like 'show', :project end - describe '#render_markup' do - subject { helper.render_markup(link: 'link-ish') } - - it_behaves_like 'api v3 path', '/render/markdown?context=link-ish' + describe 'query paths' do + it_behaves_like 'resource', :query - context 'no link given' do - subject { helper.render_markup } + describe '#query_default' do + subject { helper.query_default } - it { is_expected.to eql('/api/v3/render/markdown') } + it_behaves_like 'api v3 path', '/queries/default' end - end - - describe '#post' do - subject { helper.post 1 } - - it_behaves_like 'api v3 path', '/posts/1' - end - describe '#principals' do - subject { helper.principals } - - it_behaves_like 'api v3 path', '/principals' - end - - describe 'priorities paths' do - describe '#priorities' do - subject { helper.priorities } + describe '#query_project_default' do + subject { helper.query_project_default(42) } - it_behaves_like 'api v3 path', '/priorities' + it_behaves_like 'api v3 path', '/projects/42/queries/default' end - describe '#priority' do - subject { helper.priority 1 } + describe '#query_star' do + subject { helper.query_star 1 } - it_behaves_like 'api v3 path', '/priorities/1' + it_behaves_like 'api v3 path', '/queries/1/star' end - end - describe 'projects paths' do - describe '#projects' do - subject { helper.projects } + describe '#query_unstar' do + subject { helper.query_unstar 1 } - it_behaves_like 'api v3 path', '/projects' + it_behaves_like 'api v3 path', '/queries/1/unstar' end - describe '#project' do - subject { helper.project 1 } + describe '#query_column' do + subject { helper.query_column 'updated_on' } - it_behaves_like 'api v3 path', '/projects/1' + it_behaves_like 'api v3 path', '/queries/columns/updated_on' end - end - - describe '#queries' do - subject { helper.queries } - - it_behaves_like 'api v3 path', '/queries' - end - - describe '#query' do - subject { helper.query 1 } - - it_behaves_like 'api v3 path', '/queries/1' - end - - describe '#query_default' do - subject { helper.query_default } - - it_behaves_like 'api v3 path', '/queries/default' - end - - describe '#query_project_default' do - subject { helper.query_project_default(42) } - - it_behaves_like 'api v3 path', '/projects/42/queries/default' - end - - describe '#create_query_form' do - subject { helper.create_query_form } - - it_behaves_like 'api v3 path', '/queries/form' - end - - describe '#query_form' do - subject { helper.query_form(42) } - - it_behaves_like 'api v3 path', '/queries/42/form' - end - - describe '#query_star' do - subject { helper.query_star 1 } - it_behaves_like 'api v3 path', '/queries/1/star' - end - - describe '#query_unstar' do - subject { helper.query_unstar 1 } - - it_behaves_like 'api v3 path', '/queries/1/unstar' - end - - describe '#query_column' do - subject { helper.query_column 'updated_on' } - - it_behaves_like 'api v3 path', '/queries/columns/updated_on' - end - - describe '#query_group_by' do - subject { helper.query_group_by 'status' } + describe '#query_group_by' do + subject { helper.query_group_by 'status' } - it_behaves_like 'api v3 path', '/queries/group_bys/status' - end - - describe '#query_sort_by' do - subject { helper.query_sort_by 'status', 'desc' } - - it_behaves_like 'api v3 path', '/queries/sort_bys/status-desc' - end + it_behaves_like 'api v3 path', '/queries/group_bys/status' + end - describe '#query_filter' do - subject { helper.query_filter 'status' } + describe '#query_sort_by' do + subject { helper.query_sort_by 'status', 'desc' } - it_behaves_like 'api v3 path', '/queries/filters/status' - end + it_behaves_like 'api v3 path', '/queries/sort_bys/status-desc' + end - describe '#query_filter_instance_schemas' do - subject { helper.query_filter_instance_schemas } + describe '#query_filter' do + subject { helper.query_filter 'status' } - it_behaves_like 'api v3 path', '/queries/filter_instance_schemas' - end + it_behaves_like 'api v3 path', '/queries/filters/status' + end - describe '#query_filter_instance_schema' do - subject { helper.query_filter_instance_schema('bogus') } + describe '#query_filter_instance_schemas' do + subject { helper.query_filter_instance_schemas } - it_behaves_like 'api v3 path', '/queries/filter_instance_schemas/bogus' - end + it_behaves_like 'api v3 path', '/queries/filter_instance_schemas' + end - describe '#query_project_form' do - subject { helper.query_project_form(42) } + describe '#query_filter_instance_schema' do + subject { helper.query_filter_instance_schema('bogus') } - it_behaves_like 'api v3 path', '/projects/42/queries/form' - end + it_behaves_like 'api v3 path', '/queries/filter_instance_schemas/bogus' + end - describe '#query_project_filter_instance_schemas' do - subject { helper.query_project_filter_instance_schemas(42) } + describe '#query_project_form' do + subject { helper.query_project_form(42) } - it_behaves_like 'api v3 path', '/projects/42/queries/filter_instance_schemas' - end + it_behaves_like 'api v3 path', '/projects/42/queries/form' + end - describe '#query_operator' do - subject { helper.query_operator '=' } + describe '#query_project_filter_instance_schemas' do + subject { helper.query_project_filter_instance_schemas(42) } - it_behaves_like 'api v3 path', '/queries/operators/=' - end + it_behaves_like 'api v3 path', '/projects/42/queries/filter_instance_schemas' + end - describe '#query_schema' do - subject { helper.query_schema } + describe '#query_operator' do + subject { helper.query_operator '=' } - it_behaves_like 'api v3 path', '/queries/schema' - end + it_behaves_like 'api v3 path', '/queries/operators/=' + end - describe '#query_project_schema' do - subject { helper.query_project_schema('42') } + describe '#query_project_schema' do + subject { helper.query_project_schema('42') } - it_behaves_like 'api v3 path', '/projects/42/queries/schema' - end + it_behaves_like 'api v3 path', '/projects/42/queries/schema' + end - describe '#query_available_projects' do - subject { helper.query_available_projects } + describe '#query_available_projects' do + subject { helper.query_available_projects } - it_behaves_like 'api v3 path', '/queries/available_projects' + it_behaves_like 'api v3 path', '/queries/available_projects' + end end describe 'relations paths' do - describe '#relation' do - subject { helper.relation 1 } - - it_behaves_like 'api v3 path', '/relations' - end - - describe '#relation' do - subject { helper.relation 1 } - - it_behaves_like 'api v3 path', '/relations/1' - end + it_behaves_like 'index', :relation + it_behaves_like 'show', :relation end describe 'revisions paths' do - describe '#revision' do - subject { helper.revision 1 } - - it_behaves_like 'api v3 path', '/revisions/1' - end + it_behaves_like 'show', :revision describe '#show_revision' do subject { helper.show_revision 'foo', 1234 } @@ -468,61 +351,18 @@ describe ::API::V3::Utilities::PathHelper do end end - describe '#roles' do - subject { helper.roles } - - it_behaves_like 'api v3 path', '/roles' - end - - describe '#role' do - subject { helper.role 12 } - - it_behaves_like 'api v3 path', '/roles/12' - end - - describe 'schemas paths' do - describe '#work_package_schema' do - subject { helper.work_package_schema 1, 2 } - - it_behaves_like 'api v3 path', '/work_packages/schemas/1-2' - end - - describe '#work_package_schemas' do - subject { helper.work_package_schemas } - - it_behaves_like 'api v3 path', '/work_packages/schemas' - end - - describe '#work_package_schemas with filters' do - subject { helper.work_package_schemas [1, 2], [3, 4] } - - def self.filter - CGI.escape([{ id: { operator: '=', values: ['1-2', '3-4'] } }].to_s) - end - - it_behaves_like 'api v3 path', - "/work_packages/schemas?filters=#{filter}" - end - - describe '#work_package_sums_schema' do - subject { helper.work_package_sums_schema } - - it_behaves_like 'api v3 path', '/work_packages/schemas/sums' - end + describe 'roles paths' do + it_behaves_like 'index', :role + it_behaves_like 'show', :role end describe 'statuses paths' do - describe '#statuses' do - subject { helper.statuses } - - it_behaves_like 'api v3 path', '/statuses' - end - - describe '#status' do - subject { helper.status 1 } + it_behaves_like 'index', :status + it_behaves_like 'show', :status + end - it_behaves_like 'api v3 path', '/statuses/1' - end + describe 'grids paths' do + it_behaves_like 'resource', :grid end describe 'string object paths' do @@ -536,26 +376,15 @@ describe ::API::V3::Utilities::PathHelper do expect(helper.string_object(value)).to eql('/api/v3/string_objects?value=foo%2Fbar%20baz') end end + end - describe '#status' do - subject { helper.status 1 } - - it_behaves_like 'api v3 path', '/statuses/1' - end + context 'status paths' do + it_behaves_like 'show', :status end context 'time_entry paths' do - describe '.time_entries' do - subject { helper.time_entries } - - it_behaves_like 'api v3 path', '/time_entries' - end - - describe '.time_entry' do - subject { helper.time_entry 42 } - - it_behaves_like 'api v3 path', '/time_entries/42' - end + it_behaves_like 'index', :time_entry + it_behaves_like 'show', :time_entry describe '.time_entries_activity' do subject { helper.time_entries_activity 42 } @@ -565,37 +394,19 @@ describe ::API::V3::Utilities::PathHelper do end describe 'types paths' do - describe '#types' do - subject { helper.types } - - it_behaves_like 'api v3 path', '/types' - end + it_behaves_like 'index', :type + it_behaves_like 'show', :type describe '#types_by_project' do subject { helper.types_by_project 12 } it_behaves_like 'api v3 path', '/projects/12/types' end - - describe '#type' do - subject { helper.type 1 } - - it_behaves_like 'api v3 path', '/types/1' - end end describe 'users paths' do - describe '#users' do - subject { helper.users } - - it_behaves_like 'api v3 path', '/users' - end - - describe '#user' do - subject { helper.user 1 } - - it_behaves_like 'api v3 path', '/users/1' - end + it_behaves_like 'index', :user + it_behaves_like 'show', :user end describe 'group paths' do @@ -606,80 +417,34 @@ describe ::API::V3::Utilities::PathHelper do end end - describe '#version' do - subject { helper.version 42 } - - it_behaves_like 'api v3 path', '/versions/42' - end - - describe '#version_form' do - subject { helper.version_form(42) } - - it_behaves_like 'api v3 path', '/versions/42/form' - end - - describe '#versions' do - subject { helper.versions } - - it_behaves_like 'api v3 path', '/versions' - end - - describe '#versions_available_projects' do - subject { helper.versions_available_projects } - - it_behaves_like 'api v3 path', '/versions/available_projects' - end - - describe '#versions_by_project' do - subject { helper.versions_by_project 42 } - - it_behaves_like 'api v3 path', '/projects/42/versions' - end - - describe '#projects_by_version' do - subject { helper.projects_by_version 42 } + describe 'version paths' do + it_behaves_like 'resource', :version - it_behaves_like 'api v3 path', '/versions/42/projects' - end - - describe '#version_schema' do - subject { helper.version_schema } + describe '#versions_available_projects' do + subject { helper.versions_available_projects } - it_behaves_like 'api v3 path', '/versions/schema' - end + it_behaves_like 'api v3 path', '/versions/available_projects' + end - describe '#version_create_form' do - subject { helper.create_version_form } + describe '#versions_by_project' do + subject { helper.versions_by_project 42 } - it_behaves_like 'api v3 path', '/versions/form' - end + it_behaves_like 'api v3 path', '/projects/42/versions' + end - describe '#work_packages_by_project' do - subject { helper.work_packages_by_project 42 } + describe '#projects_by_version' do + subject { helper.projects_by_version 42 } - it_behaves_like 'api v3 path', '/projects/42/work_packages' + it_behaves_like 'api v3 path', '/versions/42/projects' + end end describe 'wiki pages paths' do - describe '#wiki_page' do - subject { helper.wiki_page 1 } - - it_behaves_like 'api v3 path', '/wiki_pages/1' - end + it_behaves_like 'show', :wiki_page end describe 'work packages paths' do - describe '#work_packages' do - subject { helper.work_packages } - - it_behaves_like 'api v3 path', '/work_packages' - end - - describe '#work_package' do - subject { helper.work_package 1 } - - it_behaves_like 'api v3 path', '/work_packages/1' - end + it_behaves_like 'resource', :work_package, except: [:schema] describe '#work_package_activities' do subject { helper.work_package_activities 42 } @@ -705,22 +470,97 @@ describe ::API::V3::Utilities::PathHelper do it_behaves_like 'api v3 path', '/work_packages/42/revisions' end - describe '#work_package_form' do - subject { helper.work_package_form 1 } - - it_behaves_like 'api v3 path', '/work_packages/1/form' - end - describe '#work_package_watchers' do subject { helper.work_package_watchers 1 } it_behaves_like 'api v3 path', '/work_packages/1/watchers' end + describe '#work_packages_by_project' do + subject { helper.work_packages_by_project 42 } + + it_behaves_like 'api v3 path', '/projects/42/work_packages' + end + + describe '#create_project_work_package_form' do + subject { helper.create_project_work_package_form 42 } + + it_behaves_like 'api v3 path', '/projects/42/work_packages/form' + end + describe '#watcher' do subject { helper.watcher 1, 42 } it_behaves_like 'api v3 path', '/work_packages/42/watchers/1' end + + describe 'available ... paths' do + describe '#available_assignees' do + subject { helper.available_assignees 42 } + + it_behaves_like 'api v3 path', '/projects/42/available_assignees' + end + + describe '#available_responsibles' do + subject { helper.available_responsibles 42 } + + it_behaves_like 'api v3 path', '/projects/42/available_responsibles' + end + + describe '#available_watchers' do + subject { helper.available_watchers 42 } + + it_behaves_like 'api v3 path', '/work_packages/42/available_watchers' + end + + describe '#available_projects_on_edit' do + subject { helper.available_projects_on_edit 42 } + + it_behaves_like 'api v3 path', '/work_packages/42/available_projects' + end + + describe '#available_projects_on_create' do + subject { helper.available_projects_on_create(nil) } + + it_behaves_like 'api v3 path', '/work_packages/available_projects' + end + + describe '#available_projects_on_create with type' do + subject { helper.available_projects_on_create(1) } + + it_behaves_like 'api v3 path', '/work_packages/available_projects?for_type=1' + end + end + + describe 'schemas paths' do + describe '#work_package_schema' do + subject { helper.work_package_schema 1, 2 } + + it_behaves_like 'api v3 path', '/work_packages/schemas/1-2' + end + + describe '#work_package_schemas' do + subject { helper.work_package_schemas } + + it_behaves_like 'api v3 path', '/work_packages/schemas' + end + + describe '#work_package_schemas with filters' do + subject { helper.work_package_schemas [1, 2], [3, 4] } + + def self.filter + CGI.escape([{ id: { operator: '=', values: ['1-2', '3-4'] } }].to_s) + end + + it_behaves_like 'api v3 path', + "/work_packages/schemas?filters=#{filter}" + end + + describe '#work_package_sums_schema' do + subject { helper.work_package_sums_schema } + + it_behaves_like 'api v3 path', '/work_packages/schemas/sums' + end + end end end diff --git a/spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb b/spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb index ce68f5a724..d8379ccfbe 100644 --- a/spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb +++ b/spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb @@ -31,12 +31,22 @@ require 'spec_helper' describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do let(:project) { FactoryBot.build_stubbed(:project) } let(:type) { FactoryBot.build_stubbed(:type) } - let(:work_package) { + let(:work_package) do FactoryBot.build_stubbed(:work_package, - project: project, - type: type) - } - let(:current_user) { double('current user') } + project: project, + type: type) + end + let(:current_user) do + double('current user').tap do |u| + allow(u) + .to receive(:allowed_to?) + .and_return(true) + end + end + + before do + login_as(current_user) + end subject { described_class.new(work_package: work_package) } @@ -103,20 +113,20 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do end context 'changed work package' do - let(:work_package) { + let(:work_package) do double('original work package', id: double, clone: cloned_wp, status: double('wrong status'), persisted?: true).as_null_object - } - let(:cloned_wp) { + end + let(:cloned_wp) do double('cloned work package', new_statuses_allowed_to: status_result) - } - let(:stored_status) { + end + let(:stored_status) do double('good status') - } + end before do allow(work_package).to receive(:persisted?).and_return(true) @@ -143,11 +153,11 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do end describe '#assignable_types' do - let(:result) { + let(:result) do result = double allow(result).to receive(:includes).and_return(result) result - } + end it 'calls through to the project' do expect(project).to receive(:types).and_return(result) diff --git a/spec/lib/api/v3/work_packages/schema/typed_work_package_schema_spec.rb b/spec/lib/api/v3/work_packages/schema/typed_work_package_schema_spec.rb index 1f80addbe1..0bae20cd84 100644 --- a/spec/lib/api/v3/work_packages/schema/typed_work_package_schema_spec.rb +++ b/spec/lib/api/v3/work_packages/schema/typed_work_package_schema_spec.rb @@ -32,9 +32,19 @@ describe ::API::V3::WorkPackages::Schema::TypedWorkPackageSchema do let(:project) { FactoryBot.build(:project) } let(:type) { FactoryBot.build(:type) } - let(:current_user) { double } + let(:current_user) do + double('user').tap do |u| + allow(u) + .to receive(:allowed_to?) + .and_return(true) + end + end subject { described_class.new(project: project, type: type) } + before do + login_as(current_user) + end + it 'has the project set' do expect(subject.project).to eql(project) end diff --git a/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb b/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb index a25ce9fb29..a66900d178 100644 --- a/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb @@ -42,8 +42,14 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do end end let(:current_user) do - FactoryBot.build_stubbed(:user) + FactoryBot.build_stubbed(:user).tap do |user| + allow(user) + .to receive(:allowed_to?) do |per, pro| + project == pro && permissions.include?(per) + end + end end + let(:permissions) { [:edit_work_packages] } let(:attribute_query) do FactoryBot.build_stubbed(:query).tap do |query| query.filters.clear @@ -83,6 +89,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do let(:available_custom_fields) { [] } before do + login_as(current_user) allow(schema).to receive(:writable?).and_call_original end @@ -557,12 +564,28 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do end describe 'project' do - it_behaves_like 'has basic schema properties' do - let(:path) { 'project' } - let(:type) { 'Project' } - let(:name) { I18n.t('attributes.project') } - let(:required) { true } - let(:writable) { true } + context 'if having the move_work_packages permission' do + let(:permissions) { [:move_work_packages] } + + it_behaves_like 'has basic schema properties' do + let(:path) { 'project' } + let(:type) { 'Project' } + let(:name) { I18n.t('attributes.project') } + let(:required) { true } + let(:writable) { true } + end + end + + context 'if having the edit_work_packages permission' do + let(:permissions) { [:edit_work_packages] } + + it_behaves_like 'has basic schema properties' do + let(:path) { 'project' } + let(:type) { 'Project' } + let(:name) { I18n.t('attributes.project') } + let(:required) { true } + let(:writable) { false } + end end context 'when updating' do @@ -583,7 +606,22 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do it_behaves_like 'links to allowed values via collection link' do let(:path) { 'project' } - let(:href) { api_v3_paths.available_projects_on_create } + let(:href) { api_v3_paths.available_projects_on_create(wp_type.id) } + end + end + + context 'when creating (new_record with empty type)' do + let(:work_package) do + FactoryBot.build(:stubbed_work_package, project: project, type: nil) do |wp| + allow(wp) + .to receive(:available_custom_fields) + .and_return(available_custom_fields) + end + end + + it_behaves_like 'links to allowed values via collection link' do + let(:path) { 'project' } + let(:href) { api_v3_paths.available_projects_on_create(nil) } end end @@ -656,18 +694,34 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do end describe 'versions' do - it_behaves_like 'has basic schema properties' do - let(:path) { 'version' } - let(:type) { 'Version' } - let(:name) { I18n.t('activerecord.attributes.work_package.fixed_version') } - let(:required) { false } - let(:writable) { true } + context 'if having the assign_versions permission' do + let(:permissions) { [:assign_versions] } + + it_behaves_like 'has basic schema properties' do + let(:path) { 'version' } + let(:type) { 'Version' } + let(:name) { I18n.t('activerecord.attributes.work_package.fixed_version') } + let(:required) { false } + let(:writable) { true } + end + + it_behaves_like 'has a collection of allowed values' do + let(:json_path) { 'version' } + let(:href_path) { 'versions' } + let(:factory) { :version } + end end - it_behaves_like 'has a collection of allowed values' do - let(:json_path) { 'version' } - let(:href_path) { 'versions' } - let(:factory) { :version } + context 'if having the edit_work_packages permission' do + let(:permissions) { [:edit_work_packages] } + + it_behaves_like 'has basic schema properties' do + let(:path) { 'version' } + let(:type) { 'Version' } + let(:name) { I18n.t('activerecord.attributes.work_package.fixed_version') } + let(:required) { false } + let(:writable) { false } + end end end diff --git a/spec/lib/api/v3/work_packages/work_package_payload_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_payload_representer_spec.rb index 5a5f378faf..0dea516223 100644 --- a/spec/lib/api/v3/work_packages/work_package_payload_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_payload_representer_spec.rb @@ -45,7 +45,11 @@ describe ::API::V3::WorkPackages::WorkPackagePayloadRepresenter do end let(:user) do - FactoryBot.build_stubbed(:user) + FactoryBot.build_stubbed(:user) do |u| + allow(u) + .to receive(:allowed_to?) + .and_return(true) + end end let(:representer) do @@ -56,7 +60,9 @@ describe ::API::V3::WorkPackages::WorkPackagePayloadRepresenter do let(:available_custom_fields) { [] } before do - allow(work_package).to receive(:lock_version).and_return(1) + allow(work_package) + .to receive(:lock_version) + .and_return(1) end context 'generation' do @@ -305,12 +311,12 @@ describe ::API::V3::WorkPackages::WorkPackagePayloadRepresenter do end describe 'assignee and responsible' do - let(:user) { FactoryBot.build_stubbed(:user) } - let(:link) { "/api/v3/users/#{user.id}" } + let(:other_user) { FactoryBot.build_stubbed(:user) } + let(:link) { "/api/v3/users/#{other_user.id}" } describe 'assignee' do before do - work_package.assigned_to = user + work_package.assigned_to = other_user end it_behaves_like 'linked property' do @@ -322,7 +328,7 @@ describe ::API::V3::WorkPackages::WorkPackagePayloadRepresenter do describe 'responsible' do before do - work_package.responsible = user + work_package.responsible = other_user end it_behaves_like 'linked property' do diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb index 5a3fb1e7c2..50513e45d9 100644 --- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -342,6 +342,20 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do let(:link) { 'updateImmediately' } end end + + context 'user is lacks edit permission but has assign_versions' do + let(:permissions) { all_permissions - [:edit_work_packages] + [:assign_versions] } + + it_behaves_like 'has an untitled link' do + let(:link) { 'update' } + let(:href) { api_v3_paths.work_package_form(work_package.id) } + end + + it_behaves_like 'has an untitled link' do + let(:link) { 'updateImmediately' } + let(:href) { api_v3_paths.work_package(work_package.id) } + end + end end describe 'status' do diff --git a/spec/lib/database_spec.rb b/spec/lib/database_spec.rb index eb6f25ca03..0c31decb8e 100644 --- a/spec/lib/database_spec.rb +++ b/spec/lib/database_spec.rb @@ -62,7 +62,6 @@ describe OpenProject::Database do it 'should be able to use the helper methods' do allow(OpenProject::Database).to receive(:adapter_name).and_return 'PostgresQL' - expect(OpenProject::Database.mysql?).to equal(false) expect(OpenProject::Database.postgresql?).to equal(true) end @@ -74,12 +73,4 @@ describe OpenProject::Database do expect(OpenProject::Database.version).to eq('8.3.11') expect(OpenProject::Database.version(true)).to eq(raw_version) end - - it 'should return a version string for MySQL' do - allow(OpenProject::Database).to receive(:adapter_name).and_return 'MySQL' - allow(ActiveRecord::Base.connection).to receive(:select_value).and_return '5.1.2' - - expect(OpenProject::Database.version).to eq('5.1.2') - expect(OpenProject::Database.version(true)).to eq('5.1.2') - end end diff --git a/spec/lib/open_project/access_control/permission_spec.rb b/spec/lib/open_project/access_control/permission_spec.rb new file mode 100644 index 0000000000..ff9730f3e9 --- /dev/null +++ b/spec/lib/open_project/access_control/permission_spec.rb @@ -0,0 +1,51 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe OpenProject::AccessControl::Permission do + describe '#dependencies' do + context 'for a permission with a dependency' do + subject { OpenProject::AccessControl.permission(:edit_work_packages) } + + it 'denotes the prerequiresites' do + expect(subject.dependencies) + .to match_array([:view_work_packages]) + end + end + + context 'for a permission without a dependency' do + subject { OpenProject::AccessControl.permission(:view_work_packages) } + + it 'is empty' do + expect(subject.dependencies) + .to be_empty + end + end + end +end diff --git a/spec/lib/open_project/access_control_spec.rb b/spec/lib/open_project/access_control_spec.rb new file mode 100644 index 0000000000..da8a74749c --- /dev/null +++ b/spec/lib/open_project/access_control_spec.rb @@ -0,0 +1,145 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +describe OpenProject::AccessControl do + describe '.remove_modules_permissions' do + let!(:all_former_permissions) { OpenProject::AccessControl.permissions } + let!(:former_repository_permissions) do + module_permissions = OpenProject::AccessControl.modules_permissions(['repository']) + + module_permissions.select do |permission| + permission.project_module == :repository + end + end + + subject { OpenProject::AccessControl } + + before do + OpenProject::AccessControl.remove_modules_permissions(:repository) + end + + after do + raise 'Test outdated' unless OpenProject::AccessControl.instance_variable_defined?(:@permissions) + OpenProject::AccessControl.instance_variable_set(:@permissions, all_former_permissions) + OpenProject::AccessControl.clear_caches + end + + it 'removes from global permissions' do + expect(subject.permissions).not_to include(former_repository_permissions) + end + + it 'removes from public permissions' do + expect(subject.public_permissions).not_to include(former_repository_permissions) + end + + it 'removes from members only permissions' do + expect(subject.members_only_permissions).not_to include(former_repository_permissions) + end + + it 'removes from loggedin only permissions' do + expect(subject.loggedin_only_permissions).not_to include(former_repository_permissions) + end + + it 'should disable repository module' do + expect(subject.available_project_modules).not_to include(:repository) + end + end + + describe '#permissions' do + it 'is an array of permissions' do + expect(described_class.permissions.all? { |p| p.is_a?(OpenProject::AccessControl::Permission) }) + .to be_truthy + end + end + + describe '#permission' do + context 'for a project module permission' do + subject { described_class.permission(:view_work_packages) } + + it 'is a permission' do + is_expected + .to be_a(OpenProject::AccessControl::Permission) + end + + it 'is the permission with the queried for name' do + expect(subject.name) + .to eql(:view_work_packages) + end + + it 'belongs to a project module' do + expect(subject.project_module) + .to eql(:work_package_tracking) + end + end + + context 'for a non module permission' do + subject { described_class.permission(:edit_project) } + + it 'is a permission' do + is_expected + .to be_a(OpenProject::AccessControl::Permission) + end + + it 'is the permission with the queried for name' do + expect(subject.name) + .to eql(:edit_project) + end + + it 'belongs to a project module' do + expect(subject.project_module) + .to be_nil + end + + it 'includes actions' do + expect(subject.actions) + .to include('project_settings/show') + end + end + end + + describe '#dependencies' do + context 'for a permission with a prerequisite' do + subject { described_class.permission(:edit_work_packages) } + + it 'denotes the prerequiresites' do + expect(subject.dependencies) + .to match_array([:view_work_packages]) + end + end + + context 'for a permission without a prerequisite' do + subject { described_class.permission(:view_work_packages) } + + it 'denotes the prerequiresites' do + expect(subject.dependencies) + .to be_empty + end + end + end +end diff --git a/spec/lib/open_project/macros/include_wiki_page_macro_spec.rb b/spec/lib/open_project/macros/include_wiki_page_macro_spec.rb index ef271405b1..79f52a5508 100644 --- a/spec/lib/open_project/macros/include_wiki_page_macro_spec.rb +++ b/spec/lib/open_project/macros/include_wiki_page_macro_spec.rb @@ -114,9 +114,8 @@ describe 'OpenProject include wiki page macro' do is_expected.to be_html_eql('

-

-

included from same project

@@ -177,9 +176,8 @@ describe 'OpenProject include wiki page macro' do is_expected.to be_html_eql('

-

-

Included from other project

diff --git a/spec/lib/open_project/plugins/module_handler_spec.rb b/spec/lib/open_project/plugins/module_handler_spec.rb index be373657d4..cfb01fce44 100644 --- a/spec/lib/open_project/plugins/module_handler_spec.rb +++ b/spec/lib/open_project/plugins/module_handler_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' describe OpenProject::Plugins::ModuleHandler do - let!(:all_former_permissions) { Redmine::AccessControl.permissions } + let!(:all_former_permissions) { OpenProject::AccessControl.permissions } before do disabled_modules = OpenProject::Plugins::ModuleHandler.disable_modules('repository') @@ -36,14 +36,14 @@ describe OpenProject::Plugins::ModuleHandler do end after do - raise 'Test outdated' unless Redmine::AccessControl.instance_variable_defined?(:@permissions) - Redmine::AccessControl.instance_variable_set(:@permissions, all_former_permissions) - Redmine::AccessControl.clear_caches + raise 'Test outdated' unless OpenProject::AccessControl.instance_variable_defined?(:@permissions) + OpenProject::AccessControl.instance_variable_set(:@permissions, all_former_permissions) + OpenProject::AccessControl.clear_caches end context '#disable' do it 'should disable repository module' do - expect(Redmine::AccessControl.available_project_modules).not_to include(:repository) + expect(OpenProject::AccessControl.available_project_modules).not_to include(:repository) end end end diff --git a/spec/lib/open_project/text_formatting/markdown/markdown_formatting_spec.rb b/spec/lib/open_project/text_formatting/markdown/markdown_formatting_spec.rb index 9570907dba..0b547fce9e 100644 --- a/spec/lib/open_project/text_formatting/markdown/markdown_formatting_spec.rb +++ b/spec/lib/open_project/text_formatting/markdown/markdown_formatting_spec.rb @@ -58,9 +58,8 @@ describe OpenProject::TextFormatting::Formats::Markdown::Formatter do it 'should use of backslashes followed by numbers in headers' do html = <<-HTML.strip_heredoc -

-

2009\\02\\09

@@ -279,51 +278,49 @@ describe OpenProject::TextFormatting::Formats::Markdown::Formatter do html = <<-HTML.strip_heredoc

Table of contents

-
    +

    -

    -

    The first h1 heading

    Some text after the first h1 heading

    -

    -

    The first h2 heading

    Some text after the first h2 heading

    -

    -

    The first h3 heading

    Some text after the first h3 heading

    -

    -

    The second h1 heading

    Some text after the second h1 heading

    -

    -

    The second h2 heading

    Some text after the second h2 heading

    -

    -

    The second h3 heading

    Some text after the second h3 heading

    diff --git a/spec/lib/redmine/access_control_spec.rb b/spec/lib/redmine/access_control_spec.rb deleted file mode 100644 index f1a1cab4b1..0000000000 --- a/spec/lib/redmine/access_control_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require 'spec_helper' -describe Redmine::AccessControl do - describe '.remove_modules_permissions' do - let!(:all_former_permissions) { Redmine::AccessControl.permissions } - let!(:former_repository_permissions) do - module_permissions = Redmine::AccessControl.modules_permissions(['repository']) - - module_permissions.select do |permission| - permission.project_module == :repository - end - end - - subject { Redmine::AccessControl } - - before do - Redmine::AccessControl.remove_modules_permissions(:repository) - end - - after do - raise 'Test outdated' unless Redmine::AccessControl.instance_variable_defined?(:@permissions) - Redmine::AccessControl.instance_variable_set(:@permissions, all_former_permissions) - Redmine::AccessControl.clear_caches - end - - it 'removes from global permissions' do - expect(subject.permissions).not_to include(former_repository_permissions) - end - - it 'removes from public permissions' do - expect(subject.public_permissions).not_to include(former_repository_permissions) - end - - it 'removes from members only permissions' do - expect(subject.members_only_permissions).not_to include(former_repository_permissions) - end - - it 'removes from loggedin only permissions' do - expect(subject.loggedin_only_permissions).not_to include(former_repository_permissions) - end - - it 'should disable repository module' do - expect(subject.available_project_modules).not_to include(:repository) - end - end -end diff --git a/spec/models/copy_project_job_spec.rb b/spec/models/copy_project_job_spec.rb index cfd0073a45..7910d23b57 100644 --- a/spec/models/copy_project_job_spec.rb +++ b/spec/models/copy_project_job_spec.rb @@ -47,7 +47,7 @@ describe CopyProjectJob, type: :model do let(:copy_job) { CopyProjectJob.new user_id: user_de.id, source_project_id: source_project.id, - target_project_params: target_project, + target_project_params: {}, associations_to_copy: [] } @@ -118,6 +118,32 @@ describe CopyProjectJob, type: :model do end end + describe 'copy project fails with internal error' do + let(:admin) { FactoryBot.create(:admin) } + let(:source_project) { FactoryBot.create(:project) } + let(:copy_job) { + CopyProjectJob.new user_id: admin.id, + source_project_id: source_project.id, + target_project_params: params, + associations_to_copy: [:work_packages] + } # send mails + let(:params) { { name: 'Copy', identifier: 'copy' } } + + before do + allow(User).to receive(:current).and_return(admin) + allow(ProjectMailer).to receive(:copy_project_succeeded).and_raise 'error message not meant for user' + end + + it 'renders a error when unexpected errors occur' do + expect(ProjectMailer) + .to receive(:copy_project_failed) + .with(admin, source_project, 'Copy', [I18n.t('copy_project.failed_internal')]) + .and_return maildouble + + expect { copy_job.perform }.not_to raise_error + end + end + shared_context 'copy project' do before do copy_project_job = CopyProjectJob.new(user_id: user.id, @@ -139,13 +165,21 @@ describe CopyProjectJob, type: :model do let(:subproject) { FactoryBot.create(:project, parent: project) } describe 'invalid parent' do - before do expect(ProjectMailer).to receive(:copy_project_failed).and_return(maildouble) end - include_context 'copy project' do let(:project_to_copy) { subproject } end - it { expect(Project.all).to match_array([project, subproject]) } + it "creates no new project" do + expect(Project.all).to match_array([project, subproject]) + end + + it "notifies the user of the failure" do + mail = ActionMailer::Base.deliveries + .find { |m| m.message_id.start_with? "openproject.project-#{user.id}-#{subproject.id}" } + + expect(mail).to be_present + expect(mail.subject).to eq "Cannot copy project #{subproject.name}" + end end describe 'valid parent' do @@ -158,8 +192,6 @@ describe CopyProjectJob, type: :model do } before do - expect(ProjectMailer).to receive(:copy_project_succeeded).and_return(maildouble) - member_add_subproject end @@ -175,6 +207,14 @@ describe CopyProjectJob, type: :model do expect(subproject.reload.enabled_module_names).not_to be_empty end + + it "notifies the user of the success" do + mail = ActionMailer::Base.deliveries + .find { |m| m.message_id.start_with? "openproject.project-#{user.id}-#{subject.id}" } + + expect(mail).to be_present + expect(mail.subject).to eq "Created project #{subject.name}" + end end end end diff --git a/spec/models/custom_field_spec.rb b/spec/models/custom_field_spec.rb index a775d9067b..9159f44def 100644 --- a/spec/models/custom_field_spec.rb +++ b/spec/models/custom_field_spec.rb @@ -339,9 +339,8 @@ describe CustomField, type: :model do shared_examples_for 'saving updates field\'s updated_at' do it 'updates updated_at' do - # mysql does not store milliseconds so we have to slow down the tests by orders of magnitude timestamp_before = field.updated_at - sleep 1 + sleep 0.001 field.save expect(field.updated_at).not_to eql(timestamp_before) end diff --git a/spec/models/mail_handler_spec.rb b/spec/models/mail_handler_spec.rb index 6ec9e55fb0..e288df18d2 100644 --- a/spec/models/mail_handler_spec.rb +++ b/spec/models/mail_handler_spec.rb @@ -99,7 +99,6 @@ describe MailHandler, type: :model do expect(work_package.attachments.first.filename).to eq('Photo25.jpg') end - context 'with existing attachment' do let!(:attachment) { FactoryBot.create(:attachment, container: work_package) } diff --git a/spec/features/mysql/deprecation_spec.rb b/spec/models/queries/projects/filters/type_filter_spec.rb similarity index 60% rename from spec/features/mysql/deprecation_spec.rb rename to spec/models/queries/projects/filters/type_filter_spec.rb index 1959ecbdb6..7da6e38d45 100644 --- a/spec/features/mysql/deprecation_spec.rb +++ b/spec/models/queries/projects/filters/type_filter_spec.rb @@ -1,3 +1,5 @@ +#-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) @@ -28,34 +30,24 @@ require 'spec_helper' -feature 'MySQL deprecation spec', js: true do - let(:user) { FactoryBot.create :admin } - - before do - Capybara.reset! - login_as user - - - end - - it 'renders a warning in admin areas', with_config: { show_warning_bars: true } do - if OpenProject::Database.postgresql? - # Does not render - visit info_admin_index_path - expect(page).to have_no_selector('#mysql-db-warning') - else - visit home_path - expect(page).to have_no_selector('#mysql-db-warning') - - visit info_admin_index_path - expect(page).to have_selector('#mysql-db-warning') - - # Hides in localstorage - find('.warning-bar--disable-on-hover').click - expect(page).to have_no_selector('#mysql-db-warning') +describe Queries::Projects::Filters::TypeFilter, type: :model do + it_behaves_like 'basic query filter' do + let(:class_key) { :type_id } + let(:type) { :list } + let(:model) { Project } + let(:attribute) { :type_id } + let(:values) { ['3'] } + let(:admin) { FactoryBot.build_stubbed(:admin) } + let(:user) { FactoryBot.build_stubbed(:user) } + + before do + allow(Type).to receive(:pluck).with(:name, :id).and_return([['Foo', '1234']]) + end - visit info_admin_index_path - expect(page).to have_no_selector('#mysql-db-warning') + describe '#allowed_values' do + it 'is a list of the possible values' do + expect(instance.allowed_values).to match_array([['Foo', '1234']]) + end end end end diff --git a/spec/models/queries/queries/query_query_spec.rb b/spec/models/queries/queries/query_query_spec.rb index 4033b70337..d3bb64d035 100644 --- a/spec/models/queries/queries/query_query_spec.rb +++ b/spec/models/queries/queries/query_query_spec.rb @@ -49,7 +49,7 @@ describe Queries::Queries::QueryQuery, type: :model do describe '#results' do it 'is the same as handwriting the query' do # apparently, strings are accepted to be compared to - # integers in the dbs (mysql, postgresql) + # integers in the postgresql expected = base_scope .merge(Query .where("queries.project_id IN ('1','2')")) diff --git a/spec/models/query/results_spec.rb b/spec/models/query/results_spec.rb index 240a4e02bb..f3d16f81aa 100644 --- a/spec/models/query/results_spec.rb +++ b/spec/models/query/results_spec.rb @@ -588,14 +588,6 @@ describe ::Query::Results, type: :model do end end - # Introduced to ensure being able to group by custom fields - # when running on a MySQL server. - # When upgrading to rails 5, the sql_mode passed on with the connection - # does include the "only_full_group_by" flag by default which causes our queries to become - # invalid because (mysql error): - # "SELECT list is not in GROUP BY clause and contains nonaggregated column - # 'config_myproject_test.work_packages.id' which is not functionally - # dependent on columns in GROUP BY clause" context 'when grouping by custom field' do let!(:custom_field) do FactoryBot.create(:int_wp_custom_field, is_for_all: true, is_filter: true) diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb index 62d80d0540..ea13827c47 100644 --- a/spec/models/role_spec.rb +++ b/spec/models/role_spec.rb @@ -133,4 +133,19 @@ describe Role, type: :model do it_behaves_like 'adding' end end + + describe '#givable' do + before do + # this should not be necessary once Role (in a membership) and GlobalRole have + # a common ancestor class, e.g. Role (a new one) + @mem_role1 = Role.create name: 'mem_role', permissions: [] + @builtin_role1 = Role.new name: 'builtin_role1', permissions: [] + @builtin_role1.builtin = 3 + @builtin_role1.save + @global_role1 = GlobalRole.create name: 'global_role1', permissions: [] + end + + it { expect(Role.givable.size).to eq(1) } + it { expect(Role.givable[0]).to eql @mem_role1 } + end end diff --git a/spec/models/users/allowed_scope_spec.rb b/spec/models/users/allowed_scope_spec.rb index 7e8ff3d15a..83f30c1dcd 100644 --- a/spec/models/users/allowed_scope_spec.rb +++ b/spec/models/users/allowed_scope_spec.rb @@ -270,7 +270,7 @@ describe User, 'allowed scope' do w/ the permission belonging to a module w/o the module being active' do let(:permission) do - Redmine::AccessControl.permissions.find { |p| p.project_module.present? } + OpenProject::AccessControl.permissions.find { |p| p.project_module.present? } end before do @@ -291,7 +291,7 @@ describe User, 'allowed scope' do w/ the permission belonging to a module w/ the module being active' do let(:permission) do - Redmine::AccessControl.permissions.find { |p| p.project_module.present? } + OpenProject::AccessControl.permissions.find { |p| p.project_module.present? } end before do diff --git a/spec/requests/api/v3/attachments/attachment_resource_spec.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb similarity index 58% rename from spec/requests/api/v3/attachments/attachment_resource_spec.rb rename to spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 79c155b7e8..bd0019feff 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_spec.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -29,30 +29,37 @@ require 'spec_helper' require 'rack/test' -describe 'API v3 Attachment resource', type: :request, content_type: :json do +shared_examples 'an APIv3 attachment resource', type: :request, content_type: :json do |include_by_container = true| include Rack::Test::Methods include API::V3::Utilities::PathHelper include FileHelpers - let(:current_user) do + let(:current_user) { user_with_permissions } + + let(:user_with_permissions) do FactoryBot.create(:user, member_in_project: project, member_through_role: role) end + let(:author) do current_user end + let(:project) { FactoryBot.create(:project, is_public: false) } let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) do - %i[view_work_packages view_wiki_pages delete_wiki_pages_attachments - edit_work_packages edit_wiki_pages edit_messages] - end - let(:work_package) { FactoryBot.create(:work_package, author: current_user, project: project) } + let(:attachment) { FactoryBot.create(:attachment, container: container, author: author) } - let(:wiki) { FactoryBot.create(:wiki, project: project) } - let(:wiki_page) { FactoryBot.create(:wiki_page, wiki: wiki) } - let(:forum) { FactoryBot.create(:forum, project: project) } - let(:forum_message) { FactoryBot.create(:message, forum: forum) } - let(:container) { work_package } + let(:container) { send attachment_type } + + let(:attachment_type) { raise "attachment type goes here, e.g. work_package" } + let(:permissions) { all_permissions } + + let(:all_permissions) { Array([create_permission, read_permission, update_permission]).flatten.compact } + + let(:create_permission) { raise "permissions go here, e.g. add_work_packages" } + let(:read_permission) { raise "permissions go here, e.g. view_work_packages" } + let(:update_permission) { raise "permissions go here, e.g. edit_work_packages" } + + let(:missing_permissions_user) { user_with_permissions } before do allow(User).to receive(:current).and_return current_user @@ -62,43 +69,36 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do subject(:response) { last_response } let(:get_path) { api_v3_paths.attachment attachment.id } - %i[wiki_page work_package forum_message].each do |attachment_type| - context "with a #{attachment_type} attachment" do - let(:container) { send(attachment_type) } + let(:container) { send(attachment_type) } - context 'logged in user' do - before do - get get_path - end + context 'logged in user' do + before do + get get_path + end - it 'should respond with 200' do - expect(subject.status).to eq(200) - end + it 'should respond with 200' do + expect(subject.status).to eq(200) + end - it 'should respond with correct attachment' do - expect(subject.body).to be_json_eql(attachment.filename.to_json).at_path('fileName') - end + it 'should respond with correct attachment' do + expect(subject.body).to be_json_eql(attachment.filename.to_json).at_path('fileName') + end - context 'requesting nonexistent attachment' do - let(:get_path) { api_v3_paths.attachment 9999 } + context 'requesting nonexistent attachment' do + let(:get_path) { api_v3_paths.attachment 9999 } - it_behaves_like 'not found' do - let(:id) { 9999 } - let(:type) { 'Attachment' } - end - end + it_behaves_like 'not found' do + let(:id) { 9999 } + let(:type) { 'Attachment' } + end + end - context 'requesting attachments without sufficient permissions' do - if attachment_type == :forum_message - let(:current_user) { FactoryBot.create(:user) } - else - let(:permissions) { [] } - end + context 'requesting attachments without sufficient permissions' do + let(:current_user) { missing_permissions_user } + let(:permissions) { all_permissions - Array(read_permission) } - it_behaves_like 'not found' do - let(:type) { 'Attachment' } - end - end + it_behaves_like 'not found' do + let(:type) { 'Attachment' } end end end @@ -208,29 +208,23 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do end end - %i[wiki_page work_package forum_message].each do |attachment_type| - context "with a #{attachment_type} attachment" do - let(:container) { send(attachment_type) } - - context 'with required permissions' do - it_behaves_like 'deletes the attachment' + context 'with required permissions' do + it_behaves_like 'deletes the attachment' - context 'for a non-existent attachment' do - let(:path) { api_v3_paths.attachment 1337 } + context 'for a non-existent attachment' do + let(:path) { api_v3_paths.attachment 1337 } - it_behaves_like 'not found' do - let(:id) { 1337 } - let(:type) { 'Attachment' } - end - end + it_behaves_like 'not found' do + let(:id) { 1337 } + let(:type) { 'Attachment' } end + end + end - context 'without required permissions' do - let(:permissions) { %i[view_work_packages view_wiki_pages] } + context 'without required permissions' do + let(:permissions) { all_permissions - Array(update_permission) } - it_behaves_like 'does not delete the attachment' - end - end + it_behaves_like 'does not delete the attachment' end context "with an uncontainered attachment" do @@ -308,4 +302,99 @@ describe 'API v3 Attachment resource', type: :request, content_type: :json do end end end + + context 'by container', if: include_by_container do + subject(:response) { last_response } + + describe '#get' do + let(:get_path) { api_v3_paths.send "attachments_by_#{attachment_type}", container.id } + + before do + FactoryBot.create_list(:attachment, 2, container: container) + get get_path + end + + it 'should respond with 200' do + expect(subject.status).to eq(200) + end + + it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' + end + + describe '#post' do + let(:request_path) { api_v3_paths.send "attachments_by_#{attachment_type}", container.id } + let(:request_parts) { { metadata: metadata, file: file } } + let(:metadata) { { fileName: 'cat.png' }.to_json } + let(:file) { mock_uploaded_file(name: 'original-filename.txt') } + let(:max_file_size) { 1 } # given in kiB + + before do + allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s + post request_path, request_parts + end + + it 'should respond with HTTP Created' do + expect(subject.status).to eq(201) + end + + it 'should return the new attachment' do + expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') + end + + it 'ignores the original file name' do + expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') + end + + context 'metadata section is missing' do + let(:request_parts) { { file: file } } + + it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + end + + context 'file section is missing' do + # rack-test won't send a multipart request without a file being present + # however as long as we depend on correctly named sections this test should do just fine + let(:request_parts) { { metadata: metadata, wrongFileSection: file } } + + it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') + end + + context 'metadata section is no valid JSON' do + let(:metadata) { '"fileName": "cat.png"' } + + it_behaves_like 'parse error' + end + + context 'metadata is missing the fileName' do + let(:metadata) { Hash.new.to_json } + + it_behaves_like 'constraint violation' do + let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } + end + end + + context 'file is too large' do + let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } + let(:expanded_localization) do + I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) + end + + it_behaves_like 'constraint violation' do + let(:message) { "File #{expanded_localization}" } + end + end + + context 'only allowed to add, but not to edit' do + let(:permissions) { all_permissions - Array(update_permission) } + + it_behaves_like 'unauthorized access' + end + + context 'only allowed to view' do + let(:permissions) { Array(read_permission) } + + it_behaves_like 'unauthorized access' + end + end + end end diff --git a/spec/requests/api/v3/attachments/attachments_by_post_resource_spec.rb b/spec/requests/api/v3/attachments/attachments_by_post_resource_spec.rb deleted file mode 100644 index 438812be26..0000000000 --- a/spec/requests/api/v3/attachments/attachments_by_post_resource_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require 'spec_helper' -require 'rack/test' - -describe 'API v3 Attachments by post resource', type: :request do - include Rack::Test::Methods - include API::V3::Utilities::PathHelper - include FileHelpers - - let(:current_user) do - FactoryBot.create(:user, - member_in_project: project, - member_through_role: role) - end - let(:project) { FactoryBot.create(:project) } - let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { [:view_messages] } - let(:forum) { FactoryBot.create(:forum, project: project) } - let(:forum_message) { FactoryBot.create(:message, forum: forum) } - - subject(:response) { last_response } - - before do - allow(User).to receive(:current).and_return current_user - end - - describe '#get' do - let(:get_path) { api_v3_paths.attachments_by_post forum_message.id } - - before do - FactoryBot.create_list(:attachment, 2, container: forum_message) - get get_path - end - - it 'should respond with 200' do - expect(subject.status).to eq(200) - end - - it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' - end - - describe '#post' do - let(:permissions) { %i[view_messages edit_messages] } - - let(:request_path) { api_v3_paths.attachments_by_post forum_message.id } - let(:request_parts) { { metadata: metadata, file: file } } - let(:metadata) { { fileName: 'cat.png' }.to_json } - let(:file) { mock_uploaded_file(name: 'original-filename.txt') } - let(:max_file_size) { 1 } # given in kiB - - before do - allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s - post request_path, request_parts - end - - it 'should respond with HTTP Created' do - expect(subject.status).to eq(201) - end - - it 'should return the new attachment' do - expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') - end - - it 'ignores the original file name' do - expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') - end - - context 'metadata section is missing' do - let(:request_parts) { { file: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'file section is missing' do - # rack-test won't send a multipart request without a file being present - # however as long as we depend on correctly named sections this test should do just fine - let(:request_parts) { { metadata: metadata, wrongFileSection: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'metadata section is no valid JSON' do - let(:metadata) { '"fileName": "cat.png"' } - - it_behaves_like 'parse error' - end - - context 'metadata is missing the fileName' do - let(:metadata) { Hash.new.to_json } - - it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } - end - end - - context 'file is too large' do - let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } - let(:expanded_localization) do - I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) - end - - it_behaves_like 'constraint violation' do - let(:message) { "File #{expanded_localization}" } - end - end - - context 'only allowed to add messages, but no edit permission' do - let(:permissions) { %i[view_messages add_messages] } - - it_behaves_like 'unauthorized access' - end - - context 'only allowed to view messages' do - let(:permissions) { [:view_messages] } - - it_behaves_like 'unauthorized access' - end - end -end diff --git a/spec/requests/api/v3/attachments/attachments_by_wiki_page_resource_spec.rb b/spec/requests/api/v3/attachments/attachments_by_wiki_page_resource_spec.rb deleted file mode 100644 index 0e5153e553..0000000000 --- a/spec/requests/api/v3/attachments/attachments_by_wiki_page_resource_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require 'spec_helper' -require 'rack/test' - -describe 'API v3 Attachments by wiki page resource', type: :request do - include Rack::Test::Methods - include API::V3::Utilities::PathHelper - include FileHelpers - - let(:current_user) do - FactoryBot.create(:user, - member_in_project: project, - member_through_role: role) - end - let(:project) { FactoryBot.create(:project) } - let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { [:view_wiki_pages] } - let(:wiki) { FactoryBot.create(:wiki, project: project) } - let(:wiki_page) { FactoryBot.create(:wiki_page, wiki: wiki) } - - subject(:response) { last_response } - - before do - allow(User).to receive(:current).and_return current_user - end - - describe '#get' do - let(:get_path) { api_v3_paths.attachments_by_wiki_page wiki_page.id } - - before do - FactoryBot.create_list(:attachment, 2, container: wiki_page) - get get_path - end - - it 'should respond with 200' do - expect(subject.status).to eq(200) - end - - it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' - end - - describe '#post' do - let(:permissions) { %i[view_wiki_pages edit_wiki_pages] } - - let(:request_path) { api_v3_paths.attachments_by_wiki_page wiki_page.id } - let(:request_parts) { { metadata: metadata, file: file } } - let(:metadata) { { fileName: 'cat.png' }.to_json } - let(:file) { mock_uploaded_file(name: 'original-filename.txt') } - let(:max_file_size) { 1 } # given in kiB - - before do - allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s - post request_path, request_parts - end - - it 'should respond with HTTP Created' do - expect(subject.status).to eq(201) - end - - it 'should return the new attachment' do - expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') - end - - it 'ignores the original file name' do - expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') - end - - context 'metadata section is missing' do - let(:request_parts) { { file: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'file section is missing' do - # rack-test won't send a multipart request without a file being present - # however as long as we depend on correctly named sections this test should do just fine - let(:request_parts) { { metadata: metadata, wrongFileSection: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'metadata section is no valid JSON' do - let(:metadata) { '"fileName": "cat.png"' } - - it_behaves_like 'parse error' - end - - context 'metadata is missing the fileName' do - let(:metadata) { Hash.new.to_json } - - it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } - end - end - - context 'file is too large' do - let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } - let(:expanded_localization) do - I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) - end - - it_behaves_like 'constraint violation' do - let(:message) { "File #{expanded_localization}" } - end - end - - context 'only allowed to view wiki pages' do - let(:permissions) { [:view_wiki_pages] } - - it_behaves_like 'unauthorized access' - end - end -end diff --git a/spec/requests/api/v3/attachments/attachments_by_work_package_resource_spec.rb b/spec/requests/api/v3/attachments/attachments_by_work_package_resource_spec.rb deleted file mode 100644 index 5a40c848bc..0000000000 --- a/spec/requests/api/v3/attachments/attachments_by_work_package_resource_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require 'spec_helper' -require 'rack/test' - -describe 'API v3 Attachments by work package resource', type: :request do - include Rack::Test::Methods - include API::V3::Utilities::PathHelper - include FileHelpers - - let(:current_user) do - FactoryBot.create(:user, - member_in_project: project, - member_through_role: role) - end - let(:project) { FactoryBot.create(:project, is_public: false) } - let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { [:view_work_packages] } - let(:work_package) { FactoryBot.create(:work_package, author: current_user, project: project) } - - subject(:response) { last_response } - - before do - allow(User).to receive(:current).and_return current_user - end - - describe '#get' do - let(:get_path) { api_v3_paths.attachments_by_work_package work_package.id } - - before do - FactoryBot.create_list(:attachment, 2, container: work_package) - get get_path - end - - it 'should respond with 200' do - expect(subject.status).to eq(200) - end - - it_behaves_like 'API V3 collection response', 2, 2, 'Attachment' - end - - describe '#post' do - let(:permissions) { %i[view_work_packages edit_work_packages] } - - let(:request_path) { api_v3_paths.attachments_by_work_package work_package.id } - let(:request_parts) { { metadata: metadata, file: file } } - let(:metadata) { { fileName: 'cat.png' }.to_json } - let(:file) { mock_uploaded_file(name: 'original-filename.txt') } - let(:max_file_size) { 1 } # given in kiB - - before do - allow(Setting).to receive(:attachment_max_size).and_return max_file_size.to_s - post request_path, request_parts - end - - it 'should respond with HTTP Created' do - expect(subject.status).to eq(201) - end - - it 'should return the new attachment' do - expect(subject.body).to be_json_eql('Attachment'.to_json).at_path('_type') - end - - it 'ignores the original file name' do - expect(subject.body).to be_json_eql('cat.png'.to_json).at_path('fileName') - end - - context 'metadata section is missing' do - let(:request_parts) { { file: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'file section is missing' do - # rack-test won't send a multipart request without a file being present - # however as long as we depend on correctly named sections this test should do just fine - let(:request_parts) { { metadata: metadata, wrongFileSection: file } } - - it_behaves_like 'invalid request body', I18n.t('api_v3.errors.multipart_body_error') - end - - context 'metadata section is no valid JSON' do - let(:metadata) { '"fileName": "cat.png"' } - - it_behaves_like 'parse error' - end - - context 'metadata is missing the fileName' do - let(:metadata) { Hash.new.to_json } - - it_behaves_like 'constraint violation' do - let(:message) { "fileName #{I18n.t('activerecord.errors.messages.blank')}" } - end - end - - context 'file is too large' do - let(:file) { mock_uploaded_file(content: 'a' * 2.kilobytes) } - let(:expanded_localization) do - I18n.t('activerecord.errors.messages.file_too_large', count: max_file_size.kilobytes) - end - - it_behaves_like 'constraint violation' do - let(:message) { "File #{expanded_localization}" } - end - end - - context 'only allowed to add work packages, but no edit permission' do - let(:permissions) { %i[view_work_packages add_work_packages] } - - it_behaves_like 'unauthorized access' - end - - context 'only allowed to view work packages' do - let(:permissions) { [:view_work_packages] } - - it_behaves_like 'unauthorized access' - end - end -end diff --git a/spec/requests/api/v3/attachments/forum_message_spec.rb b/spec/requests/api/v3/attachments/forum_message_spec.rb new file mode 100644 index 0000000000..1cf654279b --- /dev/null +++ b/spec/requests/api/v3/attachments/forum_message_spec.rb @@ -0,0 +1,45 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './attachment_resource_shared_examples' + +describe "forum message attachments" do + it_behaves_like "an APIv3 attachment resource", include_by_container = false do + let(:attachment_type) { :forum_message } + + let(:create_permission) { nil } + let(:read_permission) { nil } + let(:update_permission) { :edit_messages } + + let(:forum) { FactoryBot.create(:forum, project: project) } + let(:forum_message) { FactoryBot.create(:message, forum: forum) } + + let(:missing_permissions_user) { FactoryBot.create(:user) } + end +end diff --git a/spec/requests/api/v3/attachments/wiki_page_spec.rb b/spec/requests/api/v3/attachments/wiki_page_spec.rb new file mode 100644 index 0000000000..6e284862e2 --- /dev/null +++ b/spec/requests/api/v3/attachments/wiki_page_spec.rb @@ -0,0 +1,43 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './attachment_resource_shared_examples' + +describe "wiki page attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :wiki_page } + + let(:create_permission) { nil } + let(:read_permission) { :view_wiki_pages } + let(:update_permission) { %i(delete_wiki_pages_attachments edit_wiki_pages) } + + let(:wiki) { FactoryBot.create(:wiki, project: project) } + let(:wiki_page) { FactoryBot.create(:wiki_page, wiki: wiki) } + end +end diff --git a/spec/requests/api/v3/attachments/work_package_spec.rb b/spec/requests/api/v3/attachments/work_package_spec.rb new file mode 100644 index 0000000000..07ee15bb36 --- /dev/null +++ b/spec/requests/api/v3/attachments/work_package_spec.rb @@ -0,0 +1,44 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './attachment_resource_shared_examples' + +describe "work package attachments" do + it_behaves_like "an APIv3 attachment resource" do + let(:attachment_type) { :work_package } + + let(:create_permission) { :add_work_packages } + let(:read_permission) { :view_work_packages } + let(:update_permission) { :edit_work_packages } + + let(:work_package) do + FactoryBot.create :work_package, author: current_user, project: project + end + end +end diff --git a/spec/requests/api/v3/category_resource_spec.rb b/spec/requests/api/v3/category_resource_spec.rb index 71205ddd65..6a5c6a8479 100644 --- a/spec/requests/api/v3/category_resource_spec.rb +++ b/spec/requests/api/v3/category_resource_spec.rb @@ -39,24 +39,24 @@ describe 'API v3 Category resource' do let(:anonymous_user) { FactoryBot.create(:user) } let(:privileged_user) do FactoryBot.create(:user, - member_in_project: private_project, - member_through_role: role) + member_in_project: private_project, + member_through_role: role) end let!(:categories) { FactoryBot.create_list(:category, 3, project: private_project) } let!(:other_categories) { FactoryBot.create_list(:category, 2, project: public_project) } let!(:user_categories) do FactoryBot.create_list(:category, - 2, - project: private_project, - assigned_to: privileged_user) + 2, + project: private_project, + assigned_to: privileged_user) end describe 'categories by project' do subject(:response) { last_response } context 'logged in user' do - let(:get_path) { api_v3_paths.categories private_project.id } + let(:get_path) { api_v3_paths.categories_by_project private_project.id } before do allow(User).to receive(:current).and_return privileged_user @@ -67,7 +67,7 @@ describe 'API v3 Category resource' do end context 'not logged in user' do - let(:get_path) { api_v3_paths.categories private_project.id } + let(:get_path) { api_v3_paths.categories_by_project private_project.id } before do allow(User).to receive(:current).and_return anonymous_user diff --git a/spec/requests/api/v3/membership_resources_spec.rb b/spec/requests/api/v3/membership_resources_spec.rb index d8bee56b05..8308783cc4 100644 --- a/spec/requests/api/v3/membership_resources_spec.rb +++ b/spec/requests/api/v3/membership_resources_spec.rb @@ -458,4 +458,220 @@ describe 'API v3 memberhips resource', type: :request, content_type: :json do end end end + + describe 'PATCH api/v3/memberships/:id' do + let(:path) { api_v3_paths.membership(other_member.id) } + let(:another_role) { FactoryBot.create(:role) } + let(:body) do + { + _links: { + "roles": [ + { + href: api_v3_paths.role(another_role.id) + } + ] + } + }.to_json + end + + let(:members) { [own_member, other_member] } + + before do + members + + login_as current_user + + patch path, body + end + + it 'responds with 200' do + expect(last_response.status).to eq(200) + end + + it 'updates the member' do + expect(other_member.roles.reload) + .to match_array [another_role] + end + + it 'returns the updated version' do + expect(last_response.body) + .to be_json_eql('Membership'.to_json) + .at_path('_type') + + expect(last_response.body) + .to be_json_eql([{ href: api_v3_paths.role(another_role.id), title: another_role.name }].to_json) + .at_path('_links/roles') + + # unchanged + expect(last_response.body) + .to be_json_eql(project.name.to_json) + .at_path('_links/project/title') + + expect(last_response.body) + .to be_json_eql(other_user.name.to_json) + .at_path('_links/principal/title') + end + + context 'if attempting to empty the roles' do + let(:body) do + { + _links: { + "roles": [] + } + }.to_json + end + + it 'returns 422' do + expect(last_response.status) + .to eql(422) + + expect(last_response.body) + .to be_json_eql("Roles need to be assigned.".to_json) + .at_path('message') + end + end + + context 'if attempting to assign unassignable roles' do + let(:anonymous_role) { FactoryBot.create(:anonymous_role) } + let(:body) do + { + _links: { + "roles": [ + { + href: api_v3_paths.role(anonymous_role.id) + } + ] + } + }.to_json + end + + it 'returns 422' do + expect(last_response.status) + .to eql(422) + + expect(last_response.body) + .to be_json_eql("Roles has an unassignable role.".to_json) + .at_path('message') + end + end + + context 'if attempting to switch the project' do + let(:other_project) do + FactoryBot.create(:project).tap do |p| + FactoryBot.create(:member, + project: p, + roles: [FactoryBot.create(:role, permissions: [:manage_members])], + user: current_user) + end + end + + let(:body) do + { + _links: { + "project": { + "href": api_v3_paths.project(other_project.id) + + } + } + }.to_json + end + + it 'returns 422' do + expect(last_response.status) + .to eql(422) + + expect(last_response.body) + .to be_json_eql("You must not write a read-only attribute.".to_json) + .at_path('message') + end + end + + context 'if attempting to switch the principal' do + let(:another_user) do + FactoryBot.create(:user) + end + + let(:body) do + { + _links: { + "principal": { + "href": api_v3_paths.user(another_user.id) + + } + } + }.to_json + end + + it 'returns 422' do + expect(last_response.status) + .to eql(422) + + expect(last_response.body) + .to be_json_eql("You must not write a read-only attribute.".to_json) + .at_path('message') + + expect(last_response.body) + .to be_json_eql("user".to_json) + .at_path('_embedded/details/attribute') + end + end + + context 'if lacking the manage permissions' do + let(:permissions) { [:view_members] } + + it_behaves_like 'unauthorized access' + end + + context 'if lacking the view permissions' do + let(:permissions) { [] } + + it_behaves_like 'not found' do + let(:id) { member.id } + let(:type) { 'Membership' } + end + end + end + + describe 'DELETE /api/v3/memberships/:id' do + let(:path) { api_v3_paths.membership(other_member.id) } + let(:members) { [own_member, other_member] } + + before do + members + login_as current_user + + delete path + end + + subject { last_response } + + context 'with required permissions' do + it 'responds with HTTP No Content' do + expect(subject.status).to eq 204 + end + + it 'deletes the member' do + expect(Member.exists?(other_member.id)).to be_falsey + end + + context 'for a non-existent version' do + let(:path) { api_v3_paths.membership 1337 } + + it_behaves_like 'not found' do + let(:id) { 1337 } + let(:type) { 'Membership' } + end + end + end + + context 'without permission to delete members' do + let(:permissions) { [:view_members] } + + it_behaves_like 'unauthorized access' + + it 'does not delete the member' do + expect(Member.exists?(other_member.id)).to be_truthy + end + end + end end diff --git a/spec/requests/api/v3/memberships/create_form_resource_spec.rb b/spec/requests/api/v3/memberships/create_form_resource_spec.rb index 41151a5312..248cb01da6 100644 --- a/spec/requests/api/v3/memberships/create_form_resource_spec.rb +++ b/spec/requests/api/v3/memberships/create_form_resource_spec.rb @@ -44,7 +44,7 @@ describe ::API::V3::Memberships::CreateFormAPI, content_type: :json do let(:other_user) { FactoryBot.create(:user) } let(:permissions) { [:manage_members] } - let(:path) { api_v3_paths.create_memberships_form } + let(:path) { api_v3_paths.create_membership_form } let(:parameters) { {} } before do diff --git a/spec/requests/api/v3/memberships/update_form_resource_spec.rb b/spec/requests/api/v3/memberships/update_form_resource_spec.rb new file mode 100644 index 0000000000..62efdcb70f --- /dev/null +++ b/spec/requests/api/v3/memberships/update_form_resource_spec.rb @@ -0,0 +1,229 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. + +require 'spec_helper' +require 'rack/test' + +describe ::API::V3::Memberships::UpdateFormAPI, content_type: :json do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + + let(:member) { FactoryBot.create(:member, project: project, roles: [role, another_role]) } + let(:project) { FactoryBot.create(:project) } + let(:user) do + FactoryBot.create(:user, + member_in_project: project, + member_through_role: role) + end + let(:role) { FactoryBot.create(:role, permissions: permissions) } + let(:other_role) { FactoryBot.create(:role) } + let(:another_role) { FactoryBot.create(:role) } + let(:other_user) { FactoryBot.create(:user) } + let(:permissions) { [:manage_members] } + let(:project) { FactoryBot.create(:project) } + let(:path) { api_v3_paths.membership_form(member.id) } + let(:parameters) do + { + _links: { + roles: [ + { + href: api_v3_paths.role(role.id) + }, + { + href: api_v3_paths.role(other_role.id) + } + ] + } + } + end + + before do + login_as(user) + post path, parameters.to_json + end + + subject(:response) { last_response } + + describe '#POST /api/v3/memberships/:id/form' do + it 'returns 200 OK' do + expect(response.status).to eq(200) + end + + it 'returns a form' do + expect(response.body) + .to be_json_eql('Form'.to_json) + .at_path('_type') + end + + it 'does not update the member (no new roles)' do + expect(member.roles.reload) + .to match_array [role, another_role] + end + + it 'contains the update roles in the payload' do + expect(response.body) + .to have_json_size(2) + .at_path('_embedded/payload/_links/roles') + + expect(response.body) + .to be_json_eql(api_v3_paths.role(role.id).to_json) + .at_path('_embedded/payload/_links/roles/0/href') + + expect(response.body) + .to be_json_eql(api_v3_paths.role(other_role.id).to_json) + .at_path('_embedded/payload/_links/roles/1/href') + end + + it 'does not contain the project in the payload' do + expect(response.body) + .not_to have_json_path('_embedded/payload/_links/project') + end + + it 'does not contain the principal in the payload' do + expect(response.body) + .not_to have_json_path('_embedded/payload/_links/principal') + end + + context 'with wanting to remove all roles' do + let(:parameters) do + { + _links: { + roles: [] + } + } + end + + it 'has 1 validation errors' do + expect(subject.body).to have_json_size(1).at_path('_embedded/validationErrors') + end + + it 'notes roles cannot be empty' do + expect(subject.body) + .to be_json_eql("Roles need to be assigned.".to_json) + .at_path('_embedded/validationErrors/roles/message') + end + + it 'has no commit link' do + expect(subject.body) + .not_to have_json_path('_links/commit') + end + end + + context 'with wanting to alter the project' do + let(:other_project) do + role = FactoryBot.create(:role, permissions: permissions) + + FactoryBot.create(:project, + members: [ + FactoryBot.create(:member, + roles: [role], + user: user) + ]) + end + let(:parameters) do + { + _links: { + project: { + href: api_v3_paths.project(other_project.id) + } + } + } + end + + it 'has 1 validation errors' do + expect(subject.body).to have_json_size(1).at_path('_embedded/validationErrors') + end + + it 'has a validation error on project' do + expect(subject.body).to have_json_path('_embedded/validationErrors/project') + end + + it 'notes project to not be writeable' do + expect(subject.body) + .to be_json_eql(false) + .at_path('_embedded/schema/project/writable') + end + + it 'has no commit link' do + expect(subject.body) + .not_to have_json_path('_links/commit') + end + end + + context 'with wanting to alter the principal' do + let(:other_principal) do + FactoryBot.create(:user) + end + let(:parameters) do + { + _links: { + principal: { + href: api_v3_paths.user(other_principal.id) + } + } + } + end + + it 'has 1 validation errors' do + expect(subject.body).to have_json_size(1).at_path('_embedded/validationErrors') + end + + it 'has a validation error on principal' do + expect(subject.body).to have_json_path('_embedded/validationErrors/user') + end + + it 'notes principal to not be writeable' do + expect(subject.body) + .to be_json_eql(false) + .at_path('_embedded/schema/principal/writable') + end + + it 'has no commit link' do + expect(subject.body) + .not_to have_json_path('_links/commit') + end + end + + context 'without the necessary edit permission' do + let(:permissions) { [:view_members] } + + it 'returns 403 Not Authorized' do + expect(response.status).to eq(403) + end + end + + context 'without the necessary view permission' do + let(:permissions) { [] } + + it 'returns 404 Not Found' do + expect(response.status).to eq(404) + end + end + end +end diff --git a/spec/requests/api/v3/version_resource_spec.rb b/spec/requests/api/v3/version_resource_spec.rb index b7e9e41671..fbe701a742 100644 --- a/spec/requests/api/v3/version_resource_spec.rb +++ b/spec/requests/api/v3/version_resource_spec.rb @@ -170,7 +170,7 @@ describe 'API v3 Version resource', content_type: :json do .to be_present end - it 'returns the newly created version' do + it 'returns the updated version' do expect(last_response.body) .to be_json_eql('Version'.to_json) .at_path('_type') diff --git a/spec/requests/api/v3/work_package_resource_spec.rb b/spec/requests/api/v3/work_package_resource_spec.rb index b6ad733d40..9eace5270a 100644 --- a/spec/requests/api/v3/work_package_resource_spec.rb +++ b/spec/requests/api/v3/work_package_resource_spec.rb @@ -47,7 +47,7 @@ describe 'API v3 Work package resource', FactoryBot.create(:project, identifier: 'test_project', is_public: false) end let(:role) { FactoryBot.create(:role, permissions: permissions) } - let(:permissions) { %i[view_work_packages edit_work_packages] } + let(:permissions) { %i[view_work_packages edit_work_packages assign_versions] } let(:current_user) do user = FactoryBot.create(:user, member_in_project: project, member_through_role: role) @@ -309,6 +309,20 @@ describe 'API v3 Work package resource', end it_behaves_like 'lock version updated' + + context 'for a user having assign_versions but lacking edit_work_packages permission' do + let(:permissions) { %i[view_work_packages assign_versions] } + + include_context 'patch request' + + it { expect(response.status).to eq(422) } + + it 'has a readonly error' do + expect(response.body) + .to be_json_eql('urn:openproject-org:api:v3:errors:PropertyIsReadOnly'.to_json) + .at_path('errorIdentifier') + end + end end context 'description' do @@ -727,6 +741,20 @@ describe 'API v3 Work package resource', it_behaves_like 'lock version updated' end + + context 'for a user lacking the assign_versions permission' do + let(:permissions) { %i[view_work_packages edit_work_packages] } + + include_context 'patch request' + + it { expect(response.status).to eq(422) } + + it 'has a readonly error' do + expect(response.body) + .to be_json_eql('urn:openproject-org:api:v3:errors:PropertyIsReadOnly'.to_json) + .at_path('errorIdentifier') + end + end end context 'category' do diff --git a/spec/requests/api/v3/work_packages/available_projects_on_create_api_spec.rb b/spec/requests/api/v3/work_packages/available_projects_on_create_api_spec.rb index 84f5bd590e..57dab3d96c 100644 --- a/spec/requests/api/v3/work_packages/available_projects_on_create_api_spec.rb +++ b/spec/requests/api/v3/work_packages/available_projects_on_create_api_spec.rb @@ -38,30 +38,56 @@ describe API::V3::WorkPackages::AvailableProjectsOnCreateAPI, type: :request do let(:project) { FactoryBot.create(:project) } let(:user) do FactoryBot.create(:user, - member_in_project: project, - member_through_role: add_role) + member_in_project: project, + member_through_role: add_role) end + let(:type_id) { nil } - before do - project + context 'with a type filter present' do + let(:type) { FactoryBot.create :type } + let(:type_id) { type.id } + let(:project_with_type) { FactoryBot.create :project, types: [type] } + let(:member) do + FactoryBot.create(:member, principal: user, project: project_with_type, roles: [add_role]) + end - allow(User).to receive(:current).and_return(user) - get api_v3_paths.available_projects_on_create - end + before do + project + project_with_type + member - context 'w/ the necessary permissions' do - it_behaves_like 'API V3 collection response', 1, 1, 'Project' + allow(User).to receive(:current).and_return(user) + get api_v3_paths.available_projects_on_create(type_id) + end - it 'has the project for which the add_work_packages permission exists' do - expect(last_response.body).to be_json_eql(project.id).at_path('_embedded/elements/0/id') + it 'returns only the filtered one' do + expect(last_response.body).to be_json_eql(1).at_path('total') + expect(last_response.body).to be_json_eql(project_with_type.id).at_path('_embedded/elements/0/id') end end - context 'w/o any add_work_packages permission' do - let(:add_role) do - FactoryBot.create(:role, permissions: []) + describe 'with a single project' do + before do + project + + allow(User).to receive(:current).and_return(user) + get api_v3_paths.available_projects_on_create(type_id) end - it { expect(last_response.status).to eq(403) } + context 'w/ the necessary permissions' do + it_behaves_like 'API V3 collection response', 1, 1, 'Project' + + it 'has the project for which the add_work_packages permission exists' do + expect(last_response.body).to be_json_eql(project.id).at_path('_embedded/elements/0/id') + end + end + + context 'w/o any add_work_packages permission' do + let(:add_role) do + FactoryBot.create(:role, permissions: []) + end + + it { expect(last_response.status).to eq(403) } + end end end diff --git a/spec/requests/api/v3/work_packages/create_form_resource_spec.rb b/spec/requests/api/v3/work_packages/create_form_resource_spec.rb index 59396d45de..7eed2b7927 100644 --- a/spec/requests/api/v3/work_packages/create_form_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/create_form_resource_spec.rb @@ -59,7 +59,7 @@ describe ::API::V3::WorkPackages::CreateProjectFormAPI do it 'has the available_projects link for creation in the schema' do expect(response.body) - .to be_json_eql(api_v3_paths.available_projects_on_create.to_json) + .to be_json_eql(api_v3_paths.available_projects_on_create(nil).to_json) .at_path('_embedded/schema/project/_links/allowedValues/href') end diff --git a/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb b/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb index 4de6bca59e..b4738604a4 100644 --- a/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb @@ -34,9 +34,16 @@ describe 'API v3 Work package form resource', type: :request do include Capybara::RSpecMatchers include API::V3::Utilities::PathHelper + shared_let(:all_allowed_permissions) { %i[view_work_packages edit_work_packages assign_versions] } + shared_let(:assign_permissions) { %i[view_work_packages assign_versions] } shared_let(:project) { FactoryBot.create(:project, is_public: false) } shared_let(:work_package) { FactoryBot.create(:work_package, project: project) } - shared_let(:authorized_user) { FactoryBot.create(:user, member_in_project: project) } + shared_let(:authorized_user) do + FactoryBot.create(:user, member_in_project: project, member_with_permissions: all_allowed_permissions) + end + shared_let(:authorized_assign_user) do + FactoryBot.create(:user, member_in_project: project, member_with_permissions: assign_permissions) + end shared_let(:unauthorized_user) { FactoryBot.create(:user) } describe '#post' do @@ -67,7 +74,7 @@ describe 'API v3 Work package form resource', type: :request do it_behaves_like 'not found' end - context 'user with needed permissions' do + context 'user with all edit permissions' do let(:params) {} let(:current_user) { authorized_user } @@ -691,5 +698,45 @@ describe 'API v3 Work package form resource', type: :request do end end end + + context 'user with assign version permissions' do + let(:params) do + { + lockVersion: work_package.lock_version + } + end + + include_context 'post request' do + let(:current_user) { authorized_assign_user } + end + + subject { last_response.body } + + shared_examples_for 'valid payload' do + it { expect(last_response.status).to eq(200) } + + it { is_expected.to have_json_path('_embedded/payload') } + + it { is_expected.to have_json_path('_embedded/payload/lockVersion') } + + it { is_expected.to have_json_path('_embedded/payload/_links/version') } + + it { is_expected.not_to have_json_path('_embedded/payload/subject') } + end + + it_behaves_like 'valid payload' + + it 'denotes subject to not be writeable' do + is_expected + .to be_json_eql(false) + .at_path('_embedded/schema/subject/writable') + end + + it 'denotes version to be writeable' do + is_expected + .to be_json_eql(true) + .at_path('_embedded/schema/version/writable') + end + end end end diff --git a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb index b5f105e385..333bcfab58 100644 --- a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb @@ -145,7 +145,7 @@ describe API::V3::WorkPackages::Schema::WorkPackageSchemasAPI, type: :request do expect(last_response.headers['ETag']).to match(/W\/\"\w+\"/) end - it 'caches the response' do + it 'caches the response', skip: true do schema_class = API::V3::WorkPackages::Schema::TypedWorkPackageSchema representer_class = API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter diff --git a/spec/services/api/v3/update_query_from_v3_params_service_spec.rb b/spec/services/api/v3/update_query_from_v3_params_service_spec.rb index 50d6f01bb9..e7776cf343 100644 --- a/spec/services/api/v3/update_query_from_v3_params_service_spec.rb +++ b/spec/services/api/v3/update_query_from_v3_params_service_spec.rb @@ -63,7 +63,7 @@ describe ::API::V3::UpdateQueryFromV3ParamsService, allow(mock) .to receive(:call) - .with(parsed_params) + .with(parsed_params, valid_subset: false) .and_return(mock_update_query_service_response) mock diff --git a/spec/services/api/v3/work_package_collection_from_query_service_spec.rb b/spec/services/api/v3/work_package_collection_from_query_service_spec.rb index 0115a3e95a..1a6b03d8d2 100644 --- a/spec/services/api/v3/work_package_collection_from_query_service_spec.rb +++ b/spec/services/api/v3/work_package_collection_from_query_service_spec.rb @@ -136,7 +136,7 @@ describe ::API::V3::WorkPackageCollectionFromQueryService, allow(mock) .to receive(:call) - .with(params) + .with(params, valid_subset: false) .and_return(mock_update_query_service_response) mock diff --git a/spec/services/authorization/user_allowed_query_spec.rb b/spec/services/authorization/user_allowed_query_spec.rb index d117fd0c06..8dae2f2371 100644 --- a/spec/services/authorization/user_allowed_query_spec.rb +++ b/spec/services/authorization/user_allowed_query_spec.rb @@ -271,7 +271,7 @@ describe Authorization::UserAllowedQuery do w/ the permission belonging to a module w/o the module being active' do let(:permission) do - Redmine::AccessControl.permissions.find { |p| p.project_module.present? } + OpenProject::AccessControl.permissions.find { |p| p.project_module.present? } end before do @@ -291,7 +291,7 @@ describe Authorization::UserAllowedQuery do w/ the permission belonging to a module w/ the module being active' do let(:permission) do - Redmine::AccessControl.permissions.find { |p| p.project_module.present? } + OpenProject::AccessControl.permissions.find { |p| p.project_module.present? } end before do diff --git a/spec/services/create_work_package_service_spec.rb b/spec/services/create_work_package_service_spec.rb deleted file mode 100644 index 884838dd04..0000000000 --- a/spec/services/create_work_package_service_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -#-- encoding: UTF-8 -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See doc/COPYRIGHT.rdoc for more details. - -require 'spec_helper' - -describe CreateWorkPackageService do - let(:user) { FactoryBot.build_stubbed(:user) } - let(:work_package) { FactoryBot.build_stubbed(:work_package, author: nil) } - let(:project) { FactoryBot.build_stubbed(:project_with_types) } - let(:instance) { described_class.new(user: user) } - let(:errors) { double('errors') } - - describe '.contract' do - it 'uses the CreateContract contract' do - expect(instance.contract_class).to eql WorkPackages::CreateContract - end - end - - describe '.new' do - it 'takes a user which is available as a getter' do - expect(instance.user).to eql user - end - end - - describe '#call' do - let(:mock_contract) do - double(WorkPackages::CreateContract, - new: mock_contract_instance) - end - let(:mock_contract_instance) do - mock_model(WorkPackages::CreateContract) - end - let(:attributes) do - { project_id: 1, - subject: 'lorem ipsum', - status_id: 5 } - end - - before do - allow(instance) - .to receive(:contract_class) - .and_return(mock_contract) - - allow(WorkPackage) - .to receive(:new) - .and_return(work_package) - - allow(work_package) - .to receive(:save) - .and_return true - allow(mock_contract_instance) - .to receive(:validate) - .and_return true - end - - subject { instance.call(work_package) } - - context 'if contract validates and the work package saves' do - it 'is successful' do - expect(subject) - .to be_success - end - - it 'has no errors' do - expect(subject.errors) - .to be_empty - end - - it 'returns the work package as a result' do - result = subject.result - - expect(result).to be_a WorkPackage - end - - it 'sets the user to be the author' do - result = subject.result - - expect(result.author).to eql(user) - end - end - - context 'if contract does not validate' do - before do - allow(mock_contract_instance) - .to receive(:validate) - .and_return false - end - - it 'is unsuccessful' do - expect(subject) - .to_not be_success - end - - it "returns the contract's errors" do - allow(mock_contract_instance) - .to receive(:errors) - .and_return errors - - expect(subject.errors).to eql errors - end - end - - context 'if work_package does not save' do - before do - allow(work_package) - .to receive(:save) - .and_return false - end - - it 'is unsuccessful' do - expect(subject) - .to_not be_success - end - - it "returns the work_package's errors" do - allow(work_package) - .to receive(:errors) - .and_return errors - - expect(subject.errors).to eql errors - end - end - end -end diff --git a/spec/services/work_packages/set_attributes_service_spec.rb b/spec/services/work_packages/set_attributes_service_spec.rb index b58ce0f586..92a0d05d8b 100644 --- a/spec/services/work_packages/set_attributes_service_spec.rb +++ b/spec/services/work_packages/set_attributes_service_spec.rb @@ -244,10 +244,6 @@ describe WorkPackages::SetAttributesService, type: :model do let(:attributes) { {} } let(:work_package) { new_work_package } - before do - work_package.author = nil - end - it_behaves_like 'service call' do it "sets the service's author" do subject @@ -255,6 +251,13 @@ describe WorkPackages::SetAttributesService, type: :model do expect(work_package.author) .to eql user end + + it 'notes the author to be system changed' do + subject + + expect(work_package.changed_by_system) + .to include('author_id') + end end end @@ -554,6 +557,13 @@ describe WorkPackages::SetAttributesService, type: :model do expect(work_package.category) .to eql new_category end + + it 'adds change to system changes' do + subject + + expect(work_package.changed_by_system) + .to include('category_id') + end end end @@ -576,6 +586,13 @@ describe WorkPackages::SetAttributesService, type: :model do expect(work_package.type) .to eql default_type end + + it 'adds change to system changes' do + subject + + expect(work_package.changed_by_system) + .to include('type_id') + end end context 'no default type exists in new project' do @@ -587,6 +604,13 @@ describe WorkPackages::SetAttributesService, type: :model do expect(work_package.type) .to eql other_type end + + it 'adds change to system changes' do + subject + + expect(work_package.changed_by_system) + .to include('type_id') + end end context 'when also setting a new type via attributes' do @@ -598,6 +622,13 @@ describe WorkPackages::SetAttributesService, type: :model do expect(work_package.type) .to eql yet_another_type end + + it 'does not set the change to system changes' do + subject + + expect(work_package.changed_by_system) + .not_to include('type_id') + end end end end diff --git a/spec/services/work_packages/update_service_integration_spec.rb b/spec/services/work_packages/update_service_integration_spec.rb index 3337d60cde..84220cd41b 100644 --- a/spec/services/work_packages/update_service_integration_spec.rb +++ b/spec/services/work_packages/update_service_integration_spec.rb @@ -314,7 +314,6 @@ describe WorkPackages::UpdateService, 'integration tests', type: :model do expect(subject) .to be_success - expect(subject.result.type) .to eql other_type end diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index b69879111b..df95c1cd12 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -168,25 +168,23 @@ module Components def openChildrenAutocompleter retry_block do + next if page.has_selector?('.wp-relations--children .ng-input input') find('.wp-inline-create--reference-link', text: I18n.t('js.relation_buttons.add_existing_child')).click # Security check to be sure that the autocompleter has finished loading - page.find '.ng-dropdown-panel-items' + page.find '.wp-relations--children .ng-input input' end end def add_existing_child(work_package) - # Locate the create row container - container = find('.wp-relations--add-form') - # Enter the query and select the child - autocomplete = container.find(".wp-relations--autocomplete") + autocomplete = page.find(".wp-relations--add-form .wp-relations--autocomplete") select_autocomplete autocomplete, query: work_package.id, results_selector: '.ng-dropdown-panel-items', select_text: work_package.subject - container.find('.wp-create-relation--save').click + page.find('.wp-relations--add-form .wp-create-relation--save').click end def expect_child(work_package) diff --git a/spec/support/pages/work_packages/embedded_work_packages_table.rb b/spec/support/pages/work_packages/embedded_work_packages_table.rb index 625774994a..42fffd262e 100644 --- a/spec/support/pages/work_packages/embedded_work_packages_table.rb +++ b/spec/support/pages/work_packages/embedded_work_packages_table.rb @@ -28,9 +28,12 @@ require 'support/pages/page' require 'support/pages/work_packages/work_packages_table' +require 'support/components/ng_select_autocomplete_helpers.rb' module Pages class EmbeddedWorkPackagesTable < WorkPackagesTable + include ::Components::NgSelectAutocompleteHelpers + attr_reader :container def initialize(container, project = nil) @@ -41,5 +44,27 @@ module Pages def table_container container.find('.work-package-table') end + + def click_reference_inline_create + ## + # When using the inline create on initial page load, + # there is a delay on travis where inline create can be clicked. + sleep 1 + container.find('.wp-inline-create--reference-link').click + + # Returns the autocomplete container + container.find('.wp-relations--autocomplete') + end + + def reference_work_package(work_package, query: work_package.subject) + click_reference_inline_create + + autocomplete_container = container.find('.wp-relations--autocomplete') + select_autocomplete autocomplete_container, + query: query, + results_selector: '.ng-dropdown-panel-items' + + expect_work_package_listed work_package + end end end diff --git a/spec/support/shared/acts_as_watchable.rb b/spec/support/shared/acts_as_watchable.rb index 245def8165..15206bbc61 100644 --- a/spec/support/shared/acts_as_watchable.rb +++ b/spec/support/shared/acts_as_watchable.rb @@ -79,7 +79,7 @@ MESSAGE end let(:is_public_permission) do - Redmine::AccessControl.public_permissions.map(&:name).include?(watch_permission) + OpenProject::AccessControl.public_permissions.map(&:name).include?(watch_permission) end shared_context 'non member role has the permission to watch' do diff --git a/spec/support/work_packages/work_package_field.rb b/spec/support/work_packages/work_package_field.rb index 81cf2ad05e..ab6997d531 100644 --- a/spec/support/work_packages/work_package_field.rb +++ b/spec/support/work_packages/work_package_field.rb @@ -151,7 +151,7 @@ class WorkPackageField scroll_to_element(input_element) input_element.find('input').set content - page.find('.ng-option', text: 'Create new: ' + content).click + page.find('.ng-option', text: 'Create: ' + content).click end def type(text) diff --git a/spec/tasks/backup_specs.rb b/spec/tasks/backup_specs.rb index 97d2f9f57a..d96d10d71d 100644 --- a/spec/tasks/backup_specs.rb +++ b/spec/tasks/backup_specs.rb @@ -28,95 +28,6 @@ require 'spec_helper' -describe 'mysql' do - let(:database_config) do - { 'adapter' => 'mysql2', - 'database' => 'openproject-database', - 'username' => 'testuser', - 'password' => 'testpassword' } - end - - before do - expect(ActiveRecord::Base).to receive(:configurations).at_least(:once).and_return('test' => database_config) - allow(FileUtils).to receive(:mkdir_p).and_return(nil) - end - - describe 'backup:database:create' do - include_context 'rake' - - it 'calls the mysqldump binary' do - expect(Kernel).to receive(:system) do |*args| - expect(args.first).to eql('mysqldump') - end - subject.invoke - end - - it 'writes the mysql config file' do - expect(Kernel).to receive(:system) do |*args| - defaults_file = args.find { |s| s.starts_with? '--defaults-file=' } - defaults_file = defaults_file[('--defaults-file='.length)..-1] - expect(File.readable?(defaults_file)).to be true - - file_contents = File.read defaults_file - expect(file_contents).to include('testuser') - expect(file_contents).to include('testpassword') - end - subject.invoke - end - - it 'uses the first task parameter as the target filename' do - custom_file_path = './foo/bar/testfile.sql' - expect(Kernel).to receive(:system) do |*args| - result_file = args.find { |s| s.starts_with? '--result-file=' } - expect(result_file).to include(custom_file_path) - end - subject.invoke custom_file_path - end - end - - describe 'backup:database:restore' do - include_context 'rake' - - let(:backup_file) do - Tempfile.new('test_backup') - end - - after do - backup_file.unlink - end - - it 'calls the mysql binary' do - expect(Kernel).to receive(:system) do |*args| - expect(args.first).to start_with('mysql') - end - subject.invoke backup_file.path - end - - it 'writes the mysql config file' do - expect(Kernel).to receive(:system) do |*args| - defaults_file = args.first[/--defaults-file="(?[^"]+)"/, :file] - expect(File.readable?(defaults_file)).to be true - - file_contents = File.read defaults_file - expect(file_contents).to include('testuser') - expect(file_contents).to include('testpassword') - end - subject.invoke backup_file.path - end - - it 'uses the first task parameter as the target filename' do - expect(Kernel).to receive(:system) do |*args| - expect(args.first).to include(backup_file.path) - end - subject.invoke backup_file.path - end - - it 'throws an error when called without a parameter' do - expect { subject.invoke }.to raise_error - end - end -end - describe 'postgresql' do let(:database_config) do { 'adapter' => 'postgresql', diff --git a/spec/views/projects/level_list_api_json_spec.rb b/spec/views/projects/level_list_api_json_spec.rb deleted file mode 100644 index 9119e494e9..0000000000 --- a/spec/views/projects/level_list_api_json_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2017 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See docs/COPYRIGHT.rdoc for more details. -#++ - -require 'spec_helper' - -describe '/projects/level_list.api.rabl', type: :view do - before do - params[:format] = 'json' - end - - subject { rendered } - describe 'with no project available' do - it 'renders an empty projects document' do - assign(:projects, []) - - render - - is_expected.to have_json_size(0).at_path('projects') - end - end - - describe 'with some projects available' do - let(:projects) do - p1 = FactoryBot.build(:project, name: 'P1') - - # a result from Project.project_level_list - [{ project: p1, - level: 0 }, - { project: FactoryBot.build(:project, name: 'P2', parent: p1), - level: 1 }, - { project: FactoryBot.build(:project, name: 'P3'), - level: 0 }] - end - - before do - assign(:projects, projects) - render - end - - subject { rendered } - - it 'renders a projects document with the size of 3 of type array' do - is_expected.to have_json_size(3).at_path('projects') - end - - it 'renders all three projects' do - is_expected.to be_json_eql('P1'.to_json).at_path('projects/0/name') - is_expected.to be_json_eql('P2'.to_json).at_path('projects/1/name') - is_expected.to be_json_eql('P3'.to_json).at_path('projects/2/name') - end - - it 'renders the project levels' do - is_expected.to be_json_eql(0.to_json).at_path('projects/0/level') - is_expected.to be_json_eql(1.to_json).at_path('projects/1/level') - is_expected.to be_json_eql(0.to_json).at_path('projects/2/level') - end - end -end diff --git a/spec_legacy/fixtures/role_permissions.yml b/spec_legacy/fixtures/role_permissions.yml index 0ac8372bf1..598e93162f 100644 --- a/spec_legacy/fixtures/role_permissions.yml +++ b/spec_legacy/fixtures/role_permissions.yml @@ -336,6 +336,10 @@ role_permission131: id: 131 role_id: 2 permission: 'list_attachments' +role_permission132: + id: 132 + role_id: 2 + permission: 'assign_versions' role_permission200: id: 200 diff --git a/spec_legacy/functional/roles_controller_spec.rb b/spec_legacy/functional/roles_controller_spec.rb index 19bebf30d7..f5781674c2 100644 --- a/spec_legacy/functional/roles_controller_spec.rb +++ b/spec_legacy/functional/roles_controller_spec.rb @@ -59,55 +59,6 @@ describe RolesController, type: :controller do assert_template 'new' end - it 'should post new with validaton failure' do - post :create, - params: { - role: { - name: '', - permissions: ['add_work_packages', 'edit_work_packages', 'log_time', ''], - assignable: '0' - } - } - - assert_response :success - assert_template 'new' - assert_select 'div', attributes: { id: 'errorExplanation' } - end - - it 'should post new without workflow copy' do - post :create, - params: { - role: { - name: 'RoleWithoutWorkflowCopy', - permissions: ['add_work_packages', 'edit_work_packages', 'log_time'], - assignable: '0' - } - } - - assert_redirected_to roles_path - role = Role.find_by(name: 'RoleWithoutWorkflowCopy') - refute_nil role - assert_equal [:add_work_packages, :edit_work_packages, :log_time], role.permissions.sort - assert !role.assignable? - end - - it 'should post new with workflow copy' do - post :create, - params: { - role: { - name: 'RoleWithWorkflowCopy', - permissions: ['add_work_packages', 'edit_work_packages', 'log_time'], - assignable: '0' - }, - copy_workflow_from: '1' - } - - assert_redirected_to roles_path - role = Role.find_by(name: 'RoleWithWorkflowCopy') - refute_nil role - assert_equal Role.find(1).workflows.size, role.workflows.size - end - it 'should get edit' do get :edit, params: { id: 1 } assert_response :success @@ -115,22 +66,6 @@ describe RolesController, type: :controller do assert_equal Role.find(1), assigns(:role) end - it 'should put update' do - put :update, - params: { - id: 1, - role: { - name: 'Manager', - permissions: ['edit_project'], - assignable: '0' - } - } - - assert_redirected_to roles_path - role = Role.find(1) - assert_equal [:edit_project], role.permissions - end - it 'should destroy' do r = Role.new(name: 'ToBeDestroyed', permissions: [:view_wiki_pages]) assert r.save @@ -165,48 +100,4 @@ describe RolesController, type: :controller do value: 'delete_work_packages', checked: nil } end - - it 'should put bulk update' do - put :bulk_update, - params: { - permissions: { '0' => '', '1' => ['edit_work_packages'], '3' => ['add_work_packages', 'delete_work_packages'] } - } - assert_redirected_to roles_path - - assert_equal [:edit_work_packages], Role.find(1).permissions - assert_equal [:add_work_packages, :delete_work_packages], Role.find(3).permissions.sort - assert Role.find(2).permissions.empty? - end - - it 'should clear all permissions' do - put :bulk_update, params: { permissions: { '0' => '' } } - assert_redirected_to roles_path - assert Role.find(1).permissions.empty? - end - - it 'should move highest' do - put :update, params: { id: 3, role: { move_to: 'highest' } } - assert_redirected_to roles_path - assert_equal 1, Role.find(3).position - end - - it 'should move higher' do - position = Role.find(3).position - put :update, params: { id: 3, role: { move_to: 'higher' } } - assert_redirected_to roles_path - assert_equal position - 1, Role.find(3).position - end - - it 'should move lower' do - position = Role.find(2).position - put :update, params: { id: 2, role: { move_to: 'lower' } } - assert_redirected_to roles_path - assert_equal position + 1, Role.find(2).position - end - - it 'should move lowest' do - put :update, params: { id: 2, role: { move_to: 'lowest' } } - assert_redirected_to roles_path - assert_equal Role.count, Role.find(2).position - end end diff --git a/spec_legacy/functional/timelog_controller_spec.rb b/spec_legacy/functional/timelog_controller_spec.rb index dc10bc660c..4823f75784 100644 --- a/spec_legacy/functional/timelog_controller_spec.rb +++ b/spec_legacy/functional/timelog_controller_spec.rb @@ -92,92 +92,4 @@ describe TimelogController, type: :controller do assert_equal 2, entry.work_package_id assert_equal 2, entry.user_id end - - it 'should index all projects' do - get :index - assert_response :success - assert_template 'index' - refute_nil assigns(:total_hours) - assert_equal '162.90', '%.2f' % assigns(:total_hours) - assert_select 'form', - attributes: { action: '/time_entries', id: 'query_form' } - end - - it 'should index at project level' do - get :index, params: { project_id: 'ecookbook' } - assert_response :success - assert_template 'index' - refute_nil assigns(:entries) - assert_equal 4, assigns(:entries).size - # project and subproject - assert_equal [1, 3], assigns(:entries).map(&:project_id).uniq.sort - refute_nil assigns(:total_hours) - assert_equal '162.90', '%.2f' % assigns(:total_hours) - # display all time by default - assert_equal nil, assigns(:from) - assert_equal nil, assigns(:to) - assert_select 'form', - attributes: { action: '/projects/ecookbook/time_entries', id: 'query_form' } - end - - it 'should index at project level with date range' do - get :index, params: { project_id: 'ecookbook', from: '2007-03-20', to: '2007-04-30' } - assert_response :success - assert_template 'index' - refute_nil assigns(:entries) - assert_equal 3, assigns(:entries).size - refute_nil assigns(:total_hours) - assert_equal '12.90', '%.2f' % assigns(:total_hours) - assert_equal '2007-03-20'.to_date, assigns(:from) - assert_equal '2007-04-30'.to_date, assigns(:to) - assert_select 'form', - attributes: { action: '/projects/ecookbook/time_entries', id: 'query_form' } - end - - it 'should index at project level with period' do - get :index, params: { project_id: 'ecookbook', period: '7_days' } - assert_response :success - assert_template 'index' - refute_nil assigns(:entries) - refute_nil assigns(:total_hours) - assert_equal Date.today - 7, assigns(:from) - assert_equal Date.today, assigns(:to) - assert_select 'form', - attributes: { action: '/projects/ecookbook/time_entries', id: 'query_form' } - end - - it 'should index one day' do - get :index, params: { project_id: 'ecookbook', from: '2007-03-23', to: '2007-03-23' } - assert_response :success - assert_template 'index' - refute_nil assigns(:total_hours) - assert_equal '4.25', '%.2f' % assigns(:total_hours) - assert_select 'form', - attributes: { action: '/projects/ecookbook/time_entries', id: 'query_form' } - end - - it 'should index at issue level' do - get :index, params: { work_package_id: 1 } - assert_response :success - assert_template 'index' - refute_nil assigns(:entries) - assert_equal 2, assigns(:entries).size - refute_nil assigns(:total_hours) - assert_equal 154.25, assigns(:total_hours) - # display all time based on what's been logged - assert_equal nil, assigns(:from) - assert_equal nil, assigns(:to) - assert_select 'form', - attributes: { action: work_package_time_entries_path(1), id: 'query_form' } - end - - it 'should index atom feed' do - TimeEntry.all.each(&:recreate_initial_journal!) - - get :index, params: { project_id: 1, format: 'atom' } - assert_response :success - assert_equal 'application/atom+xml', response.content_type - refute_nil assigns(:items) - assert assigns(:items).first.is_a?(TimeEntry) - end end diff --git a/spec_legacy/unit/mail_handler_spec.rb b/spec_legacy/unit/mail_handler_spec.rb index 080479a54e..a12c63c3e6 100644 --- a/spec_legacy/unit/mail_handler_spec.rb +++ b/spec_legacy/unit/mail_handler_spec.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF)