Merge branch 'dev' into feature/with-angular-busy

Conflicts:
	bower.json
pull/1478/head
Till Breuer 11 years ago
commit 8df7b9f6f3
  1. 2
      Gemfile
  2. 18
      Gemfile.lock
  3. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.eot
  4. 11
      app/assets/fonts/openproject_icon/openproject-icon-font.svg
  5. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.ttf
  6. BIN
      app/assets/fonts/openproject_icon/openproject-icon-font.woff
  7. 13
      app/assets/javascripts/angular/controllers/dialogs/group-by.js
  8. 4
      app/assets/javascripts/angular/services/query-service.js
  9. 11
      app/assets/javascripts/angular/ui_components/has-dropdown-menu-directive.js
  10. 4
      app/assets/stylesheets/content/_modal.md
  11. 3
      app/assets/stylesheets/content/_wiki.sass
  12. 1
      app/assets/stylesheets/default/main.css.erb
  13. 5
      app/assets/stylesheets/fonts/_openproject_icon_font.md
  14. 33
      app/assets/stylesheets/fonts/_openproject_icon_font.sass
  15. 3
      app/assets/stylesheets/global/_variables.sass
  16. 12
      app/assets/stylesheets/scm.css.sass
  17. 125
      app/controllers/api/experimental/concerns/can.rb
  18. 17
      app/controllers/api/experimental/work_packages_controller.rb
  19. 2
      app/controllers/api/v2/planning_elements_controller.rb
  20. 9
      app/views/api/experimental/work_packages/index.api.rabl
  21. 4
      app/views/repositories/_dir_list_content.html.erb
  22. 2
      bower.json
  23. 4
      config/routes.rb
  24. 38
      db/migrate/20130807083716_change_attachment_journals_description_length.rb
  25. 12
      doc/CHANGELOG.md
  26. 41
      lib/api/root.rb
  27. 52
      lib/api/v3/queries/queries_api.rb
  28. 54
      lib/api/v3/queries/query_model.rb
  29. 76
      lib/api/v3/queries/query_representer.rb
  30. 1
      lib/api/v3/root.rb
  31. 2
      lib/api/v3/work_packages/work_packages_api.rb
  32. 4
      lib/redmine.rb
  33. 6
      public/templates/work_packages/column_context_menu.html
  34. 377
      spec/api/query_resource_spec.rb
  35. 18
      spec/controllers/api/experimental/work_packages_controller_spec.rb
  36. 23
      spec/controllers/api/v2/planning_elements_controller_spec.rb
  37. 63
      spec/views/api/experimental/work_packages/index_api_json_spec.rb

@ -194,7 +194,7 @@ end
# API gems
gem 'grape', '~> 0.7.0'
gem 'representable', :github => 'finnlabs/representable'
gem 'representable', git: 'https://github.com/finnlabs/representable'
gem 'roar', '~> 0.12.6'
gem 'reform', require: false

@ -1,12 +1,3 @@
GIT
remote: git://github.com/finnlabs/representable.git
revision: 5f8fbcb1e61720699135c39be3f36a725ca870ad
specs:
representable (1.8.1)
multi_json
nokogiri
uber
GIT
remote: https://github.com/Compass/compass-rails
revision: f97a4f41518204683aeec7037da3d9b8c57ef4cb
@ -22,6 +13,15 @@ GIT
rack-protection (1.5.2)
rack
GIT
remote: https://github.com/finnlabs/representable
revision: 5f8fbcb1e61720699135c39be3f36a725ca870ad
specs:
representable (1.8.1)
multi_json
nokogiri
uber
GIT
remote: https://github.com/finnlabs/rspec-example_disabler.git
revision: deb9c38e3f4e3688724583ac1ff58e1ae8aba409

