Merge remote-tracking branch 'origin/dev' into housekeeping/remove-angularjs

pull/7385/head
Oliver Günther 5 years ago
commit 0814858989
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 168
      .travis.yml
  2. 9
      Gemfile
  3. 5
      Gemfile.lock
  4. 3
      app/assets/javascripts/onboarding/homescreen_tour.js
  5. 4
      app/assets/stylesheets/content/_modal.sass
  6. 1
      app/assets/stylesheets/content/_table.sass
  7. 8
      app/assets/stylesheets/content/_user.sass
  8. 8
      app/assets/stylesheets/content/_wiki.sass
  9. 2
      app/assets/stylesheets/content/menus/_project_autocompletion.sass
  10. 7
      app/assets/stylesheets/content/work_packages/_table_content.sass
  11. 5
      app/assets/stylesheets/content/work_packages/_table_hierarchy.sass
  12. 13
      app/assets/stylesheets/content/work_packages/tabs/_relations.sass
  13. 6
      app/assets/stylesheets/layout/_main_menu.sass
  14. 2
      app/assets/stylesheets/layout/_toolbar_mobile.sass
  15. 4
      app/assets/stylesheets/layout/_top_menu.sass
  16. 7
      app/assets/stylesheets/layout/work_packages/_details_view.sass
  17. 8
      app/assets/stylesheets/layout/work_packages/_full_view.sass
  18. 2
      app/assets/stylesheets/layout/work_packages/_query_menu.sass
  19. 2
      app/assets/stylesheets/layout/work_packages/_table.sass
  20. 1
      app/assets/stylesheets/layout/work_packages/_table_embedded.sass
  21. 25
      app/assets/stylesheets/openproject/_mixins.sass
  22. 70
      app/contracts/delete_contract.rb
  23. 16
      app/contracts/members/base_contract.rb
  24. 13
      app/contracts/members/create_contract.rb
  25. 20
      app/contracts/members/delete_contract.rb
  26. 32
      app/contracts/members/update_contract.rb
  27. 82
      app/contracts/model_contract.rb
  28. 10
      app/contracts/projects/delete_contract.rb
  29. 92
      app/contracts/roles/base_contract.rb
  30. 40
      app/contracts/roles/create_contract.rb
  31. 32
      app/contracts/roles/update_contract.rb
  32. 20
      app/contracts/time_entries/delete_contract.rb
  33. 11
      app/contracts/versions/delete_contract.rb
  34. 23
      app/contracts/work_packages/base_contract.rb
  35. 12
      app/contracts/work_packages/create_contract.rb
  36. 20
      app/contracts/work_packages/delete_contract.rb
  37. 4
      app/contracts/work_packages/skip_authorization_checks.rb
  38. 32
      app/contracts/work_packages/update_contract.rb
  39. 7
      app/controllers/projects_controller.rb
  40. 107
      app/controllers/roles_controller.rb
  41. 76
      app/controllers/timelog_controller.rb
  42. 14
      app/controllers/versions_controller.rb
  43. 16
      app/helpers/projects_helper.rb
  44. 23
      app/helpers/roles_helper.rb
  45. 6
      app/helpers/warning_bar_helper.rb
  46. 2
      app/mailers/project_mailer.rb
  47. 31
      app/models/attachment.rb
  48. 18
      app/models/custom_field/order_statements.rb
  49. 11
      app/models/journal.rb
  50. 50
      app/models/journal/aggregated_journal.rb
  51. 6
      app/models/mail_handler.rb
  52. 2
      app/models/member_role.rb
  53. 65
      app/models/mixins/changed_by_system.rb
  54. 6
      app/models/principal.rb
  55. 4
      app/models/project.rb
  56. 2
      app/models/project/copy.rb
  57. 47
      app/models/queries/filters/strategies/relation.rb
  58. 12
      app/models/queries/not_existing_filter.rb
  59. 15
      app/models/queries/operators.rb
  60. 36
      app/models/queries/operators/blocked.rb
  61. 36
      app/models/queries/operators/blocks.rb
  62. 36
      app/models/queries/operators/children.rb
  63. 36
      app/models/queries/operators/duplicated.rb
  64. 36
      app/models/queries/operators/duplicates.rb
  65. 36
      app/models/queries/operators/follows.rb
  66. 36
      app/models/queries/operators/includes.rb
  67. 36
      app/models/queries/operators/parent.rb
  68. 36
      app/models/queries/operators/part_of.rb
  69. 36
      app/models/queries/operators/precedes.rb
  70. 36
      app/models/queries/operators/relates.rb
  71. 36
      app/models/queries/operators/required.rb
  72. 36
      app/models/queries/operators/requires.rb
  73. 1
      app/models/queries/projects.rb
  74. 66
      app/models/queries/projects/filters/type_filter.rb
  75. 1
      app/models/queries/work_packages.rb
  76. 8
      app/models/queries/work_packages/filter/filter_for_wp_mixin.rb
  77. 1
      app/models/queries/work_packages/filter/project_filter.rb
  78. 67
      app/models/queries/work_packages/filter/relatable_filter.rb
  79. 1
      app/models/queries/work_packages/filter/relates_filter.rb
  80. 1
      app/models/queries/work_packages/filter/subject_or_id_filter.rb
  81. 8
      app/models/queries/work_packages/filter/work_package_filter.rb
  82. 28
      app/models/query/results.rb
  83. 16
      app/models/role.rb
  84. 31
      app/models/user.rb
  85. 2
      app/models/user/project_authorization_cache.rb
  86. 2
      app/models/work_package.rb
  87. 11
      app/policies/work_package_policy.rb
  88. 7
      app/seeders/basic_data/role_seeder.rb
  89. 4
      app/services/api/v3/parse_query_params_service.rb
  90. 4
      app/services/api/v3/update_query_from_v3_params_service.rb
  91. 4
      app/services/api/v3/work_package_collection_from_query_service.rb
  92. 4
      app/services/authorization/project_query.rb
  93. 4
      app/services/authorization/user_allowed_query.rb
  94. 6
      app/services/base_services/create.rb
  95. 7
      app/services/base_services/set_attributes.rb
  96. 12
      app/services/members/create_service.rb
  97. 7
      app/services/members/delete_service.rb
  98. 13
      app/services/members/update_service.rb
  99. 50
      app/services/roles/create_service.rb
  100. 37
      app/services/roles/notify_mixin.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -85,210 +85,138 @@ jobs:
- bash script/ci/runner.sh npm - bash script/ci/runner.sh npm
- stage: test - stage: test
name: 'spec_legacy (1/1) - mysql' name: 'spec_legacy (1/1) - standard'
script: 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 - bash script/ci/runner.sh spec_legacy 1 1
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'spec_legacy (1/1) - postgres standard' name: 'spec_legacy (1/1) - bim'
script: script:
- bash script/ci/setup.sh spec_legacy postgres - bash script/ci/setup.sh spec_legacy bim
- 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/runner.sh spec_legacy 1 1 - bash script/ci/runner.sh spec_legacy 1 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'units (1/4) - mysql' name: 'units (1/4) - standard'
script: script:
- bash script/ci/setup.sh units mysql - bash script/ci/setup.sh units
- bash script/ci/runner.sh units 4 1 - bash script/ci/runner.sh units 4 1
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'units (1/4) - postgres standard' name: 'units (1/4) - bim'
script: script:
- bash script/ci/setup.sh units postgres - bash script/ci/setup.sh units bim
- 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/runner.sh units 4 1 - bash script/ci/runner.sh units 4 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'units (2/4) - mysql' name: 'units (2/4) - standard'
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'
script: script:
- bash script/ci/setup.sh units postgres - bash script/ci/setup.sh units
- bash script/ci/runner.sh units 4 2 - bash script/ci/runner.sh units 4 2
- stage: test - stage: test
name: 'units (2/4) - postgres bim' name: 'units (2/4) - bim'
script: script:
- bash script/ci/setup.sh units postgres bim - bash script/ci/setup.sh units bim
- bash script/ci/runner.sh units 4 2 - bash script/ci/runner.sh units 4 2
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'units (3/4) - mysql' name: 'units (3/4) - standard'
script: script:
- bash script/ci/setup.sh units mysql - bash script/ci/setup.sh units
- bash script/ci/runner.sh units 4 3 - bash script/ci/runner.sh units 4 3
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'units (3/4) - postgres standard' name: 'units (3/4) - bim'
script: script:
- bash script/ci/setup.sh units postgres - bash script/ci/setup.sh units bim
- 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/runner.sh units 4 3 - bash script/ci/runner.sh units 4 3
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'units (4/4) - mysql' name: 'units (4/4) - standard'
script: script:
- bash script/ci/setup.sh units mysql - bash script/ci/setup.sh units
- bash script/ci/runner.sh units 4 4 - bash script/ci/runner.sh units 4 4
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'units (4/4) - postgres standard' name: 'units (4/4) - bim'
script: script:
- bash script/ci/setup.sh units postgres - bash script/ci/setup.sh units bim
- 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/runner.sh units 4 4 - bash script/ci/runner.sh units 4 4
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'features (1/4) - mysql' name: 'features (1/4) - standard'
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'
script: script:
- bash script/ci/setup.sh features postgres - bash script/ci/setup.sh features
- bash script/ci/runner.sh features 4 1 - bash script/ci/runner.sh features 4 1
- stage: test - stage: test
name: 'features (1/4) - postgres bim' name: 'features (1/4) - bim'
script: script:
- bash script/ci/setup.sh features postgres bim - bash script/ci/setup.sh features bim
- bash script/ci/runner.sh features 4 1 - bash script/ci/runner.sh features 4 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'features (2/4) - mysql' name: 'features (2/4) - standard'
script: script:
- bash script/ci/setup.sh features mysql - bash script/ci/setup.sh features
- bash script/ci/runner.sh features 4 2 - bash script/ci/runner.sh features 4 2
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'features (2/4) - postgres standard' name: 'features (2/4) - bim'
script: script:
- bash script/ci/setup.sh features postgres - bash script/ci/setup.sh features bim
- 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/runner.sh features 4 2 - bash script/ci/runner.sh features 4 2
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'features (3/4) - mysql' name: 'features (3/4) - standard'
script: script:
- bash script/ci/setup.sh features mysql - bash script/ci/setup.sh features
- bash script/ci/runner.sh features 4 3 - bash script/ci/runner.sh features 4 3
if: env(SKIP_MYSQL_TESTING) IS blank
- stage: test - stage: test
name: 'features (3/4) - postgres standard' name: 'features (3/4) - bim'
script: script:
- bash script/ci/setup.sh features postgres - bash script/ci/setup.sh features bim
- 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/runner.sh features 4 3 - bash script/ci/runner.sh features 4 3
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'features (4/4) - mysql' name: 'features (4/4) - standard'
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'
script: script:
- bash script/ci/setup.sh features postgres - bash script/ci/setup.sh features
- bash script/ci/runner.sh features 4 4 - bash script/ci/runner.sh features 4 4
- stage: test - stage: test
name: 'features (4/4) - postgres bim' name: 'features (4/4) - bim'
script: script:
- bash script/ci/setup.sh features postgres bim - bash script/ci/setup.sh features bim
- bash script/ci/runner.sh features 4 4 - bash script/ci/runner.sh features 4 4
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'plugins:units (1/1) - mysql' name: 'plugins:units (1/1) - standard'
script: script:
- bash script/ci/setup.sh plugins:units mysql - bash script/ci/setup.sh plugins:units
- 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/runner.sh plugins:units 1 1 - bash script/ci/runner.sh plugins:units 1 1
if: head_branch !~ /^core\// if: head_branch !~ /^core\//
- stage: test - stage: test
name: 'plugins:units (1/1) - postgres bim' name: 'plugins:units (1/1) - bim'
script: 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 - bash script/ci/runner.sh plugins:units 1 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'plugins:features (1/1) - mysql' name: 'plugins:features (1/1) - standard'
script: script:
- bash script/ci/setup.sh plugins:features mysql - bash script/ci/setup.sh plugins:features
- 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/runner.sh plugins:features 1 1 - bash script/ci/runner.sh plugins:features 1 1
if: head_branch !~ /^core\// if: head_branch !~ /^core\//
- stage: test - stage: test
name: 'plugins:features (1/1) - postgres bim' name: 'plugins:features (1/1) - bim'
script: 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 - bash script/ci/runner.sh plugins:features 1 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/
- stage: test - stage: test
name: 'plugins:cucumber (1/1) - mysql' name: 'plugins:cucumber (1/1) - standard'
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'
script: 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 - bash script/ci/runner.sh plugins:cucumber 1 1
if: head_branch !~ /^core\// if: head_branch !~ /^core\//
- stage: test - stage: test
name: 'plugins:cucumber (1/1) - postgres bim' name: 'plugins:cucumber (1/1) - bim'
script: 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 - bash script/ci/runner.sh plugins:cucumber 1 1
if: head_branch =~ /^(bim\/|dev|release\/)/ if: head_branch =~ /^(bim\/|dev|release\/)/

@ -103,11 +103,6 @@ gem 'bcrypt', '~> 3.1.6'
gem 'multi_json', '~> 1.13.1' gem 'multi_json', '~> 1.13.1'
gem 'oj', '~> 3.7.0' 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 'daemons'
gem 'delayed_job_active_record', '~> 4.1.1' gem 'delayed_job_active_record', '~> 4.1.1'
@ -281,10 +276,6 @@ gem 'reform-rails', '~> 0.1.7'
gem 'roar', '~> 1.1.0' gem 'roar', '~> 1.1.0'
platforms :mri, :mingw, :x64_mingw do platforms :mri, :mingw, :x64_mingw do
group :mysql2 do
gem 'mysql2', '~> 0.5.0'
end
group :postgres do group :postgres do
gem 'pg', '~> 1.1.0' gem 'pg', '~> 1.1.0'
end end

@ -578,7 +578,6 @@ GEM
mustermann (1.0.3) mustermann (1.0.3)
mustermann-grape (1.0.0) mustermann-grape (1.0.0)
mustermann (~> 1.0.0) mustermann (~> 1.0.0)
mysql2 (0.5.2)
net-ldap (0.16.1) net-ldap (0.16.1)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (6.0.0.351) newrelic_rpm (6.0.0.351)
@ -650,8 +649,6 @@ GEM
pry (>= 0.9.11) pry (>= 0.9.11)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0) puma (3.12.0)
rabl (0.14.0)
activesupport (>= 2.3.14)
rack (2.0.6) rack (2.0.6)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
@ -954,7 +951,6 @@ DEPENDENCIES
meta-tags (~> 2.11.0) meta-tags (~> 2.11.0)
multi_json (~> 1.13.1) multi_json (~> 1.13.1)
my_page! my_page!
mysql2 (~> 0.5.0)
net-ldap (~> 0.16.0) net-ldap (~> 0.16.0)
newrelic_rpm newrelic_rpm
nokogiri (~> 1.10.3) nokogiri (~> 1.10.3)
@ -998,7 +994,6 @@ DEPENDENCIES
pry-rescue (~> 1.5.0) pry-rescue (~> 1.5.0)
pry-stack_explorer (~> 0.4.9.2) pry-stack_explorer (~> 0.4.9.2)
puma (~> 3.12.0) puma (~> 3.12.0)
rabl (~> 0.14.0)
rack-attack (~> 5.4.2) rack-attack (~> 5.4.2)
rack-mini-profiler rack-mini-profiler
rack-protection (~> 2.0.0) rack-protection (~> 2.0.0)