@ -170,7 +170,6 @@
<glyph unicode="&#57506;" d="M85 398l342 0 0-227-342 0z m399-284l0 341-456 0 0-341 143 0 0-29-57 0 0-28 284 0 0 28-57 0 0 29z"/>
<glyph unicode="&#57507;" d="M256 142l-57 0 0-57 57 0z m142 114l-284 0 0 199 284 0z m-57-199l-170 0 0 114 170 0z m-313 427l0-456 399 0 57 57 0 399z"/>
<glyph unicode="&#57508;" d="M454 60l11-5 4-10-4-11-11-5-391 0c-9 0-16 7-16 16 0 8 7 15 16 15z m-152 422l0 0c15-3 8 0 21-9l123-124c12-12 12-32 0-44l-188-190c-12-12-35-25-52-25l-46 0c-16 0-40 13-51 25l-70 74c-12 12-12 31 0 43l241 241 10 7z m-132-164l-110-110 70-72c6-6 22-16 30-16l46 0c9 0 21 10 27 16l58 61z"/>
<glyph unicode="&#57509;" d="M150 89c0-33-27-61-61-61-33 0-61 28-61 61 0 34 28 61 61 61 34 0 61-27 61-61m177-57l-80 0c-1 57-24 111-64 151-40 41-94 63-151 64l0 80c162-2 293-133 295-295m157 0l-81 0c-2 204-167 369-370 372l0 80c247-3 448-204 451-452"/>
<glyph unicode="&#57510;" d="M372 114c-2 142-116 256-258 258l0 55c170-2 311-143 313-313z m-108 0c-1 28-18 77-45 105-28 28-77 45-105 45l0 55c114-1 204-91 205-205z m-108 0c-23 0-42 19-42 42 0 23 19 42 42 42 23 0 42-19 42-42 0-23-19-42-42-42m242 370l-284 0c-47 0-86-39-86-86l0-284c0-47 39-86 86-86l284 0c47 0 86 39 86 86l0 284c0 47-39 86-86 86"/>
<glyph unicode="&#57511;" d="M298 452c-94 0-172-66-192-153l-76 1 103-165 106 162-70 1c18 54 68 94 129 94 75 0 136-62 136-137 0-76-61-137-136-137-28 0-54 8-76 23l-33-51c31-20 69-32 109-32 108 0 197 88 197 197 0 108-89 197-197 197"/>
<glyph unicode="&#57512;" d="M416 194l68-44 0-67-68-45-67 45 0 67z m0-21l-48-33 48-33 49 33z m-38 205l-59 40 59 39 59-39z m0 106l-81-55 0-81 81-54 81 54 0 81z m-209-119l140-93 0-141-140-93-141 93 0 141z m0-46l-103-68 103-68 102 68z"/>
@ -241,8 +240,12 @@
<glyph unicode="&#57573;" d="M174 435l171 0 0-56-171 0z m0-150l171 0 0-51-171 0z m0-114l171 0 0-52-171 0z m290-104l-8 0 0 17 17 0 0-9c0-3-4-8-9-8z m-25 0l-17 0 0 17 17 0z m-35 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-18 0 0 17 18 0z m-35 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-8 0c-6 0-9 3-9 8l0 9 17 0z m0 35l-17 0 0 19 17 0z m0 36l-17 0 0 19 17 0z m0 38l-17 0 0 19 17 0z m0 36l-17 0 0 18 17 0z m0 35l-17 0 0 19 17 0z m0 38l-17 0 0 19 17 0z m0 36l-17 0 0 8c0 6 3 9 9 9l8 0z m376 0l-17 0 0 17 17 0z m-35 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m-34 0l-18 0 0 17 18 0z m-35 0l-17 0 0 17 17 0z m-34 0l-17 0 0 17 17 0z m376 0l-17 0 0 17 8 0c5 0 9-3 9-9z m0-219l-17 0 0 19 17 0z m0 36l-17 0 0 19 17 0z m0 38l-17 0 0 19 17 0z m0 36l-17 0 0 18 17 0z m0 35l-17 0 0 19 17 0z m0 38l-17 0 0 19 17 0z"/>
<glyph unicode="&#57575;" d="M63 381l384 0 0-35-384 0z m0-60l384 0 0-34-384 0z m97 184l0-95-97 0z m-97-387l384 0 0-34-384 0z m0-60l384 0 0-34-384 0z m97 184l0-95-97 0z"/>
<glyph unicode="&#57576;" d="M227 174l-68-114-69 114z m-77 278l17 0 0-329-17 0z m111-114l68 114 69-114z m60 51l17 0 0-329-17 0z"/>
<glyph unicode="&#57577;" d="M211 195l-26 0 0-47 15 0 0 15 11 0c11 0 17 7 17 16 0 9-6 16-17 16z m-2-20l-9 0 0 8 9 0c2 0 4-1 4-4 0-2-2-4-4-4z m47 20l-21 0 0-47 21 0c15 0 26 9 26 24 0 15-11 23-26 23z m0-35l-7 0 0 23 7 0c8 0 12-5 12-11 0-6-5-12-12-12z m35-12l14 0 0 18 21 0 0 12-21 0 0 5 22 0 0 12-36 0z m120 300l0 0 0 21c0 4-4 8-9 8l-168 0-133-132 0-302c0-4 4-8 9-8l292 0c5 0 9 4 9 8l0 13 0 0z m-259-362l0 241 91 0c5 0 9 4 9 9l0 90 108 0 0-340z"/>
<glyph unicode="&#57578;" d="M207 183c4 0 8-3 9-6l12 5c-2 7-9 14-21 14-15 0-26-10-26-24 0-15 11-25 26-25 12 0 19 8 21 14l-12 6c-1-4-5-7-9-7-7 0-12 5-12 12 0 6 5 11 12 11z m42-1c0 1 1 2 3 2 5 0 10-2 14-5l8 10c-5 5-12 7-20 7-13 0-20-8-20-16 0-18 27-14 27-18 0-2-3-3-6-3-6 0-11 3-15 6l-7-10c5-5 12-8 22-8 12 0 20 6 20 17 0 17-26 13-26 18z m56-19l-11 32-16 0 18-47 18 0 17 47-16 0z m106 285l0 0 0 21c0 4-4 8-9 8l-168 0-133-132 0-302c0-4 4-8 9-8l292 0c5 0 9 4 9 8l0 13 0 0z m-259-362l0 241 91 0c5 0 9 4 9 9l0 90 108 0 0-340z"/>
<glyph unicode="&#57574;" d="M210 328l84 0 0-279-84 0z m256 128l-213-277-215 277z"/>
<glyph unicode="&#57369;" d="M72 14c7 18 15 34 23 49l25 45-45 83 20 0 36-68 36 68 19 0-44-82 12-20 13-24c4-8 8-16 12-25 4-9 8-17 11-25l-18 0c-3 5-5 10-8 17-3 6-6 13-10 20l-12 22-11 20-12-20-11-22c-4-7-7-14-10-20-3-7-6-13-8-18z m150 45l0 218 17 4 0-222c0-5 1-9 2-13 1-3 2-6 3-8 2-2 4-3 7-4 2-1 5-2 9-2l-3-20c-6 0-11 1-15 3-4 1-8 4-11 8-3 4-5 9-7 15-1 5-2 12-2 21z m58-38l4 20c1-1 2-1 4-2 2-1 4-3 7-4 3-1 7-2 11-3 4-1 9-1 14-1 10 0 18 2 25 6 6 4 10 11 10 21 0 5-1 9-2 12-1 4-3 7-6 9-2 3-6 6-10 8l-17 9c-5 3-10 5-14 8-5 3-9 6-12 10-4 4-7 8-9 13-2 6-3 12-3 19 0 15 4 27 13 36 8 9 20 13 34 13 10 0 17-1 24-3 6-2 10-4 12-6l-3-20c-3 1-6 3-11 5-5 3-13 4-22 4-4 0-8-1-12-2-3-1-6-2-9-5-3-2-5-4-6-8-2-3-3-7-3-12 0-4 1-8 2-12 2-3 4-6 7-9 3-2 6-5 10-7l14-7c5-3 10-6 15-9 5-3 9-6 13-10 3-4 6-9 9-14 2-6 3-13 3-21 0-16-5-28-14-36-9-9-22-13-38-13-12 0-21 2-27 4-7 3-11 5-13 7z m-242 491l31 0 0-310-31 0z m15 0l249 0 0-62-249 0z m357-140l31 0 0-341-31 0z m31-15l-155 0 0 0 0 155 14 0 140-141 0-14"/>
<glyph unicode="&#57577;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-259 26l0 92 29 0c6 0 12 0 16-3 3-3 9-3 12-6 4-3 7-7 7-10 0-3 3-6 3-13 0-3 0-9-3-12 0-4-3-7-7-10-3-3-6-3-12-6-7-4-10-4-16-4l-13 0 0-32-16 0z m16 44l13 0c3 0 6 0 9 0 3 0 3 4 7 4 3 0 3 3 3 6 0 3 0 3 0 6 0 4 0 4 0 7 0 3-3 3-3 6 0 3-4 3-7 3-3 0-6 0-9 0l-13 0z m61-44l0 92 35 0c6 0 13 0 19-3 6-3 10-6 16-9 6-4 6-10 10-16 3-7 3-13 3-20 0-6 0-12-3-19-4-6-7-9-10-16-3-6-10-3-16-6-6-3-13-3-19-3z m19 12l19 0c3 0 10 0 13 4 3 0 6 3 9 6 4 3 4 6 7 10 3 3 3 9 3 12 0 4 0 10-3 13-3 3-7 10-10 13-3 3-6 6-9 6-4 0-7 4-13 4l-16 0z m77-12l0 92 57 0 0-12-41 0 0-29 35 0 0-13-35 0 0-38z"/>
<glyph unicode="&#57579;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-259 26l0 92 29 0c6 0 12 0 16-3 3-3 9-3 12-6 4-3 7-7 7-10 0-3 3-6 3-13 0-3 0-9-3-12 0-4-3-7-7-10-3-3-6-3-12-6-7-4-10-4-16-4l-13 0 0-32-16 0z m16 44l13 0c3 0 6 0 9 0 3 0 3 4 7 4 3 0 3 3 3 6 0 3 0 3 0 6 0 4 0 4 0 7 0 3-3 3-3 6 0 3-4 3-7 3-3 0-6 0-9 0l-13 0z m61-44l0 92 35 0c6 0 13 0 19-3 6-3 10-6 16-9 6-4 6-10 10-16 3-7 3-13 3-20 0-6 0-12-3-19-4-6-7-9-10-16-3-6-10-3-16-6-6-3-13-3-19-3z m19 12l19 0c3 0 10 0 13 4 3 0 6 3 9 6 4 3 4 6 7 10 3 3 3 9 3 12 0 4 0 10-3 13-3 3-7 10-10 13-3 3-6 6-9 6-4 0-7 4-13 4l-16 0z m77-12l0 92 57 0 0-12-41 0 0-29 35 0 0-13-35 0 0-38z m-176 166l233 0 0-16-233 0z m0 54l233 0 0-16-233 0z m3 58l118 0 0-16-118 0z"/>
<glyph unicode="&#57509;" d="M150 89c0-33-27-61-61-61-33 0-61 28-61 61 0 34 28 61 61 61 34 0 61-27 61-61m177-57l-80 0c-1 57-24 111-64 151-40 41-94 63-151 64l0 80c162-2 293-133 295-295m157 0l-81 0c-2 204-167 369-370 372l0 80c247-3 448-204 451-452"/>
<glyph unicode="&#57580;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-189 54c0-16-13-28-29-28-16 0-28 12-28 28 0 16 12 29 28 29 16 0 29-13 29-29z m77-25l-35 0c0 25-10 51-29 70-19 19-42 29-70 29l0 35c73 0 134-61 134-134z m74 0l-36 0c0 93-76 169-169 169l0 36c112 0 201-93 205-205z"/>
<glyph unicode="&#57581;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-266 70c0 7 0 13 4 20 3 6 6 9 9 16 3 3 10 6 16 9 7 3 13 3 19 3 7 0 13 0 20-3 6-3 9-6 12-9l-6-7c0 0 0 0-3 0 0 0-3 0-3 0 0 0-4 0-4 3 0 4-6 0-9 0-3 0-7 0-10 0-3 0-9 0-13-3-3-3-3 0-6-3-3-3-3-6-6-10 0-3-4-9-4-12 0-7 0-10 4-13 0-3 3-7 6-10 3-3 6-6 10-6 3 0 6-3 9-3 3 0 3 0 7 0 3 0 3 0 6 0 3 0 3 0 3 3 0 3 3 3 3 3 0 0 4 0 4 0l6-6c-3-4-10-10-13-10-6-3-13-3-19-3-6 0-13 0-19 3-7 3-7 0-13 6-6 7-6 10-10 16 0 4 0 10 0 16z m84-35l6 10c0 0 0 0 3 0 0 0 0 0 3 0 0 0 4 0 4 0 0 0 3-3 3-3 3 0 3-4 6-4 3 0 7 0 10 0 6 0 9 0 13 4 3 3 3 6 3 9 0 3 0 3-3 7 0 0-4 3-7 3-3 3-6 3-9 3-4 0-7 0-7 3-3 0-6 3-6 3-3 0-7 4-7 4 0 0-6 6-6 9 0 3-3 7-3 10 0 3 0 6 3 9 3 4 3 7 6 10 4 3 7 3 10 6 3 0 10 4 13 4 6 0 9 0 16-4 3-3 9-3 13-6l-4-10c0 0 0-3-3-3-3 0 0 0-3 0 0 0-3 0-3 0 0 0-3 0-3 3 0 0-4 0-7 4-3 0-3 0-6 0-3 0-3 0-7 0-3 0-3 0-3-4 0 0-3-3-3-3 0 0 0-3 0-3 0-3 0-3 3-6 3-4 3-4 3-4 4 0 4-3 7-3 3 0 6-3 6-3 3 0 7-3 7-3 3 0 6-3 6-3 0 0 3-4 6-7 4-3 4-6 4-9 0-4 0-10-4-13 0-3-3-7-6-10-3-3-6-6-10-6-3 0-9-3-12-3-4 0-7 0-10 0-3 0-6 0-10 3-3 0-6 3-6 3 0 0-3 3-6 3z m67 83l13 0c0 0 3 0 3 0 0 0 3-3 3-3l22-57c0-4 0-4 4-7 3-3 0-3 3-6 0 6 3 9 3 13l22 57c0 0 0 3 4 3 3 0 3 0 3 0l13 0-39-92-16 0z"/>
<glyph unicode="&#57582;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-253 26l32 48-29 44 16 0c0 0 4 0 4 0l22-35c0 0 0 0 0 3 0 4 0 0 0 4l19 32c0 0 3 3 3 3l16 0-28-45 32-48-20 0c0 0-3 0-3 0 0 0 0 0-3 3l-19 29-19-32c0 0 0 0-4-3-3-3 0 0-3 0l-16 0z m96 0l0 92 16 0 0-80 39 0 0-12z m58 9l6 10c0 0 0 0 3 0 0 0 0 0 4 0 0 0 3 0 3 0 0 0 3-3 3-3 3 0 3-4 6-4 4 0 7 0 10 0 6 0 10 0 13 4 3 3 3 6 3 9 0 3 0 3-3 7 0 0-3 3-7 3-3 0-3 3-6 3-3 0-6 0-6 3-4 0-7 3-7 3-3 0-6 4-6 4 0 0-3 3-7 6 0 3-3 6-3 10 0 3 0 6 3 9 0 3 4 7 7 10 3 3 6 3 9 6 4 3 10 3 13 3 7 0 10 0 16-3 3-3 10-3 13-6l-3-10c0 0 0-3-3-3-4 0 0 0-4 0 0 0-3 0-3 0 0 0-3 0-3 3 0 3-3 0-6 3-4 0-4 0-7 0-3 0-3 0-6 0-3 0-3 0-3-3 0 0-4-3-4-3 0 0 0-3 0-3 0-3 0-3 4-7 0 0 3-3 3-3 3 0 3-3 6-3 3 0 7-3 7-3 3 0 6-3 6-3 3 0 6-4 6-4 4-3 4-3 7-6 0-3 3-6 3-10 0-3 0-9-3-12 0-4-3-7-7-10-3-3-6-6-9-6-3 0-10-4-13-4-3 0-6 0-10 0-3 0-6 0-9 4-3 0-7 3-7 3-6 3-6 6-9 6z"/>
<glyph unicode="&#57583;" d="M317 512l-269 0 0-512 416 0 0 365z m83-448l-288 0 0 384 179 0 13-13 0-83 83 0 13-13z m-253 26l32 48-29 44 16 0c0 0 4 0 4 0l22-35c0 0 0 0 0 3 0 4 0 0 0 4l19 32c0 0 3 3 3 3l16 0-28-45 32-48-20 0c0 0-3 0-3 0 0 0 0 0-3 3l-19 29-19-32c0 0 0 0-4-3-3-3 0 0-3 0l-16 0z m96 0l0 92 16 0 0-80 39 0 0-12z m58 9l6 10c0 0 0 0 3 0 0 0 0 0 4 0 0 0 3 0 3 0 0 0 3-3 3-3 3 0 3-4 6-4 4 0 7 0 10 0 6 0 10 0 13 4 3 3 3 6 3 9 0 3 0 3-3 7 0 0-3 3-7 3-3 0-3 3-6 3-3 0-6 0-6 3-4 0-7 3-7 3-3 0-6 4-6 4 0 0-3 3-7 6 0 3-3 6-3 10 0 3 0 6 3 9 0 3 4 7 7 10 3 3 6 3 9 6 4 3 10 3 13 3 7 0 10 0 16-3 3-3 10-3 13-6l-3-10c0 0 0-3-3-3-4 0 0 0-4 0 0 0-3 0-3 0 0 0-3 0-3 3 0 3-3 0-6 3-4 0-4 0-7 0-3 0-3 0-6 0-3 0-3 0-3-3 0 0-4-3-4-3 0 0 0-3 0-3 0-3 0-3 4-7 0 0 3-3 3-3 3 0 3-3 6-3 3 0 7-3 7-3 3 0 6-3 6-3 3 0 6-4 6-4 4-3 4-3 7-6 0-3 3-6 3-10 0-3 0-9-3-12 0-4-3-7-7-10-3-3-6-6-9-6-3 0-10-4-13-4-3 0-6 0-10 0-3 0-6 0-9 4-3 0-7 3-7 3-6 3-6 6-9 6z m-160 157l233 0 0-16-233 0z m0 54l233 0 0-16-233 0z m0 58l118 0 0-16-118 0z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 79 KiB

@ -59,13 +59,20 @@ angular.module('openproject.workPackages.controllers')
$scope.workPackageTableData = WorkPackagesTableService.getWorkPackagesTableData();
function buildOptions() {
var blankOption = { id: null, label: ' ', other: null };
$scope.groupableColumnsData = $scope.groupableColumns.map(function(column){
return { id: column.name, label: column.title, other: column.title };
});
$scope.groupableColumnsData.unshift(blankOption);
}
$scope.$watch('workPackageTableData.groupableColumns', function(groupableColumns){
if (!groupableColumns) return;
$scope.groupableColumns = groupableColumns;
$scope.groupableColumnsData = groupableColumns.map(function(column){
return { id: column.name, label: column.title, other: column.title };
});
buildOptions();
var currentGroupBy = $scope.groupableColumnsData.filter(function(column){
return column.id == QueryService.getGroupBy();

@ -145,10 +145,6 @@ angular.module('openproject.services')
},
loadAvailableUnusedColumns: function(projectIdentifier) {
if(availableUnusedColumns.length) {
return $q.when(availableUnusedColumns);
}
return QueryService.loadAvailableColumns(projectIdentifier)
.then(function(available_columns) {
availableUnusedColumns = WorkPackagesTableHelper.getColumnDifference(available_columns, QueryService.getSelectedColumns());

@ -76,8 +76,8 @@ angular.module('openproject.uiComponents')
ctrl.open();
contextMenu.open(locals)
.then(function(menuElement) {
menuElement.css(getCssPositionProperties(menuElement, element));
.then(function(element) {
menuElement = element;
});
}
@ -87,6 +87,10 @@ angular.module('openproject.uiComponents')
contextMenu.close();
}
function positionDropdown() {
menuElement.css(getCssPositionProperties(menuElement, element));
}
element.bind(triggerOnEvent, function(event) {
event.preventDefault();
event.stopPropagation();
@ -95,6 +99,9 @@ angular.module('openproject.uiComponents')
toggle();
});
// set css position parameters after the digest has been completed
if (contextMenu.active()) positionDropdown();
scope.$root.$broadcast('openproject.markDropdownsAsClosed', element);
});

@ -24,7 +24,7 @@
</li>
<li>
<a>
<i class="icon-page-xls icon-big"></i>
<i class="icon-page-xls-descr icon-big"></i>
<span class="export-label">XLS with description</span>
</a>
</li>
@ -36,7 +36,7 @@
</li>
<li>
<a>
<i class="icon-page-pdf icon-big"></i>
<i class="icon-page-pdf-descr icon-big"></i>
<span class="export-label">PDF with description</span>
</a>
</li>

@ -28,7 +28,7 @@
@import global/all
div.wiki
font-size: $wiki_toc_ul_font_size
font-size: $wiki_default_font_size
line-height: 1.6em
h1, h2
margin: 1em 0 1em 0
@ -62,6 +62,7 @@ div.wiki
margin-right: 12px
margin-left: 0
display: table
font-size: $wiki_toc_ul_font_size
&.right
float: right
margin-left: 12px

@ -99,7 +99,6 @@ tr.entry td.filename { width: 30%; }
tr.entry td.size { text-align: right; font-size: 90%; }
tr.entry td.revision, tr.entry td.author { text-align: center; }
tr.entry td.age { text-align: right; }
tr.entry.file td.filename a { margin-left: 16px; }
tr span.expander {cursor: pointer;}
tr.open span .expand { display:none; }

@ -29,7 +29,7 @@
<i class="icon-arrow-right6-3"></i>
<i class="icon-arrow-right7"></i>
<i class="icon-arrow-right8"></i>
<i class="icon-atom"></i> <i class="icon-rss2"></i>
<i class="icon-rss2"></i>
<i class="icon-attachment"></i>
<i class="icon-attention1"></i>
<i class="icon-attention2"></i>
@ -224,7 +224,6 @@
<i class="icon-work_package-closed"></i>
<i class="icon-work_package-edit"></i>
<i class="icon-work_package-note"></i>
<i class="icon-xls"></i> <i class="icon-excel"></i>
<i class="icon-yes2"></i>
<i class="icon-yes3"></i>
<i class="icon-zoom-in"></i>
@ -240,8 +239,10 @@
<i class="icon-timeline-view"></i>
<i class="icon-toggle"></i>
<i class="icon-page-pdf"></i>
<i class="icon-page-pdf-descr"></i>
<i class="icon-page-csv"></i>
<i class="icon-page-xls"></i>
<i class="icon-page-xls-descr"></i>
<i class="icon-page-atom"></i>
<i class="icon-sort-by"></i>
<i class="icon-group-by"></i>

@ -303,9 +303,6 @@ dt > .icon-news:before,
.icon-group:before
content: "\e018"
.icon-xls:before, .icon-excel:before
content: "\e019"
.icon-priority:before
content: "\e01a"
@ -729,9 +726,6 @@ dt > .icon-reply:before,
.icon-rubber:before
content: "\e0a4"
.icon-atom:before, .icon-rss2:before
content: "\e0a5"
.icon-rss:before
content: "\e0a6"
@ -935,23 +929,32 @@ dt > .icon-wiki-page:before,
.icon-filter-big:before
content: "\e0e6"
.icon-group-by2:before
content: "\e0e7"
.icon-sort-by2:before
content: "\e0e8"
.icon-page-pdf:before
content: "\e0e9"
.icon-page-csv:before
content: "\e0ea"
.icon-page-pdf-descr:before
content: "\e0eb"
.icon-page-xls:before
content: "\e019"
.icon-rss2:before
content: "\e0a5"
.icon-page-atom:before
content: "\e0a5"
content: "\e0ec"
.icon-group-by2:before
content: "\e0e7"
.icon-page-csv:before
content: "\e0ed"
.icon-sort-by2:before
content: "\e0e8"
.icon-page-xls:before
content: "\e0ee"
.icon-page-xls-descr:before
content: "\e0ef"
/* remove once all menu items have an icon */
.no-icon

@ -174,7 +174,10 @@ $my_page_edit_box_border_color: #06799F !default
$action_menu_bg_color: #FFFFFF
$wiki_default_font_size: $global_font_size
$wiki_toc_header_font_size: 10px !default
$wiki_toc_ul_font_size: 0.8em !default
$journal_attribute_font_size: 11px !default
$repository_entry_filename_margin_left: 24px !default

@ -417,9 +417,11 @@ tr.dir
span
&.dir-expander
@include icon-common
margin-right: -15px
cursor: pointer
&:before
content: "\e089"
margin-left: 5px
padding: 0
&.loading
span.dir-expander:before
content: "\e07a"
@ -429,3 +431,11 @@ tr.dir
&.open
span.dir-expander:before
content: "\e082"
tr
&.entry
&.file
td
&.filename
a
margin-left: $repository_entry_filename_margin_left

@ -0,0 +1,125 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
# This capsulates permissions a user has for a work package. It caches based
# on the work package's project and is thus optimized for the context menu.
#
# This is no conern but it was placed here so that it will be removed together
# with the rest of the experimental API.
module Api
module Experimental
module Concerns
class Can
attr_accessor :user
def initialize(user)
self.user = user
end
def actions(wp)
cache[wp].each_with_object([]) { |(k, v), a| a << k if v }
end
def allowed?(work_package, action)
cache[work_package][action]
end
private
def cache
@cache ||= Hash.new do |hash, work_package|
# copy checks for the move_work_packages permission. This makes
# sense only because the work_packages/moves controller handles
# copying multiple work packages.
hash[work_package] = {
:edit => edit_allowed?(work_package),
:log_time => log_time_allowed?(work_package),
:move => move_allowed?(work_package),
:copy => move_allowed?(work_package),
:duplicate => copy_allowed?(work_package), # duplicating is another form of copying
:delete => delete_allowed?(work_package)
}
end
end
def edit_allowed?(work_package)
@edit_cache ||= Hash.new do |hash, project|
hash[project] = user.allowed_to?(:edit_work_packages, project)
end
@edit_cache[work_package.project] || work_package.new_statuses_allowed_to(user).present?
end
def log_time_allowed?(work_package)
@log_time_cache ||= Hash.new do |hash, project|
hash[project] = user.allowed_to?(:log_time, project)
end
@log_time_cache[work_package.project]
end
def move_allowed?(work_package)
@move_cache ||= Hash.new do |hash, project|
hash[project] = user.allowed_to?(:move_work_packages, project)
end
@move_cache[work_package.project]
end
def copy_allowed?(work_package)
type_active_in_project?(work_package) && add_allowed?(work_package)
end
def delete_allowed?(work_package)
@delete_cache ||= Hash.new do |hash, project|
hash[project] = user.allowed_to?(:delete_work_packages, project)
end
@delete_cache[work_package.project]
end
def add_allowed?(work_package)
@add_cache ||= Hash.new do |hash, project|
hash[project] = user.allowed_to?(:add_work_packages, project)
end
@add_cache[work_package.project]
end
def type_active_in_project?(work_package)
@type_active_cache ||= Hash.new do |hash, project|
hash[project] = project.types.pluck(:id)
end
@type_active_cache[work_package.project].include?(work_package.type_id)
end
end
end
end
end

@ -60,12 +60,6 @@ module Api
end
end
# determine what actions may be performed
@allowed_statuses = @work_packages.map do |i|
i.new_statuses_allowed_to(User.current)
end.inject do |memo,s|
memo & s
end
setup_context_menu_actions
# the data for the index is already produced in the assign_work_packages
@ -99,16 +93,7 @@ module Api
private
def setup_context_menu_actions
@projects = @work_packages.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
@can = {:edit => User.current.allowed_to?(:edit_work_packages, @projects),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (User.current.allowed_to?(:edit_work_packages, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
:move => (@project && User.current.allowed_to?(:move_work_packages, @project)),
:copy => (@work_package && @project.types.include?(@work_package.type) && User.current.allowed_to?(:add_work_packages, @project)),
:delete => User.current.allowed_to?(:delete_work_packages, @projects)
}
@can = Api::Experimental::Concerns::Can.new(User.current)
end
def columns_total_sums(column_names, work_packages)

@ -323,7 +323,7 @@ module Api
# re-wire the parent of this pe to the first ancestor found in the filtered set
# re-wiring is only needed, when there is actually a parent, and the parent has been filtered out
if pe.parent_id && !filtered_ids.include?(pe.parent_id)
ancestors = @planning_elements.select{|candidate| candidate.lft < pe.lft && candidate.rgt > pe.rgt }
ancestors = @planning_elements.select{|candidate| candidate.lft < pe.lft && candidate.rgt > pe.rgt && candidate.root_id == pe.root_id }
# the greatest lower boundary is the first ancestor not filtered
pe.parent_id = ancestors.empty? ? nil : ancestors.sort_by{|ancestor| ancestor.lft }.last.id
end

@ -62,11 +62,7 @@ child @work_packages => :work_packages do
end
node :_actions do |wp|
if !!@can[:move]
@can.each_with_object([]) { |(k, v), a| a << k if v } | [:copy, :duplicate]
else
@can.each_with_object([]) { |(k, v), a| a << k if v }
end
@can.actions(wp)
end
node :_links do |wp|
@ -79,7 +75,8 @@ child @work_packages => :work_packages do
move: -> { new_move_work_packages_path(ids: [wp.id]) },
copy: -> { new_move_work_packages_path(ids: [wp.id], copy: true) },
delete: -> { work_packages_bulk_path(ids: [wp.id], method: :delete) }
}.select { |action, link| @can[action] || @can[:move] && [:copy, :duplicate].include?(action) }
}.select { |action, link| @can.allowed?(wp, action) }
links = links.update(links) { |key, old_val, new_val| new_val.() }
end
end

@ -35,7 +35,7 @@ See doc/COPYRIGHT.rdoc for more details.
<tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= h(entry.kind) %>">
<td style="padding-left: <%=18 * depth%>px;" class="filename">
<% if entry.is_dir? %>
<span class="icon dir-expander" onclick="<%= remote_function :url => {:action => 'show', :project_id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
<span class="icon-context dir-expander" onclick="<%= remote_function :url => {:action => 'show', :project_id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:method => :get,
:update => { :success => tr_id },
:position => :after,
@ -44,7 +44,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %>
<%= link_to h(ent_name),
{:action => (entry.is_dir? ? 'show' : 'changes'), :project_id => @project, :path => to_path_param(ent_path), :rev => @rev},
:class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
:class => (entry.is_dir? ? 'icon-context icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
</td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
<% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>

@ -22,7 +22,7 @@
"jquery-migrate": "~1.2.1",
"momentjs": "~2.6.0",
"moment-timezone": "~0.0.6",
"angular-context-menu": "0.1.1",
"angular-context-menu": "0.1.2",
"angular-busy": "~4.0.4"
},
"devDependencies": {

@ -30,7 +30,7 @@
OpenProject::Application.routes.draw do
root :to => 'welcome#index', :as => 'home'
mount API::Root => '/'
rails_relative_url_root = OpenProject::Configuration['rails_relative_url_root'] || ''
# Redirect deprecated issue links to new work packages uris
@ -519,6 +519,4 @@ OpenProject::Application.routes.draw do
match '/:controller(/:action(/:id))'
match '/robots' => 'welcome#robots', :defaults => { :format => :txt }
root :to => 'account#login'
mount API::Root => '/'
end

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class ChangeAttachmentJournalsDescriptionLength < ActiveRecord::Migration
def change
change_column :attachment_journals,
:description,
:text
end
end

@ -79,10 +79,6 @@ See doc/COPYRIGHT.rdoc for more details.
* `#7700` No flash message on status page
* `#8228` Icons in select2 elements missing
* `#8277` Fix arrow of column headers not restrained to one line
* `#4019` Fix focus on project creation
* `#4021` Separate focus for main menu expander
* `#6288` Editing relations in modal dialog leads to warning
* `#7898` Watchers are not sorted alphabetically in work package screen
* Allowed sending of mails with only cc: or bcc: fields
* Allow adding attachments to created work packages via planning elements controller
* Remove unused rmagick dependency
@ -96,6 +92,14 @@ See doc/COPYRIGHT.rdoc for more details.
* Fix: Asset require for plug-ins
* Fix: at.who styling
* `#4019` Fix focus on project creation
* `#4021` Separate focus for main menu expander
* `#4258` Text alignment consistency in tables
* `#6288` Editing relations in modal dialog leads to warning
* `#7898` Watchers are not sorted alphabetically in work package screen
* `#9931` APIv2 does not rewire parents correctly
## 3.0.4
* `#8421` Make subdirectory configuration less of a hassle

@ -38,36 +38,43 @@ module API
format 'hal+json'
helpers do
# Needs refactoring - Will have to find a way how to access sessions in all enviroments
def current_user
return User.current if Rails.env.test?
user_id = env['rack.session']['user_id']
User.current = user_id ? User.find(user_id) : User.anonymous
end
# Split into two methods: one for authentication, one for authorization
def authorize(api, endpoint, project = nil, projects = nil, global = false)
if current_user.nil? || current_user.anonymous?
raise API::Errors::Unauthenticated.new
end
is_authorized = AuthorizationService.new(api, endpoint, project, projects, global, current_user).perform
unless is_authorized
raise API::Errors::Unauthorized.new(current_user)
def authenticate
raise API::Errors::Unauthenticated.new if current_user.nil? || current_user.anonymous?
end
def authorize(api, endpoint, options)
unless options[:allow].nil?
raise API::Errors::Unauthorized.new(current_user) unless options[:allow]
end
is_authorized = AuthorizationService.new(api, endpoint, options[:project], options[:projects],
!!options[:global], current_user).perform
raise API::Errors::Unauthorized.new(current_user) unless is_authorized
is_authorized
end
end
rescue_from API::Errors::Validation, API::Errors::UnwritableProperty, API::Errors::Unauthorized,
API::Errors::Unauthenticated do |e|
Rack::Response.new(e.to_json, e.code, e.headers).finish
rescue_from :all do |e|
case e.class.to_s
when 'API::Errors::Validation', 'API::Errors::UnwritableProperty', 'API::Errors::Unauthorized', 'API::Errors::Unauthenticated'
Rack::Response.new(e.to_json, e.code, e.headers).finish
when 'ActiveRecord::RecordNotFound'
not_found = API::Errors::NotFound.new(e.message)
Rack::Response.new(not_found.to_json, not_found.code, not_found.headers).finish
when 'ActiveRecord::RecordInvalid'
error = API::Errors::Validation.new(e.record)
Rack::Response.new(error.to_json, error.code, error.headers).finish
end
end
rescue_from ActiveRecord::RecordNotFound do |e|
not_found = API::Errors::NotFound.new(e.message)
Rack::Response.new(not_found.to_json, not_found.code, not_found.headers).finish
# run authentication before each request
before do
authenticate
end
mount API::V3::Root

@ -0,0 +1,52 @@
module API
module V3
module Queries
class QueriesAPI < Grape::API
resources :queries do
params do
requires :id, desc: 'Query id'
end
namespace ':id' do
before do
@query = Query.find(params[:id])
model = QueryModel.new(query: @query)
@representer = QueryRepresenter.new(model)
end
helpers do
def allowed_to_manage_stars?
(@query.is_public? && current_user.allowed_to?(:manage_public_queries, @query.project)) ||
(!@query.is_public? && (current_user.admin? ||
(current_user.allowed_to?(:save_queries, @query.project) && @query.user_id == current_user.id)))
end
end
patch :star do
authorize(:queries, :star, project: @query.project, allow: allowed_to_manage_stars?)
normalized_query_name = @query.name.parameterize.underscore
query_menu_item = MenuItems::QueryMenuItem.find_or_initialize_by_name_and_navigatable_id(
normalized_query_name, @query.id, title: @query.name
)
query_menu_item.save!
@representer.to_json
end
patch :unstar do
authorize(:queries, :unstar, project: @query.project, allow: allowed_to_manage_stars?)
query_menu_item = @query.query_menu_item
return @representer.to_json if @query.query_menu_item.nil?
query_menu_item.destroy
@query.reload
@representer.to_json
end
end
end
end
end
end
end

@ -0,0 +1,54 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/coercion'
module API
module V3
module Queries
class QueryModel < Reform::Form
include Composition
include Coercion
model :query
property :name, on: :query, type: String
property :project_id, on: :query, type: Integer
property :user_id, on: :query, type: Integer
property :filters, on: :query, type: String
property :is_public, on: :query, type: String
property :column_names, on: :query, type: String
property :sort_criteria, on: :query, type: String
property :group_by, on: :query, type: String
property :display_sums, on: :query, type: String
end
end
end
end

@ -0,0 +1,76 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'roar/decorator'
require 'roar/representer/json/hal'
module API
module V3
module Queries
class QueryRepresenter < Roar::Decorator
include Roar::Representer::JSON::HAL
include Roar::Representer::Feature::Hypermedia
include Rails.application.routes.url_helpers
self.as_strategy = API::Utilities::CamelCasingStrategy.new
property :_type, exec_context: :decorator
link :self do
{ href: "http://localhost:3000/api/v3/queries/#{represented.query.id}", title: "#{represented.name}" }
end
property :id, getter: -> (*) { query.id }, render_nil: true
property :name, render_nil: true
property :project_id, getter: -> (*) { query.project.id }
property :project_name, getter: -> (*) { query.project.try(:name) }
property :user_id, getter: -> (*) { query.user.try(:id) }, render_nil: true
property :user_name, getter: -> (*) { query.user.try(:name) }, render_nil: true
property :user_login, getter: -> (*) { query.user.try(:login) }, render_nil: true
property :user_mail, getter: -> (*) { query.user.try(:mail) }, render_nil: true
property :filters, render_nil: true
property :is_public, getter: -> (*) { query.is_public.to_s }, render_nil: true
property :column_names, render_nil: true
property :sort_criteria, render_nil: true
property :group_by, render_nil: true
property :display_sums, getter: -> (*) { query.display_sums.to_s }, render_nil: true
property :is_starred, getter: -> (*) { is_starred.to_s }, exec_context: :decorator
def _type
"Query"
end
def is_starred
return true if !represented.query.query_menu_item.nil?
false
end
end
end
end
end

@ -37,6 +37,7 @@ module API
version 'v3', using: :path
mount API::V3::WorkPackages::WorkPackagesAPI
mount API::V3::Queries::QueriesAPI
end
end
end

@ -17,7 +17,7 @@ module API
end
get do
authorize(:work_packages_api, :get, @work_package.project)
authorize(:work_packages_api, :get, project: @work_package.project)
@representer.to_json
end

@ -137,8 +137,8 @@ Redmine::AccessControl.map do |map|
map.permission :manage_work_package_relations, {:work_package_relations => [:create, :destroy]}
map.permission :manage_subtasks, {}
# Queries
map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
map.permission :manage_public_queries, {:queries => [:new, :edit, :star, :unstar, :destroy]}, :require => :member
map.permission :save_queries, {:queries => [:new, :edit, :star, :unstar, :destroy]}, :require => :loggedin
# Watchers
map.permission :view_work_package_watchers, {}
map.permission :add_work_package_watchers, {:watchers => [:new, :create]}

@ -1,4 +1,6 @@
<div id="column-context-menu" class="action-menu dropdown-relative dropdown-anchor-right">
<div id="column-context-menu"
class="action-menu dropdown-relative"
ng-class="{'dropdown-anchor-right': column && column.name !== 'id'}">
<ul class="menu">
<li ng-if="!!column.sortable" ng-click="sortAscending(column.name)">
<a href="#">
@ -38,7 +40,7 @@
</a>
</li>
<li ng-if="column"
<li ng-if="column.name !== 'id'"
ng-click="hideColumn(column.name)">
<i class="icon-action-menu icon-delete2"></i>
<a href="#">

@ -0,0 +1,377 @@
require 'spec_helper'
require 'rack/test'
describe 'API v3 Query resource' do
include Rack::Test::Methods
let(:project) { FactoryGirl.create(:project, :identifier => 'test_project', :is_public => false) }
let(:current_user) { FactoryGirl.create(:user) }
let(:manage_public_queries_role) { FactoryGirl.create(:role, permissions: [:manage_public_queries]) }
let(:save_queries_role) { FactoryGirl.create(:role, permissions: [:save_queries]) }
let(:role_without_query_permissions) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:unauthorize_user) { FactoryGirl.create(:user) }
describe '#star' do
let(:star_path) { "/api/v3/queries/#{query.id}/star" }
let(:filters) do
query.filters.map{ |f| {f.field.to_s => { "operator" => f.operator, "values" => f.values }}}
end
let(:expected_response) do
{
"_type" => 'Query',
"_links" => {
"self" => {
"href" => "http://localhost:3000/api/v3/queries/#{query.id}",
"title" => query.name
}
},
"id" => query.id,
"name" => query.name,
"projectId" => query.project_id,
"projectName" => query.project.name,
"userId" => query.user_id,
"userName" => query.user.try(:name),
"userLogin" => query.user.try(:login),
"userMail" => query.user.try(:mail),
"filters" => filters,
"isPublic" => query.is_public.to_s,
"columnNames" => query.column_names,
"sortCriteria" => query.sort_criteria,
"groupBy" => query.group_by,
"displaySums" => query.display_sums.to_s,
"isStarred" => "true"
}
end
describe 'public queries' do
let(:query) { FactoryGirl.create(:public_query, project: project) }
context 'user with permission to manage public queries' do
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [manage_public_queries_role.id]
member.save!
end
context 'when starring an unstarred query' do
before(:each) { patch star_path }
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response)
end
it 'should return the query with "isStarred" property set to true' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('true')
end
end
context 'when starring already starred query' do
before(:each) { patch star_path }
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response)
end
it 'should return the query with "isStarred" property set to true' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('true')
end
end
context 'when trying to star nonexistent query' do
let(:star_path) { "/api/v3/queries/999/star" }
before(:each) { patch star_path }
it 'should respond with 404' do
last_response.status.should eq(404)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_found', 'messages' => ['Couldn\'t find Query with id=999']}])
end
end
end
context 'user without permission to manage public queries' do
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [role_without_query_permissions.id]
member.save!
patch star_path
end
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
describe 'private queries' do
context 'user with permission to save queries' do
let(:query) { FactoryGirl.create(:private_query, project: project, user: current_user) }
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [save_queries_role.id]
member.save!
patch star_path
end
context 'starring his own query' do
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response)
end
it 'should return the query with "isStarred" property set to true' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('true')
end
end
context 'trying to star somebody else\'s query' do
let(:another_user) { FactoryGirl.create(:user) }
let(:query) { FactoryGirl.create(:private_query, project: project, user: another_user) }
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
context 'user without permission to save queries' do
let(:query) { FactoryGirl.create(:private_query, project: project, user: current_user) }
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [role_without_query_permissions.id]
member.save!
patch star_path
end
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
end
describe '#unstar' do
let(:unstar_path) { "/api/v3/queries/#{query.id}/unstar" }
let(:filters) do
query.filters.map{ |f| {f.field.to_s => { "operator" => f.operator, "values" => f.values }}}
end
let(:expected_response) do
{
"_type" => 'Query',
"_links" => {
"self" => {
"href" => "http://localhost:3000/api/v3/queries/#{query.id}",
"title" => query.name
}
},
"id" => query.id,
"name" => query.name,
"projectId" => query.project_id,
"projectName" => query.project.name,
"userId" => query.user_id,
"userName" => query.user.try(:name),
"userLogin" => query.user.try(:login),
"userMail" => query.user.try(:mail),
"filters" => filters,
"isPublic" => query.is_public.to_s,
"columnNames" => query.column_names,
"sortCriteria" => query.sort_criteria,
"groupBy" => query.group_by,
"displaySums" => query.display_sums.to_s,
"isStarred" => "true"
}
end
describe 'public queries' do
let(:query) { FactoryGirl.create(:public_query, project: project) }
context 'user with permission to manage public queries' do
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [manage_public_queries_role.id]
member.save!
end
context 'when unstarring a starred query' do
before(:each) do
FactoryGirl.create(:query_menu_item, query: query)
patch unstar_path
end
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response.tap{ |r| r["isStarred"] = "false" })
end
it 'should return the query with "isStarred" property set to false' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('false')
end
end
context 'when unstarring an unstarred query' do
before(:each) { patch unstar_path }
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response.tap{ |r| r["isStarred"] = "false" })
end
it 'should return the query with "isStarred" property set to true' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('false')
end
end
context 'when trying to unstar nonexistent query' do
let(:unstar_path) { "/api/v3/queries/999/unstar" }
before(:each) { patch unstar_path }
it 'should respond with 404' do
last_response.status.should eq(404)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_found', 'messages' => ['Couldn\'t find Query with id=999']}])
end
end
end
context 'user without permission to manage public queries' do
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [role_without_query_permissions.id]
member.save!
patch unstar_path
end
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
describe 'private queries' do
context 'user with permission to save queries' do
let(:query) { FactoryGirl.create(:private_query, project: project, user: current_user) }
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [save_queries_role.id]
member.save!
patch unstar_path
end
context 'unstarring his own query' do
it 'should respond with 200' do
last_response.status.should eq(200)
end
it 'should return the query in HAL+JSON format' do
parsed_response = JSON.parse(last_response.body)
parsed_response.should eq(expected_response.tap{ |r| r["isStarred"] = "false" })
end
it 'should return the query with "isStarred" property set to true' do
parsed_response = JSON.parse(last_response.body)
parsed_response['isStarred'].should eq('false')
end
end
context 'trying to unstar somebody else\'s query' do
let(:another_user) { FactoryGirl.create(:user) }
let(:query) { FactoryGirl.create(:private_query, project: project, user: another_user) }
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
context 'user without permission to save queries' do
let(:query) { FactoryGirl.create(:private_query, project: project, user: current_user) }
before(:each) do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [role_without_query_permissions.id]
member.save!
patch unstar_path
end
it 'should respond with 403' do
last_response.status.should eq(403)
end
it 'should respond with explanatory error message' do
parsed_errors = JSON.parse(last_response.body)['errors']
parsed_errors.should eq([{ 'key' => 'not_authorized', 'messages' => ['You are not authorize to access this resource']}])
end
end
end
end
end