@ -5,7 +5,8 @@
'next #top-menu': I18n.t('js.onboarding.steps.welcome'), 'next #top-menu': I18n.t('js.onboarding.steps.welcome'),
'skipButton': {className: 'enjoyhint_btn-transparent', text: I18n.t('js.onboarding.buttons.skip')}, 'skipButton': {className: 'enjoyhint_btn-transparent', text: I18n.t('js.onboarding.buttons.skip')},
'nextButton': {text: I18n.t('js.onboarding.buttons.next')}, 'nextButton': {text: I18n.t('js.onboarding.buttons.next')},
'containerClass': '-hidden-arrow' 'containerClass': '-hidden-arrow',
'bottom': 7
}, },
{ {
'description': I18n.t('js.onboarding.steps.project_selection'), 'description': I18n.t('js.onboarding.steps.project_selection'),

@ -63,7 +63,7 @@ $modal-footer-height: $modal-header-height
max-width: 60vw max-width: 60vw
overflow-y: auto overflow-y: auto
@include styled-scroll-bar-vertical @include styled-scroll-bar
&.-wide &.-wide
min-width: 75vw min-width: 75vw
@ -81,7 +81,7 @@ $modal-footer-height: $modal-header-height
padding: 0 1.5rem padding: 0 1.5rem
max-height: calc(100vh - #{$modal-header-height} - #{$modal-footer-height}) max-height: calc(100vh - #{$modal-header-height} - #{$modal-footer-height})
overflow: auto overflow: auto
@include styled-scroll-bar-vertical @include styled-scroll-bar
&.-formattable &.-formattable
p:last-of-type p:last-of-type

@ -50,6 +50,7 @@ $input-elements: input, 'input.form--text-field', select, 'select.form--select',
overflow: overflow:
x: auto x: auto
y: auto y: auto
@include styled-scroll-bar
.generic-table--action-buttons .generic-table--action-buttons
margin-top: 2rem margin-top: 2rem

@ -64,10 +64,12 @@
user-select: none user-select: none
h1, h2, h3, h4 h1, h2, h3, h4
.avatar, .avatar-mini, .avatar-medium user-avatar
vertical-align: middle vertical-align: middle
margin-right: 7px margin-right: 5px
tr user-avatar
margin-right: 5px
.user-link .user-link
display: inline-block display: inline-block

@ -72,6 +72,10 @@ div.wiki
margin-left: 0 margin-left: 0
display: table display: table
font-size: $wiki-toc-ul-font-size font-size: $wiki-toc-ul-font-size
.section-nav
margin-bottom: 0
margin-left: 12px
&.right &.right
float: right float: right
margin-left: 12px margin-left: 12px
@ -107,8 +111,10 @@ div.wiki
a.wiki-anchor a.wiki-anchor
display: none display: none
margin-left: 6px
text-decoration: none text-decoration: none
font-size: 16px
vertical-align: middle
padding-right: 2px
&:hover &:hover
color: #aaa !important color: #aaa !important

@ -82,7 +82,7 @@
// Borders to complete the menu look // Borders to complete the menu look
border-right: 1px solid $header-drop-down-border-color border-right: 1px solid $header-drop-down-border-color
border-left: 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 // Cut off result element width
.ui-menu-item-wrapper .ui-menu-item-wrapper

@ -55,7 +55,7 @@
// Avoid that the select field gets too small // Avoid that the select field gets too small
.wp-inline-edit--field.ng-select .wp-inline-edit--field.ng-select
min-width: 110px min-width: 140px
// Styles for inline editable attributes // Styles for inline editable attributes
.work-package-table--container td.-editable .work-package-table--container td.-editable
@ -148,12 +148,7 @@ html:not(.-browser-mobile)
.wp-table--cell-span .wp-table--cell-span
padding: 2px padding: 2px
// On edge, pointer-events only work on
// block or inline-block elements
body.-browser-edge body.-browser-edge
.wp-table--cell-span
display: inline-block !important
// Ensure height is set in table // Ensure height is set in table
.work-package-table .wp-table--cell-span .work-package-table .wp-table--cell-span
height: 22px !important height: 22px !important

@ -9,11 +9,6 @@
&:hover &:hover
text-decoration: none 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 // Toggle the indicator accessibility texts
// accordingly // accordingly
.wp-table--hierarchy-indicator-collapsed .wp-table--hierarchy-indicator-collapsed

@ -80,9 +80,6 @@
.wp-relations-controls-section .wp-relations-controls-section
text-align: right text-align: right
flex-shrink: 1 flex-shrink: 1
.force-right
position: absolute
right: 0
a:hover a:hover
text-decoration: none text-decoration: none
@ -144,10 +141,12 @@
.icon-button .icon-button
cursor: not-allowed cursor: not-allowed
// Disable overflow in grid-content of create form .wp-relations-create--form
// To allow results indicator to overflow it display: flex
.wp-relations-create--form .grid-content
overflow-y: visible .wp-relations-input-section
margin-right: 10px
flex: 1
.detail-panel--relations .detail-panel--relations
.panel-toggler .icon-small .panel-toggler .icon-small

@ -50,7 +50,7 @@ $menu-item-line-height: 30px
+allow-vertical-scrolling +allow-vertical-scrolling
height: calc(100vh - #{$header-height}) height: calc(100vh - #{$header-height})
position: relative position: relative
@include styled-scroll-bar-vertical @include styled-scroll-bar
// Fixed heights to allow inner scrolling // Fixed heights to allow inner scrolling
.menu_root.closed, .menu_root.closed,
@ -67,7 +67,7 @@ $menu-item-line-height: 30px
.main-menu--children .main-menu--children
height: calc(100% - (#{$main-menu-item-height} + 10px)) // 10px spacing height: calc(100% - (#{$main-menu-item-height} + 10px)) // 10px spacing
overflow: auto overflow: auto
@include styled-scroll-bar-vertical @include styled-scroll-bar
ul ul
margin: 0 margin: 0
@ -318,7 +318,7 @@ a.main-menu--parent-node
padding-left: 7px padding-left: 7px
padding-right: 7px padding-right: 7px
@include styled-scroll-bar-vertical @include styled-scroll-bar
.main-menu--segment-header .main-menu--segment-header
@include varprop(color, main-menu-fieldset-header-color) @include varprop(color, main-menu-fieldset-header-color)

@ -38,7 +38,7 @@
background: #fff background: #fff
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap
justify-content: stretch justify-content: flex-end
width: calc(100% + 10px) width: calc(100% + 10px)
> li > li

@ -107,7 +107,7 @@ $search-input-height: 30px
overflow-y: auto overflow-y: auto
overflow-x: hidden overflow-x: hidden
@include styled-scroll-bar-vertical @include styled-scroll-bar
li li
float: none float: none
@ -244,7 +244,7 @@ $search-input-height: 30px
@include varprop(color, header-search-field-font-color) @include varprop(color, header-search-field-font-color)
.scroll-host .scroll-host
@include styled-scroll-bar-vertical @include styled-scroll-bar
max-height: 80vh max-height: 80vh
height: auto height: auto

@ -79,8 +79,11 @@ body.router--work-packages-split-view-new
top: 50px top: 50px
bottom: 55px bottom: 55px
width: 100% width: 100%
+allow-vertical-scrolling overflow-x: hidden
padding: 0 15px 0 20px overflow-y: scroll
padding: 0 5px 0 20px
@include styled-scroll-bar
.work-packages--details .work-packages--details

@ -79,9 +79,10 @@
overflow-x: hidden overflow-x: hidden
flex: 2 flex: 2
position: relative position: relative
@include styled-scroll-bar
.work-packages--panel-inner .work-packages--panel-inner
padding: 5px 15px 20px 0px padding: 0px 5px 20px 0
width: 100% width: 100%
// These styles were taken over from the details tab styling. // These styles were taken over from the details tab styling.
@ -97,12 +98,13 @@
.work-packages-full-view--split-right .work-packages-full-view--split-right
min-width: 500px min-width: 500px
overflow-y: auto overflow-y: scroll
overflow-x: auto overflow-x: auto
position: relative position: relative
@include styled-scroll-bar
.work-packages--panel-inner .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 .work-package-details-activities-activity-contents ul.work-package-details-activities-messages
padding-left: 0 padding-left: 0

@ -15,7 +15,7 @@ $wp-query-menu-search-container-height: 35px
background: none background: none
z-index: 0 // Prevent overlapping with project select dropdown (https://community.openproject.com/wp/28175) 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 .wp-query-menu--results-container

@ -121,6 +121,7 @@
flex: 1 1 flex: 1 1
// Show scrollbars for inner content // Show scrollbars for inner content
overflow: auto overflow: auto
@include styled-scroll-bar
// relative for loading indicator // relative for loading indicator
position: relative position: relative
// Hint browser that this will inner-scroll // Hint browser that this will inner-scroll
@ -142,6 +143,7 @@
overflow-x: scroll overflow-x: scroll
// Show the vertical scrollbar when necessary // Show the vertical scrollbar when necessary
overflow-y: auto overflow-y: auto
@include styled-scroll-bar
// Hidden by default // Hidden by default
display: none display: none
// Hint browser that this will inner-scroll // Hint browser that this will inner-scroll

@ -47,6 +47,7 @@ $table-timeline--compact-row-height: 28px
// Allow scrolling in narrow views // Allow scrolling in narrow views
.work-packages-split-view--tabletimeline-content .work-packages-split-view--tabletimeline-content
overflow: auto overflow: auto
@include styled-scroll-bar
// Disable css containment since we have no inner elements // Disable css containment since we have no inner elements
.work-packages-tabletimeline--table-side, .work-packages-tabletimeline--table-side,

@ -87,16 +87,17 @@
$scrollbar-color: #DDDDDD $scrollbar-color: #DDDDDD
$scrollbar-size: 10px
@mixin styled-scroll-bar-vertical @mixin styled-scroll-bar
// Firefox specific styles // Firefox specific styles
scrollbar-color: transparent transparent scrollbar-color: transparent transparent
scrollbar-width: thin scrollbar-width: thin
// Other browser styles // Other browser styles
&::-webkit-scrollbar &::-webkit-scrollbar
width: 8px height: $scrollbar-size
height: 12px width: $scrollbar-size
&::-webkit-scrollbar-track &::-webkit-scrollbar-track
background: transparent background: transparent
@ -110,24 +111,6 @@ $scrollbar-color: #DDDDDD
&::-webkit-scrollbar-thumb &::-webkit-scrollbar-thumb
visibility: visible 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 @mixin two-column-layout
column-count: 2 column-count: 2
column-gap: 3rem column-gap: 3rem

@ -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

@ -28,10 +28,6 @@
module Members module Members
class BaseContract < ::ModelContract class BaseContract < ::ModelContract
def self.model
Member
end
delegate :principal, delegate :principal,
:project, :project,
:new_record?, :new_record?,
@ -42,7 +38,6 @@ module Members
def validate def validate
user_allowed_to_manage user_allowed_to_manage
roles_grantable roles_grantable
principal_assignable
super super
end end
@ -54,15 +49,10 @@ module Members
end end
def roles_grantable def roles_grantable
unless roles.all? { |r| r.builtin == Role::NON_BUILTIN && r.class == Role } unmarked_roles = model.member_roles.reject(&:marked_for_destruction?).map(&:role)
errors.add(:roles, :ungrantable)
end
end
def principal_assignable unless unmarked_roles.all? { |r| r.builtin == Role::NON_BUILTIN && r.class == Role }
if principal && errors.add(:roles, :ungrantable)
[Principal::STATUSES[:builtin], Principal::STATUSES[:locked]].include?(principal.status)
errors.add(:principal, :unassignable)
end end
end end
end end

@ -30,6 +30,17 @@ module Members
class CreateContract < BaseContract class CreateContract < BaseContract
attribute :project attribute :project
attribute :user_id 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
end end

@ -26,18 +26,8 @@
# See docs/COPYRIGHT.rdoc for more details. # See docs/COPYRIGHT.rdoc for more details.
#++ #++
test: module Members
adapter: mysql2 class DeleteContract < ::DeleteContract
database: travis_ci_test delete_permission :manage_members
username: travis end
encoding: utf8 end
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"

@ -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

@ -45,6 +45,10 @@ class ModelContract < Reform::Contract
@attribute_validations ||= [] @attribute_validations ||= []
end end
def attribute_permissions
@attribute_permissions ||= {}
end
def attribute_aliases def attribute_aliases
@attribute_aliases ||= {} @attribute_aliases ||= {}
end end
@ -57,12 +61,23 @@ class ModelContract < Reform::Contract
property attribute property attribute
add_writable(attribute, options[:writeable]) add_writable(attribute, options[:writeable])
attribute_permission(attribute, options[:permission])
if block if block
attribute_validations << block attribute_validations << block
end end
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 private
def add_writable(attribute, writeable) def add_writable(attribute, writeable)
@ -89,7 +104,7 @@ class ModelContract < Reform::Contract
end end
# we want to add a validation error whenever someone sets a property that we don't know. # 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... # to hook into this method and return nil for unknown properties to avoid NoMethod errors...
def read_attribute_for_validation(attribute) def read_attribute_for_validation(attribute)
if respond_to? attribute if respond_to? attribute
@ -99,13 +114,7 @@ class ModelContract < Reform::Contract
def writable_attributes def writable_attributes
@writable_attributes ||= begin @writable_attributes ||= begin
writable = collect_ancestor_attributes(:writable_attributes) reduce_writable_attributes(collect_writable_attributes)
collect_ancestor_attributes(:writable_conditions).each do |attribute, condition|
writable -= [attribute, "#{attribute}_id"] unless instance_exec(&condition)
end
writable
end end
end end
@ -141,7 +150,7 @@ class ModelContract < Reform::Contract
end end
def self.model def self.model
raise NotImplementedError @model ||= name.deconstantize.singularize.constantize
end end
# use activerecord as the base scope instead of 'activemodel' to be compatible # use activerecord as the base scope instead of 'activemodel' to be compatible
@ -166,7 +175,7 @@ class ModelContract < Reform::Contract
private private
def readonly_attributes_unchanged def readonly_attributes_unchanged
invalid_changes = model.changed - writable_attributes invalid_changes = attributes_changed_by_user - writable_attributes
invalid_changes.each do |attribute| invalid_changes.each do |attribute|
outside_attribute = collect_ancestor_attributes(:attribute_aliases)[attribute] || attribute outside_attribute = collect_ancestor_attributes(:attribute_aliases)[attribute] || attribute
@ -175,6 +184,16 @@ class ModelContract < Reform::Contract
end end
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 def run_attribute_validations
attribute_validations.each { |validation| instance_exec(&validation) } attribute_validations.each { |validation| instance_exec(&validation) }
end end
@ -208,4 +227,47 @@ class ModelContract < Reform::Contract
attributes.send(cleanup_method) attributes.send(cleanup_method)
end 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 end

@ -31,13 +31,7 @@
require 'model_contract' require 'model_contract'
module Projects module Projects
class DeleteContract < BaseContract class DeleteContract < ::DeleteContract
def validate delete_permission :admin
unless user.admin?
errors.add :base, :error_unauthorized
end
super
end
end end
end end

@ -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

@ -1,12 +1,12 @@
#-- copyright #-- copyright
# OpenProject is a project management system. # 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 # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3. # 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: # 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 # Copyright (C) 2010-2013 the ChiliProject Team
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@ -26,32 +26,22 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
object @entry => "timeEntry" module Roles
attributes :id, class CreateContract < BaseContract
:spent_on, attribute :type
:comments,
:hours
node :user do |e| def validate
partial('users/show', object: e.user) type_in_allowed
end
child :activity => :activity do
attributes :name
end
child :project do super
attributes :id, :name end
end
child :work_package do private
attributes :id, :subject
node :isVisible do |w| def type_in_allowed
w.visible? unless [Role.name, GlobalRole.name].include?(model.type)
errors.add(:type, :inclusion)
end
end
end end
end end
node :isEditable do |e|
e.editable_by?(User.current)
end

@ -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

@ -29,22 +29,8 @@
#++ #++
module TimeEntries module TimeEntries
class DeleteContract < BaseContract class DeleteContract < ::DeleteContract
def validate delete_permission -> {
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?
edit_all = user.allowed_to?(:edit_time_entries, model.project) edit_all = user.allowed_to?(:edit_time_entries, model.project)
edit_own = user.allowed_to?(:edit_own_time_entries, model.project) edit_own = user.allowed_to?(:edit_own_time_entries, model.project)
@ -53,6 +39,6 @@ module TimeEntries
else else
edit_all edit_all
end end
end }
end end
end end

@ -26,11 +26,10 @@
# See docs/COPYRIGHT.rdoc for more details. # See docs/COPYRIGHT.rdoc for more details.
#++ #++
require 'grids/base_contract'
module Versions module Versions
class DeleteContract < BaseContract class DeleteContract < ::DeleteContract
# super checks that we can manage the version delete_permission :manage_versions
def validate def validate
validate_no_work_packages_attached validate_no_work_packages_attached
@ -44,9 +43,5 @@ module Versions
errors.add(:base, :undeletable_work_packages_attached) errors.add(:base, :undeletable_work_packages_attached)
end end
def validate_model?
false
end
end end
end end

@ -34,23 +34,19 @@ module WorkPackages
class BaseContract < ::ModelContract class BaseContract < ::ModelContract
include ::Attachments::ValidateReplacements include ::Attachments::ValidateReplacements
def self.model
WorkPackage
end
attribute :subject attribute :subject
attribute :description attribute :description
attribute :status_id attribute :status_id
attribute :type_id attribute :type_id
attribute :priority_id attribute :priority_id
attribute :category_id attribute :category_id
attribute :fixed_version_id do attribute :fixed_version_id,
permission: :assign_versions do
validate_fixed_version_is_assignable validate_fixed_version_is_assignable
end end
validate :validate_no_reopen_on_closed_version validate :validate_no_reopen_on_closed_version
attribute :lock_version
attribute :project_id attribute :project_id
attribute :done_ratio, attribute :done_ratio,
@ -63,9 +59,8 @@ module WorkPackages
model.leaf? model.leaf?
} }
attribute :parent_id do attribute :parent_id,
validate_user_allowed_to_set_parent if model.changed.include?('parent_id') permission: :manage_subtasks
end
attribute :assigned_to_id do attribute :assigned_to_id do
next unless model.project next unless model.project
@ -128,13 +123,15 @@ module WorkPackages
end end
def writable_attributes def writable_attributes
ret = super
# If we're in a readonly status and did not move into that status right now # If we're in a readonly status and did not move into that status right now
# only allow other status transitions # only allow other status transitions
if model.readonly_status? && !model.status_id_change if model.readonly_status? && !model.status_id_change
return %w[status status_id] ret &= %w(status status_id)
end end
super + model.available_custom_fields.map { |cf| "custom_field_#{cf.id}" } ret
end end
private private
@ -212,10 +209,6 @@ module WorkPackages
end end
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 def validate_no_reopen_on_closed_version
if model.fixed_version_id && model.reopened? && model.fixed_version.closed? 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) errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)

@ -32,10 +32,15 @@ require 'work_packages/base_contract'
module WorkPackages module WorkPackages
class CreateContract < BaseContract 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 errors.add :author_id, :invalid if model.author != user
end end
default_attribute_permission :add_work_packages
def validate def validate
user_allowed_to_add user_allowed_to_add
@ -51,5 +56,10 @@ module WorkPackages
errors.add :base, :error_unauthorized errors.add :base, :error_unauthorized
end end
end end
def attributes_changed_by_user
# lock version is initialized by AR itself
super - ['lock_version']
end
end end
end end

@ -29,23 +29,7 @@
#++ #++
module WorkPackages module WorkPackages
class DeleteContract < ::ModelContract class DeleteContract < ::DeleteContract
def self.model delete_permission :delete_work_packages
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
end end
end end

@ -36,4 +36,8 @@ module WorkPackages::SkipAuthorizationChecks
def user_allowed_to_edit; end def user_allowed_to_edit; end
def user_allowed_to_move; end def user_allowed_to_move; end
def reduce_by_writable_permissions(attributes)
attributes
end
end end

@ -1,4 +1,5 @@
#-- encoding: UTF-8 #-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) # Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -31,7 +32,8 @@ require 'work_packages/base_contract'
module WorkPackages module WorkPackages
class UpdateContract < BaseContract 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? if model.lock_version.nil? || model.lock_version_changed?
errors.add :base, :error_conflict errors.add :base, :error_conflict
end end
@ -41,45 +43,31 @@ module WorkPackages
validate :user_allowed_to_edit validate :user_allowed_to_edit
validate :user_allowed_to_move
validate :can_move_to_milestone validate :can_move_to_milestone
default_attribute_permission :edit_work_packages
attribute_permission :project_id, :move_work_packages
private private
def user_allowed_to_edit def user_allowed_to_edit
with_unchanged_project_id do with_unchanged_project_id do
next if @can.allowed?(model, :edit) next if @can.allowed?(model, :edit) ||
next user_allowed_to_change_parent if @can.allowed?(model, :manage_subtasks) @can.allowed?(model, :assign_version) ||
@can.allowed?(model, :manage_subtasks) ||
@can.allowed?(model, :move)
next if allowed_journal_addition? next if allowed_journal_addition?
errors.add :base, :error_unauthorized errors.add :base, :error_unauthorized
end end
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 def user_allowed_to_access
unless ::WorkPackage.visible(@user).exists?(model.id) unless ::WorkPackage.visible(@user).exists?(model.id)
errors.add :base, :error_not_found errors.add :base, :error_not_found
end end
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 def with_unchanged_project_id
if model.project_id_changed? if model.project_id_changed?
current_project_id = model.project_id current_project_id = model.project_id

@ -199,7 +199,7 @@ class ProjectsController < ApplicationController
flash[:error] = I18n.t(:error_can_not_archive_project) flash[:error] = I18n.t(:error_can_not_archive_project)
redirect_back fallback_location: projects_url redirect_back fallback_location: projects_url
end end
update_demo_project_settings @project, false update_demo_project_settings @project, false
end end
@ -231,10 +231,10 @@ class ProjectsController < ApplicationController
end end
def level_list def level_list
@projects = Project.project_level_list(Project.visible) projects = Project.project_level_list(Project.visible)
respond_to do |format| respond_to do |format|
format.api format.json { render json: projects_level_list_json(projects) }
end end
end end
@ -242,6 +242,7 @@ class ProjectsController < ApplicationController
def find_optional_project def find_optional_project
return true unless params[:id] return true unless params[:id]
@project = Project.find(params[:id]) @project = Project.find(params[:id])
authorize authorize
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound

@ -29,42 +29,35 @@
class RolesController < ApplicationController class RolesController < ApplicationController
include PaginationHelper include PaginationHelper
include Roles::NotifyMixin
layout 'admin' layout 'admin'
before_action :require_admin, except: [:autocomplete_for_role] before_action :require_admin, except: [:autocomplete_for_role]
def index def index
@roles = Role @roles = roles_scope
.order(Arel.sql('builtin, position')) .page(page_param)
.page(page_param) .per_page(per_page_param)
.per_page(per_page_param)
render action: 'index', layout: false if request.xhr? render action: 'index', layout: false if request.xhr?
end end
def new 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 = roles_scope
@roles = Role.order(Arel.sql('builtin, position'))
end end
def create def create
@role = Role.new(permitted_params.role? || {permissions: Role.non_member.permissions}) @call = create_role
if @role.save @role = @call.result
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by(id: params[:copy_workflow_from])) if @call.success?
@role.workflows.copy_from_role(copy_from) flash[:notice] = t(:notice_successful_create)
end
flash[:notice] = l(:notice_successful_create)
redirect_to action: 'index' redirect_to action: 'index'
notify_changed_roles(:added, @role)
else else
define_setable_permissions @roles = roles_scope
@roles = Role.order(Arel.sql('builtin, position'))
render action: 'new' render action: 'new'
end end
@ -72,18 +65,17 @@ class RolesController < ApplicationController
def edit def edit
@role = Role.find(params[:id]) @role = Role.find(params[:id])
define_setable_permissions @call = set_role_attributes(@role, 'update')
end end
def update def update
@role = Role.find(params[:id]) @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) flash[:notice] = l(:notice_successful_update)
redirect_to action: 'index' redirect_to action: 'index'
notify_changed_roles(:updated, @role)
else else
@permissions = @role.setable_permissions
render action: 'edit' render action: 'edit'
end end
end end
@ -101,21 +93,22 @@ class RolesController < ApplicationController
def report def report
@roles = Role.order(Arel.sql('builtin, position')) @roles = Role.order(Arel.sql('builtin, position'))
@permissions = Redmine::AccessControl.permissions.select {|p| !p.public?} @permissions = OpenProject::AccessControl.permissions.reject(&:public?)
end end
def bulk_update def bulk_update
@roles = Role.order(Arel.sql('builtin, position')) @roles = roles_scope
@roles.each do |role| calls = bulk_update_roles(@roles)
new_permissions = params[:permissions][role.id.to_s].presence || []
role.permissions = new_permissions
role.save
end
flash[:notice] = l(:notice_successful_update) if calls.all?(&:success?)
redirect_to action: 'index' flash[:notice] = l(:notice_successful_update)
notify_changed_roles(:bulk_update, @roles) redirect_to action: 'index'
else
@calls = calls
@permissions = OpenProject::AccessControl.permissions.reject(&:public?)
render action: 'report'
end
end end
def autocomplete_for_role def autocomplete_for_role
@ -134,11 +127,37 @@ class RolesController < ApplicationController
private private
def notify_changed_roles(action, changed_role) def set_role_attributes(role, create_or_update)
OpenProject::Notifications.send(:roles_changed, action: action, role: changed_role) contract = "Roles::#{create_or_update.camelize}Contract".constantize
Roles::SetAttributesService
.new(user: current_user, model: role, contract_class: contract)
.call(new_params)
end 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 def default_breadcrumb
if action_name == 'index' if action_name == 'index'
@ -152,17 +171,13 @@ class RolesController < ApplicationController
true true
end end
def define_setable_permissions def new_params
@permissions = group_permissions_by_module(@role.setable_permissions) permitted_params.role? || {}
end end
def group_permissions_by_module(perms) def create_params
perms_by_module = perms.group_by {|p| p.project_module.to_s} new_params
::Redmine::AccessControl .merge(copy_workflow_from: params[:copy_workflow_from],
.sorted_modules global_role: params[:global_role])
.select {|module_name| perms_by_module[module_name].present?}
.map do |module_name|
[module_name, perms_by_module[module_name]]
end
end end
end end