@ -111,17 +111,11 @@ describe Api::Experimental::WorkPackagesController do
expect(assigns(:work_packages).size).to eq(2)
expect(assigns(:can).size).to eq(6)
expect(assigns(:can)['edit']).to be_true
expect(assigns(:can)['log_time']).to be_true
expect(assigns(:can)['update']).to be_true
expect(assigns(:can)['move']).to be_true
expect(assigns(:can)['copy']).to be_false
expect(assigns(:can)['delete']).to be_true
expect(assigns(:projects)).to eq([project_1])
expect(assigns(:project)).to eq(project_1)
expect(assigns(:allowed_statuses)).to eq([])
expect(assigns(:can).allowed?(work_package_1, :edit)).to be_true
expect(assigns(:can).allowed?(work_package_1, :log_time)).to be_true
expect(assigns(:can).allowed?(work_package_1, :move)).to be_true
expect(assigns(:can).allowed?(work_package_1, :copy)).to be_true
expect(assigns(:can).allowed?(work_package_1, :delete)).to be_true
end
end
@ -132,9 +126,7 @@ describe Api::Experimental::WorkPackagesController do
get 'index', format: 'xml', query_id: 1
expect(assigns(:work_packages).size).to eq(3)
expect(assigns(:projects)).to include(project_1, project_2)
expect(assigns(:project)).to be_nil
expect(assigns(:allowed_statuses)).to eq([])
end
end
end

@ -238,6 +238,29 @@ describe Api::V2::PlanningElementsController do
end
end
describe 'w/ cross-project relations' do
before do
Setting.stub(:cross_project_work_package_relations?).and_return(true)
end
let!(:project1) { FactoryGirl.create(:project, :identifier => 'project-1') }
let!(:project2) { FactoryGirl.create(:project, :identifier => 'project-2') }
let!(:ticket_a) { FactoryGirl.create(:work_package, :id => 1, :project_id => project1.id) }
let!(:ticket_b) { FactoryGirl.create(:work_package, :id => 2, :project_id => project1.id, :parent_id => ticket_a.id) }
let!(:ticket_c) { FactoryGirl.create(:work_package, :id => 3, :project_id => project1.id, :parent_id => ticket_b.id) }
let!(:ticket_d) { FactoryGirl.create(:work_package, :id => 4, :project_id => project1.id) }
let!(:ticket_e) { FactoryGirl.create(:work_package, :id => 5, :project_id => project2.id, :parent_id => ticket_d.id) }
let!(:ticket_f) { FactoryGirl.create(:work_package, :id => 6, :project_id => project1.id, :parent_id => ticket_e.id) }
become_admin { [project1, project2] }
it 'rewires ancestors correctly' do
get 'index', project_id: project1.id, :format => 'xml'
expect(assigns(:planning_elements).last.parent_id).to eq(ticket_d.id)
end
end
describe 'changed since' do
let!(:work_package) do
work_package = Timecop.travel(5.hours.ago) do