@ -29,8 +29,6 @@
#++ #++
class TimelogController < ApplicationController class TimelogController < ApplicationController
menu_item :issues
before_action :disable_api, except: %i[index destroy] before_action :disable_api, except: %i[index destroy]
before_action :find_work_package, only: %i[new create] before_action :find_work_package, only: %i[new create]
before_action :find_project, only: %i[new create] before_action :find_project, only: %i[new create]
@ -72,49 +70,8 @@ class TimelogController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do 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 render layout: layout_non_or_no_menu
end 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 format.csv do
# Export all entries # Export all entries
@entries = TimeEntry @entries = TimeEntry
@ -133,15 +90,6 @@ class TimelogController < ApplicationController
end end
end end
def show
respond_to do |format|
# TODO: Implement html response
format.html do
head 406
end
end
end
def new def new
@time_entry = new_time_entry(@project, @issue, permitted_params.time_entry.to_h) @time_entry = new_time_entry(@project, @issue, permitted_params.time_entry.to_h)
@ -206,30 +154,6 @@ class TimelogController < ApplicationController
private 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 def find_time_entry
@time_entry = TimeEntry.find(params[:id]) @time_entry = TimeEntry.find(params[:id])
unless @time_entry.editable_by?(User.current) unless @time_entry.editable_by?(User.current)