@ -29,6 +29,29 @@
require File.expand_path('../../../../../spec_helper', __FILE__)
describe 'api/experimental/work_packages/index.api.rabl' do
def self.stub_can(permissions)
default_permissions = [:edit, :log_time, :move, :copy, :delete, :duplicate]
resulting_permissions = default_permissions.reduce({}) do |h, (k, _)|
h[k] = true if permissions[k]
h
end
let(:can) do
can = double('Api::Experimental::Concerns::Can')
allow(can).to receive(:actions) do
resulting_permissions.keys
end
allow(can).to receive(:allowed?) do |_, action|
resulting_permissions[action]
end
can
end
end
before do
params[:format] = 'json'
@ -42,7 +65,7 @@ describe 'api/experimental/work_packages/index.api.rabl' do
subject { response.body }
let(:can) { {} }
stub_can({})
describe 'with no work packages available' do
let(:work_packages) { [] }
@ -110,15 +133,12 @@ describe 'api/experimental/work_packages/index.api.rabl' do
end
context 'with some actions' do
let(:can) {
{
edit: false,
log_time: true,
update: false,
move: nil,
delete: true
}
}
stub_can(
edit: false,
log_time: true,
move: nil,
delete: true
)
it { should have_json_path('work_packages/0/_actions') }
it { should have_json_type(Array).at_path('work_packages/0/_actions') }
@ -134,26 +154,25 @@ describe 'api/experimental/work_packages/index.api.rabl' do
end
context 'with all actions' do
let(:can) {
{
edit: true,
log_time: true,
update: true,
move: true,
delete: true
}
}
stub_can(
edit: true,
log_time: true,
move: true,
copy: true,
delete: true,
duplicate: true
)
it { should have_json_path('work_packages/0/_actions') }
it { should have_json_type(Array).at_path('work_packages/0/_actions') }
it { should have_json_size(7).at_path('work_packages/0/_actions') }
it { should have_json_size(6).at_path('work_packages/0/_actions') }
it { should have_json_path('work_packages/0/_actions/' ) }
specify {
expect(parse_json(subject, 'work_packages/0/_actions/5')).to match(%r{copy})
expect(parse_json(subject, 'work_packages/0/_actions/3')).to match(%r{copy})
}
specify {
expect(parse_json(subject, 'work_packages/0/_actions/6')).to match(%r{duplicate})
expect(parse_json(subject, 'work_packages/0/_actions/5')).to match(%r{duplicate})
}
it { should have_json_path('work_packages/0/_links') }

Loading…
Cancel
Save