@ -144,10 +144,14 @@ class VersionsController < ApplicationController
end end
def retrieve_selected_type_ids(selectable_types, default_types = nil) def retrieve_selected_type_ids(selectable_types, default_types = nil)
@selected_type_ids = if (ids = params[:type_ids]) @selected_type_ids = selected_type_ids selectable_types, default_types
ids.is_a?(Array) ? ids : ids.split('/') end
else
default_types || selectable_types def selected_type_ids(selectable_types, default_types = nil)
end.map { |t| t.id.to_s } 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
end end

@ -235,6 +235,22 @@ module ProjectsHelper
s s
end 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) def projects_with_levels_order_sensitive(projects, &block)
if sorted_by_lft? if sorted_by_lft?
project_tree(projects, &block) project_tree(projects, &block)

@ -28,4 +28,27 @@
#++ #++
module RolesHelper 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 end

@ -35,12 +35,6 @@ module WarningBarHelper
OpenProject::Database.migrations_pending? OpenProject::Database.migrations_pending?
end 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 # By default, never show a warning bar in the
# test mode due to overshadowing other elements. # test mode due to overshadowing other elements.

@ -61,7 +61,7 @@ class ProjectMailer < BaseMailer
open_project_headers 'Source-Project' => source_project.identifier, open_project_headers 'Source-Project' => source_project.identifier,
'Author' => user.login 'Author' => user.login
message_id project, user message_id source_project, user
with_locale_for(user) do with_locale_for(user) do
subject = I18n.t('copy_project.failed', source_project_name: source_project.name) subject = I18n.t('copy_project.failed', source_project_name: source_project.name)

@ -175,6 +175,21 @@ class Attachment < ActiveRecord::Base
content_type || fallback content_type || fallback
end 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 def extract_fulltext
return unless OpenProject::Database.allows_tsv? return unless OpenProject::Database.allows_tsv?
job = ExtractFulltextJob.new(id) job = ExtractFulltextJob.new(id)
@ -183,16 +198,26 @@ class Attachment < ActiveRecord::Base
# Extract the fulltext of any attachments where fulltext is still nil. # Extract the fulltext of any attachments where fulltext is still nil.
# This runs inline and not in a asynchronous worker. # 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? 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 = ExtractFulltextJob.new(id)
job.perform
if run_now
job.perform
else
Delayed::Job.enqueue job, priority: ::ApplicationJob.priority_number(:low)
end
end end
end end
def self.force_extract_fulltext def self.force_extract_fulltext
return unless OpenProject::Database.allows_tsv? return unless OpenProject::Database.allows_tsv?
Attachment.pluck(:id).each do |id| Attachment.pluck(:id).each do |id|
job = ExtractFulltextJob.new(id) job = ExtractFulltextJob.new(id)
job.perform job.perform

@ -108,15 +108,8 @@ module CustomField::OrderStatements
end end
def select_custom_values_as_group 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 <<-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}' 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.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id} AND cv_sort.custom_field_id=#{id}
@ -125,15 +118,8 @@ module CustomField::OrderStatements
end end
def select_custom_values_joined_options_as_group 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 <<-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 LEFT JOIN #{CustomValue.table_name} cv_sort
ON co_sort.id = CAST(cv_sort.value AS decimal(60,3)) ON co_sort.id = CAST(cv_sort.value AS decimal(60,3))
WHERE cv_sort.customized_type='#{self.class.customized_class.name}' WHERE cv_sort.customized_type='#{self.class.customized_class.name}'

@ -56,17 +56,10 @@ class Journal < ActiveRecord::Base
# Ensure that no INSERT/UPDATE/DELETE statements as well as other code inside :with_write_lock # 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. # 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. # end of that transaction.
# Note for MySQL: THis method does not currently change anything (no locking at all)
def self.with_write_lock(journable) def self.with_write_lock(journable)
lock_name = lock_name = "journal.#{journable.class}.#{journable.id}"
if OpenProject::Database.mysql?
# MySQL only supports a single lock
"journals.write_lock"
else
"journal.#{journable.class}.#{journable.id}"
end
result = Journal.with_advisory_lock_result(lock_name, timeout_seconds: 60) do result = Journal.with_advisory_lock_result(lock_name, timeout_seconds: 60) do
yield yield

@ -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 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 # (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. # 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}") Journal.from("(#{sql_rough_group(journable, until_version, journal_id)}) #{table_name}")
.joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(2, journable, until_version, journal_id)}) addition .joins(Arel.sql("LEFT OUTER JOIN (#{sql_rough_group(journable, until_version, journal_id)}) addition
ON #{sql_on_groups_belong_condition(table_name, '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)}")) ON #{sql_on_groups_belong_condition('predecessor', table_name)}"))
.where(Arel.sql('predecessor.id IS NULL')) .where(Arel.sql('predecessor.id IS NULL'))
.order(Arel.sql("COALESCE(addition.created_at, #{table_name}.created_at) ASC")) .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 # 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 # "group_number" to the result. This allows to compare a group resulting from this query with
# its predecessor and successor. # 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 if until_version && !journable
raise 'need to provide a journable, when specifying a version limit' raise 'need to provide a journable, when specifying a version limit'
elsif journable && journable.id.nil? elsif journable && journable.id.nil?
@ -175,8 +175,8 @@ class Journal::AggregatedJournal
conditions = additional_conditions(journable, until_version, journal_id) conditions = additional_conditions(journable, until_version, journal_id)
"SELECT predecessor.*, #{sql_group_counter(uid)} AS group_number "SELECT predecessor.*, #{sql_group_counter} AS group_number
FROM #{sql_rough_group_from_clause(uid)} FROM journals predecessor
#{sql_rough_group_join(conditions[:join_conditions])} #{sql_rough_group_join(conditions[:join_conditions])}
#{sql_rough_group_where(conditions[:where_conditions])} #{sql_rough_group_where(conditions[:where_conditions])}
#{sql_rough_group_order}" #{sql_rough_group_order}"
@ -229,33 +229,10 @@ class Journal::AggregatedJournal
"ORDER BY predecessor.created_at" "ORDER BY predecessor.created_at"
end end
# The "group_number" required in :sql_rough_group has to be generated differently depending on # This method returns the appropriate statement to be used inside a SELECT to
# the DBMS used. This method returns the appropriate statement to be used inside a SELECT to
# obtain the current group number. # obtain the current group number.
# The :uid parameter allows to define non-conflicting variable names (for MySQL). def sql_group_counter
def sql_group_counter(uid) 'row_number() OVER (ORDER BY predecessor.version ASC)'
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}"
end end
# Similar to the WHERE statement used in :sql_rough_group. However, this condition will # Similar to the WHERE statement used in :sql_rough_group. However, this condition will
@ -281,13 +258,8 @@ class Journal::AggregatedJournal
return '(true = true)' return '(true = true)'
end end
if OpenProject::Database.mysql? difference = "(#{successor}.created_at - #{predecessor}.created_at)"
difference = "TIMESTAMPDIFF(second, #{predecessor}.created_at, #{successor}.created_at)" threshold = "interval '#{aggregation_time_seconds} second'"
threshold = aggregation_time_seconds
else
difference = "(#{successor}.created_at - #{predecessor}.created_at)"
threshold = "interval '#{aggregation_time_seconds} second'"
end
"(#{difference} > #{threshold})" "(#{difference} > #{threshold})"
end end

@ -31,10 +31,8 @@ class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
include Redmine::I18n include Redmine::I18n
class UnauthorizedAction < StandardError; class UnauthorizedAction < StandardError; end
end class MissingInformation < StandardError; end
class MissingInformation < StandardError;
end
attr_reader :email, :user attr_reader :email, :user

@ -98,7 +98,7 @@ class MemberRole < ActiveRecord::Base
end end
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? Watcher.prune(user: users, project_id: member.project_id) unless users.empty?
end end

@ -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

@ -82,12 +82,6 @@ class Principal < ActiveRecord::Base
firstnamelastname = "((firstname || ' ') || lastname)" firstnamelastname = "((firstname || ' ') || lastname)"
lastnamefirstname = "((lastname || ' ') || firstname)" 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(',', '')}%" s = "%#{q.to_s.downcase.strip.tr(',', '')}%"
where(['LOWER(login) LIKE :s OR ' + where(['LOWER(login) LIKE :s OR ' +

@ -715,13 +715,13 @@ class Project < ActiveRecord::Base
@allowed_permissions ||= begin @allowed_permissions ||= begin
names = enabled_modules.loaded? ? enabled_module_names : enabled_modules.pluck(:name) 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
end end
def allowed_actions def allowed_actions
@actions_allowed ||= allowed_permissions @actions_allowed ||= allowed_permissions
.map { |permission| Redmine::AccessControl.allowed_actions(permission) } .map { |permission| OpenProject::AccessControl.allowed_actions(permission) }
.flatten .flatten
end end

@ -43,6 +43,8 @@ module Project::Copy
def copy_attributes(project) def copy_attributes(project)
super super
with_model(project) do |project| with_model(project) do |project|
# Clear enabled modules
self.enabled_modules = []
self.enabled_module_names = project.enabled_module_names self.enabled_module_names = project.enabled_module_names
self.types = project.types self.types = project.types
self.work_package_custom_fields = project.work_package_custom_fields self.work_package_custom_fields = project.work_package_custom_fields

@ -1,4 +1,5 @@
#-- encoding: UTF-8 #-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) # Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -27,32 +28,34 @@
# See docs/COPYRIGHT.rdoc for more details. # See docs/COPYRIGHT.rdoc for more details.
#++ #++
## module Queries::Filters::Strategies
# Hack against rabl 0.13.0 which applies config.include_child_root to class Relation < BaseStrategy
# #collection as well as to #child calls as you would expect. delegate :allowed_values_subset,
# to: :filter
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
alias_method :to_hash_without_hack, :to_hash self.supported_operators = ::Relation::TYPES.keys + %w(parent children)
alias_method :to_hash, :to_hash_with_hack self.default_operator = ::Relation::TYPE_RELATES
end
def validate
unique_values = values.uniq
allowed_and_desired_values = allowed_values_subset & unique_values
class Builder if allowed_and_desired_values.sort != unique_values.sort
def to_hash_with_hack(object = nil, settings = nil, options = nil) errors.add(:values, :inclusion)
if @options[:building_collection] && !@options[:child_root]
@options[:root_name] = false
end 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 end
alias_method :to_hash_without_hack, :to_hash private
alias_method :to_hash, :to_hash_with_hack
def too_many_values
values.reject(&:blank?).length > 1
end
end end
end end

@ -62,6 +62,18 @@ class Queries::NotExistingFilter < Queries::Filters::Base
} }
end 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 def attributes_hash
nil nil
end end

@ -47,7 +47,20 @@ module Queries::Operators
Queries::Operators::Ago, Queries::Operators::Ago,
Queries::Operators::OnDate, Queries::Operators::OnDate,
Queries::Operators::BetweenDate, 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 OPERATORS = Hash[*(operators.map { |o| [o.symbol.to_s, o] }).flatten].freeze

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -35,6 +35,7 @@ module Queries::Projects
query = ::Queries::Projects::ProjectQuery query = ::Queries::Projects::ProjectQuery
register.filter query, filters::AncestorFilter register.filter query, filters::AncestorFilter
register.filter query, filters::TypeFilter
register.filter query, filters::ActiveOrArchivedFilter register.filter query, filters::ActiveOrArchivedFilter
register.filter query, filters::NameAndIdentifierFilter register.filter query, filters::NameAndIdentifierFilter
register.filter query, filters::CustomFieldFilter register.filter query, filters::CustomFieldFilter

@ -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

@ -75,6 +75,7 @@ module Queries::WorkPackages
register.filter Query, filters_module::CommentFilter register.filter Query, filters_module::CommentFilter
register.filter Query, filters_module::SubjectOrIdFilter register.filter Query, filters_module::SubjectOrIdFilter
register.filter Query, filters_module::ManualSortFilter register.filter Query, filters_module::ManualSortFilter
register.filter Query, filters_module::RelatableFilter
columns_module = Queries::WorkPackages::Columns columns_module = Queries::WorkPackages::Columns

@ -38,7 +38,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
end end
def value_objects def value_objects
objects = scope.find(no_templated_values) objects = visible_scope.find(no_templated_values)
if has_templated_value? if has_templated_value?
objects << ::Queries::Filters::TemplatedValue.new(WorkPackage) objects << ::Queries::Filters::TemplatedValue.new(WorkPackage)
@ -52,7 +52,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
end end
def available? def available?
scope.exists? visible_scope.exists?
end end
def ar_object_filter? def ar_object_filter?
@ -60,7 +60,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
end end
def allowed_values_subset 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? if has_templated_value?
id_values + templated_value_keys id_values + templated_value_keys
@ -71,7 +71,7 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
private private
def scope def visible_scope
if context.project if context.project
WorkPackage WorkPackage
.visible .visible

@ -1,4 +1,5 @@
#-- encoding: UTF-8 #-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) # Copyright (C) 2012-2018 the OpenProject Foundation (OPF)

@ -1,4 +1,5 @@
#-- encoding: UTF-8 #-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) # Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -26,34 +27,56 @@
# #
# See docs/COPYRIGHT.rdoc for more details. # See docs/COPYRIGHT.rdoc for more details.
#++ #++
require 'legacy_spec_helper'
describe Redmine::AccessControl do class Queries::WorkPackages::Filter::RelatableFilter < Queries::WorkPackages::Filter::WorkPackageFilter
before do include Queries::WorkPackages::Filter::FilterForWpMixin
@access_module = Redmine::AccessControl
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 end
it 'should permissions' do def relateable_to
perms = @access_module.permissions WorkPackage.relateable_to(from)
assert perms.is_a?(Array)
assert perms.first.is_a?(Redmine::AccessControl::Permission)
end end
it 'should module permission' do def from
perm = @access_module.permission(:view_work_packages) WorkPackage.find(values.first)
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')
end end
it 'should no module permission' do def canonical_operator
perm = @access_module.permission(:edit_project) Relation.canonical_type(operator)
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')
end end
end end

@ -32,7 +32,6 @@
class Queries::WorkPackages::Filter::RelatesFilter < class Queries::WorkPackages::Filter::RelatesFilter <
Queries::WorkPackages::Filter::WorkPackageFilter Queries::WorkPackages::Filter::WorkPackageFilter
include ::Queries::WorkPackages::Filter::FilterOnUndirectedRelationsMixin include ::Queries::WorkPackages::Filter::FilterOnUndirectedRelationsMixin
def relation_type def relation_type

@ -30,7 +30,6 @@
class Queries::WorkPackages::Filter::SubjectOrIdFilter < class Queries::WorkPackages::Filter::SubjectOrIdFilter <
Queries::WorkPackages::Filter::WorkPackageFilter Queries::WorkPackages::Filter::WorkPackageFilter
include Queries::WorkPackages::Filter::OrFilterForWpMixin include Queries::WorkPackages::Filter::OrFilterForWpMixin
CONTAINS_OPERATOR = '~'.freeze CONTAINS_OPERATOR = '~'.freeze

@ -44,4 +44,12 @@ class Queries::WorkPackages::Filter::WorkPackageFilter < ::Queries::Filters::Bas
def includes def includes
nil nil
end 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 end

@ -47,19 +47,18 @@ class ::Query::Results
# Returns the work package count # Returns the work package count
def work_package_count def work_package_count
WorkPackage.visible work_package_scope
.joins(all_filter_joins) .joins(all_filter_joins)
.includes(:status, :project) .includes(:status, :project)
.where(query.statement) .where(query.statement)
.references(:statuses, :projects) .references(:statuses, :projects)
.count .count
rescue ::ActiveRecord::StatementInvalid => e rescue ::ActiveRecord::StatementInvalid => e
raise ::Query::StatementInvalid.new(e.message) raise ::Query::StatementInvalid.new(e.message)
end end
def work_packages def work_packages
WorkPackage work_package_scope
.visible
.where(query.statement) .where(query.statement)
.where(options[:conditions]) .where(options[:conditions])
.includes(all_includes) .includes(all_includes)
@ -100,6 +99,12 @@ class ::Query::Results
private private
def work_package_scope
WorkPackage
.visible
.merge(filter_merges)
end
def all_includes def all_includes
(%i(status project) + (%i(status project) +
includes_for_columns(include_columns) + includes_for_columns(include_columns) +
@ -210,6 +215,13 @@ class ::Query::Results
query.filters.map(&:joins).flatten.compact query.filters.map(&:joins).flatten.compact
end 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) def clean_symbol_list(list)
list.flatten.compact.uniq.map(&:to_sym) list.flatten.compact.uniq.map(&:to_sym)
end end

@ -63,7 +63,9 @@ class Role < ActiveRecord::Base
validates_length_of :name, maximum: 30 validates_length_of :name, maximum: 30
def self.givable def self.givable
where(builtin: NON_BUILTIN).order(Arel.sql('position')) where(builtin: NON_BUILTIN)
.where(type: 'Role')
.order(Arel.sql('position'))
end end
def permissions def permissions
@ -132,14 +134,6 @@ class Role < ActiveRecord::Base
end end
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, # Return the builtin 'non member' role. If the role doesn't exist,
# it will be created on the fly. # it will be created on the fly.
def self.non_member def self.non_member
@ -179,12 +173,12 @@ class Role < ActiveRecord::Base
private private
def allowed_permissions def allowed_permissions
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.map(&:name) @allowed_permissions ||= permissions + OpenProject::AccessControl.public_permissions.map(&:name)
end end
def allowed_actions def allowed_actions
@actions_allowed ||= allowed_permissions.map do |permission| @actions_allowed ||= allowed_permissions.map do |permission|
Redmine::AccessControl.allowed_actions(permission) OpenProject::AccessControl.allowed_actions(permission)
end.flatten end.flatten
end end

@ -238,7 +238,7 @@ class User < Principal
def self.activate_user!(user, session) def self.activate_user!(user, session)
if session[:invitation_token] if session[:invitation_token]
token = Token::Invitation.find_by_plaintext_value 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 if user.id == invited_id
user.activate! user.activate!
@ -476,28 +476,28 @@ class User < Principal
# Find a user account by matching the exact login and then a case-insensitive # Find a user account by matching the exact login and then a case-insensitive
# version. Exact matches will be given priority. # version. Exact matches will be given priority.
def self.find_by_login(login) 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 # 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 # 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 end
def self.find_by_rss_key(key) def self.find_by_rss_key(key)
return nil unless Setting.feeds_enabled? return nil unless Setting.feeds_enabled?
token = Token::Rss.find_by(value: key) token = Token::Rss.find_by(value: key)
if token && token.user.active? if token&.user&.active?
token.user token.user
end end
end end
def self.find_by_api_key(key) def self.find_by_api_key(key)
return nil unless Setting.rest_api_enabled? return nil unless Setting.rest_api_enabled?
token = Token::Api.find_by_plaintext_value(key) token = Token::Api.find_by_plaintext_value(key)
if token && token.user.active? if token&.user&.active?
token.user token.user
end end
end end
@ -513,16 +513,11 @@ class User < Principal
skip_suffix_check, regexp = mail_regexp(mail) skip_suffix_check, regexp = mail_regexp(mail)
# If the recipient part already contains a suffix, don't expand # If the recipient part already contains a suffix, don't expand
return where("LOWER(mail) = ?", mail) if skip_suffix_check if skip_suffix_check
where("LOWER(mail) = ?", mail)
command = else
if OpenProject::Database.mysql? where("LOWER(mail) ~* ?", regexp)
'REGEXP' end
else
'~*'
end
where("LOWER(mail) #{command} ?", regexp)
end end
## ##
@ -563,7 +558,7 @@ class User < Principal
roles = [] roles = []
# No role on archived projects # No role on archived projects
return roles unless project && project.active? return roles unless project&.active?
# Return all roles if user is admin # Return all roles if user is admin
return Role.givable.to_a if admin? return Role.givable.to_a if admin?

@ -59,7 +59,7 @@ class User::ProjectAuthorizationCache
private private
def normalized_permission_name(action) def normalized_permission_name(action)
Redmine::AccessControl.permission(action) OpenProject::AccessControl.permission(action)
end end
def projects_by_actions def projects_by_actions

@ -260,7 +260,7 @@ class WorkPackage < ActiveRecord::Base
def assignable_versions def assignable_versions
@assignable_versions ||= begin @assignable_versions ||= begin
current_version = fixed_version_id_changed? ? Version.find_by(id: fixed_version_id_was) : fixed_version 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
end end

@ -51,7 +51,8 @@ class WorkPackagePolicy < BasePolicy
duplicate: copy_allowed?(work_package), # duplicating is another form of copying duplicate: copy_allowed?(work_package), # duplicating is another form of copying
delete: delete_allowed?(work_package), delete: delete_allowed?(work_package),
manage_subtasks: manage_subtasks_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 end
@ -125,4 +126,12 @@ class WorkPackagePolicy < BasePolicy
@comment_cache[work_package.project] @comment_cache[work_package.project]
end 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 end

@ -66,6 +66,7 @@ module BasicData
add_work_packages add_work_packages
move_work_packages move_work_packages
edit_work_packages edit_work_packages
assign_versions
add_work_package_notes add_work_package_notes
edit_own_work_package_notes edit_own_work_package_notes
manage_work_package_relations manage_work_package_relations
@ -141,7 +142,11 @@ module BasicData
end end
def project_admin 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 end
def non_member def non_member

@ -127,9 +127,9 @@ module API
end end
def highlighted_attributes_from_params(params) 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| highlighted_attributes.map do |attr|
convert_attribute(attr) convert_attribute(attr)

@ -34,7 +34,7 @@ module API
self.current_user = user self.current_user = user
end end
def call(params) def call(params, valid_subset: false)
parsed = ::API::V3::ParseQueryParamsService parsed = ::API::V3::ParseQueryParamsService
.new .new
.call(params) .call(params)
@ -42,7 +42,7 @@ module API
if parsed.success? if parsed.success?
::UpdateQueryFromParamsService ::UpdateQueryFromParamsService
.new(query, current_user) .new(query, current_user)
.call(parsed.result) .call(parsed.result, valid_subset: valid_subset)
else else
parsed parsed
end end

@ -37,10 +37,10 @@ module API
self.current_user = user self.current_user = user
end end
def call(params = {}) def call(params = {}, valid_subset: false)
update = UpdateQueryFromV3ParamsService update = UpdateQueryFromV3ParamsService
.new(query, current_user) .new(query, current_user)
.call(params) .call(params, valid_subset: valid_subset)
if update.success? if update.success?
representer = results_to_representer(params) representer = results_to_representer(params)

@ -132,9 +132,9 @@ class Authorization::ProjectQuery < Authorization::AbstractQuery
def self.permissions(action) def self.permissions(action)
if action.is_a?(Hash) if action.is_a?(Hash)
Redmine::AccessControl.allow_actions(action) OpenProject::AccessControl.allow_actions(action)
else else
[Redmine::AccessControl.permission(action)].compact [OpenProject::AccessControl.permission(action)].compact
end end
end end

@ -46,7 +46,7 @@ class Authorization::UserAllowedQuery < Authorization::AbstractUserQuery
has_role = roles_table[:id].not_eq(nil) has_role = roles_table[:id].not_eq(nil)
has_permission = role_permissions_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 has_role
else else
has_role.and(has_permission) has_role.and(has_permission)
@ -75,7 +75,7 @@ class Authorization::UserAllowedQuery < Authorization::AbstractUserQuery
transformations.register :all, transformations.register :all,
:role_permissions_join, :role_permissions_join,
after: [:roles_join] do |statement, action| after: [:roles_join] do |statement, action|
if Redmine::AccessControl.permission(action).public? if OpenProject::AccessControl.permission(action).public?
statement statement
else else
statement.outer_join(role_permissions_table) statement.outer_join(role_permissions_table)

@ -65,16 +65,16 @@ module BaseServices
def set_attributes(params) def set_attributes(params)
attributes_service_class attributes_service_class
.new(user: user, .new(user: user,
model: new_instance, model: new_instance(params),
contract_class: contract_class) contract_class: contract_class)
.call(params) .call(params)
end end
def after_save(attributes_call) def after_save(_attributes_call)
# nothing for now but subclasses can override # nothing for now but subclasses can override
end end
def new_instance def new_instance(_params)
instance_class.new instance_class.new
end end

@ -35,6 +35,13 @@ module BaseServices
def initialize(user:, model:, contract_class:) def initialize(user:, model:, contract_class:)
self.user = user self.user = user
self.model = model 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 self.contract_class = contract_class
end end

@ -28,14 +28,4 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Members::CreateService < ::BaseServices::Create class Members::CreateService < ::BaseServices::Create; end
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

@ -1,12 +1,12 @@
#-- copyright #-- copyright
# OpenProject is a project management system. # 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 # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3. # 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: # 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 # Copyright (C) 2010-2013 the ChiliProject Team
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@ -26,5 +26,4 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
collection @entries => "timeEntries" class Members::DeleteService < ::BaseServices::Delete; end
extends 'timelog/show'

@ -1,12 +1,14 @@
#-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # 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 # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3. # 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: # 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 # Copyright (C) 2010-2013 the ChiliProject Team
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@ -26,9 +28,4 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
object @user class Members::UpdateService < ::BaseServices::Update; end
attributes :id,
:name,
:firstname,
:lastname

@ -1,12 +1,14 @@
#-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # 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 # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3. # 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: # 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 # Copyright (C) 2010-2013 the ChiliProject Team
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
@ -26,24 +28,36 @@
# See doc/COPYRIGHT.rdoc for more details. # 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: private
# [
# { :project => <Project object>,
# :level => <hierarchy level> }
# ]
node(:id) { |p| p[:project].id } def create(params)
node(:name) { |p| p[:project].name } copy_workflow_id = params.delete(:copy_workflow_from)
node(:identifier) { |p| p[:project].identifier }
node(:has_children) { |p| !p[:project].leaf? }
node(:level) { |p| p[:level] }
node(:created_on, if: lambda { |p| p[:project].created_on }) do |p| super_call = super
p[:project].created_on.utc
end 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| def copy_workflows(copy_workflow_id, role)
p[:project].updated_on.utc if copy_workflow_id.present? && (copy_from = Role.find_by(id: copy_workflow_id))
role.workflows.copy_from_role(copy_from)
end
end
end end

@ -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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save