Merge branch 'dev' into feature/18329-foundation-apps-framework-forms-refactor

pull/2612/head
Alex Coles 10 years ago
commit 7f061d3dfe
  1. 42
      app/controllers/reportings_controller.rb
  2. 35
      config/configuration.yml.example
  3. 33
      config/initializers/module_handler.rb
  4. 8
      config/locales/js-de.yml
  5. 10
      config/locales/js-en.yml
  6. 63
      doc/CONFIGURATION.md
  7. 34
      doc/apiv3-documentation.apib
  8. 7
      frontend/app/services/overview-tab-inplace-editor-config.js
  9. 9
      frontend/app/timelines/models/mixins/ui.js
  10. 7
      frontend/app/timelines/services/timeline-loader-service.js
  11. 8
      frontend/app/ui_components/inplace-editor-directive.js
  12. 2
      frontend/app/ui_components/inplace-editor-dispatcher.js
  13. 2
      frontend/bower.json
  14. 24
      frontend/public/templates/components/inplace_editor.html
  15. 6
      frontend/public/templates/components/inplace_editor/editable/select2.html
  16. 1
      frontend/public/templates/components/inplace_editor/editable/text.html
  17. 1
      frontend/public/templates/components/inplace_editor/editable/wiki_textarea.html
  18. 1
      frontend/public/templates/work_packages/tabs/overview.html
  19. 30
      lib/api/decorators/single.rb
  20. 31
      lib/api/root.rb
  21. 3
      lib/api/v3/activities/activity_representer.rb
  22. 1
      lib/api/v3/attachments/attachment_representer.rb
  23. 22
      lib/api/v3/categories/categories_api.rb
  24. 50
      lib/api/v3/categories/categories_by_project_api.rb
  25. 21
      lib/api/v3/categories/category_representer.rb
  26. 8
      lib/api/v3/priorities/priority_representer.rb
  27. 15
      lib/api/v3/projects/project_representer.rb
  28. 10
      lib/api/v3/projects/projects_api.rb
  29. 19
      lib/api/v3/queries/queries_api.rb
  30. 1
      lib/api/v3/root.rb
  31. 8
      lib/api/v3/statuses/status_representer.rb
  32. 15
      lib/api/v3/users/user_representer.rb
  33. 4
      lib/api/v3/utilities/path_helper.rb
  34. 35
      lib/api/v3/versions/version_representer.rb
  35. 14
      lib/api/v3/work_packages/form/work_package_payload_representer.rb
  36. 92
      lib/api/v3/work_packages/work_package_representer.rb
  37. 38
      lib/api/v3/work_packages/work_packages_api.rb
  38. 5
      lib/open_project/configuration.rb
  39. 24
      lib/open_project/configuration/helpers.rb
  40. 50
      lib/open_project/plugins/module_handler.rb
  41. 17
      lib/redmine/access_control.rb
  42. 12
      lib/redmine/menu_manager/menu_helper.rb
  43. 4
      spec/factories/user_factory.rb
  44. 4
      spec/factories/work_package_factory.rb
  45. 68
      spec/features/menu_items/admin_menu_item_spec.rb
  46. 46
      spec/lib/api/v3/categories/category_representer_spec.rb
  47. 3
      spec/lib/api/v3/projects/project_representer_spec.rb
  48. 12
      spec/lib/api/v3/statuses/status_representer_spec.rb
  49. 26
      spec/lib/api/v3/support/links.rb
  50. 1
      spec/lib/api/v3/utilities/date_time_formatter_spec.rb
  51. 8
      spec/lib/api/v3/utilities/path_helper_spec.rb
  52. 53
      spec/lib/api/v3/versions/version_representer_spec.rb
  53. 173
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  54. 49
      spec/lib/open_project/plugins/module_handler_spec.rb
  55. 73
      spec/lib/redmine/access_control_spec.rb
  56. 90
      spec/requests/api/v3/category_resource_spec.rb
  57. 5
      spec/requests/api/v3/project_resource_spec.rb
  58. 2
      spec/requests/api/v3/support/api_helper.rb
  59. 5
      spec/requests/api/v3/work_package_resource_spec.rb

@ -35,6 +35,11 @@ class ReportingsController < ApplicationController
before_filter :find_project_by_project_id
before_filter :authorize
before_filter :find_reporting, only: [:show, :edit, :update, :confirm_destroy, :destroy]
before_filter :build_reporting, only: :create
before_filter :check_visibility, except: [:create, :index, :new, :available_projects]
accept_key_auth :index, :show
menu_item :reportings
@ -174,9 +179,6 @@ class ReportingsController < ApplicationController
end
def show
@reporting = @project.reportings_via_source.find(params[:id])
check_visibility
respond_to do |format|
format.html
end
@ -197,18 +199,7 @@ class ReportingsController < ApplicationController
end
def create
@reporting = @project.reportings_via_source.build
@reporting.reporting_to_project_id = params['reporting']['reporting_to_project_id']
if @reporting.reporting_to_project.nil?
flash.now[:error] = l('timelines.reporting_could_not_be_saved')
render action: :new, status: :unprocessable_entity
return
end
check_visibility
if @reporting.save
if @reporting.reporting_to_project.present? && @reporting.project.visible? && @reporting.save
flash[:notice] = l(:notice_successful_create)
redirect_to project_reportings_path
else
@ -218,18 +209,12 @@ class ReportingsController < ApplicationController
end
def edit
@reporting = @project.reportings_via_source.find(params[:id])
check_visibility
respond_to do |format|
format.html
end
end
def update
@reporting = @project.reportings_via_source.find(params[:id])
check_visibility
if @reporting.update_attributes(params[:reporting])
flash[:notice] = l(:notice_successful_update)
redirect_to project_reportings_path
@ -240,25 +225,28 @@ class ReportingsController < ApplicationController
end
def confirm_destroy
@reporting = @project.reportings_via_source.find(params[:id])
check_visibility
respond_to do |format|
format.html
end
end
def destroy
@reporting = @project.reportings_via_source.find(params[:id])
check_visibility
@reporting.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to project_reportings_path
end
protected
def find_reporting
@reporting = @project.reportings_via_source.find(params[:id])
end
def build_reporting
@reporting = @project.reportings_via_source.build
@reporting.reporting_to_project_id = params['reporting']['reporting_to_project_id']
end
def check_visibility
raise ActiveRecord::RecordNotFound unless @reporting.visible?
end

@ -97,6 +97,41 @@
# See following page:
#
# http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration
#
# Disable default module:
#
# By default user may choose which modules can be disabled,
# they should be listed as an array in yml format more information
# regarding yml format you can find here:
# http://symfony.com/doc/current/components/yaml/yaml_format.html
#
# disabled_modules:
# - module_name_1
# - module_name_2
#
# Hide menu items:
#
# By default user may choose which menu items can be disabled,
# they should be listed as an array in yml format more information
# regarding yml format you can find here:
# http://symfony.com/doc/current/components/yaml/yaml_format.html
#
# production:
# hidden_menu_items:
# admin_menu:
# - roles
# - types
# - statuses
# - workflows
# - enumerations
# - settings
# - ldap_authentication
# - colors
# - project_types
# - export_card_configurations
# - plugins
# - info
#
# default configuration options for all environments

@ -0,0 +1,33 @@
#-- 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.
#++
unless OpenProject::Configuration['disabled_modules'].empty?
to_disable = OpenProject::Configuration['disabled_modules']
OpenProject::Plugins::ModuleHandler.disable_modules(to_disable)
end

@ -333,6 +333,7 @@ de:
type: "Typ"
updatedAt: "Aktualisiert"
versionName: "Version"
version: "Version"
query:
column_names: "Spalten"
group_by: "Gruppiere Ergebnisse nach"
@ -389,12 +390,13 @@ de:
delete: Beziehung löschen
inplace:
button_edit: "%{attribute} bearbeiten"
button_save: "Speichern"
button_save_and_send: "Speichern mit E-Mail-Benachrichtigung"
button_cancel: "Abbrechen"
button_save: "%{attribute}: Speichern"
button_save_and_send: "%{attribute} Speichern mit E-Mail-Benachrichtigung"
button_cancel: "%{attribute} Abbrechen"
link_formatting_help: "Textformatierung"
btn_preview_enable: "Vorschau"
btn_preview_disable: "Vorschau deaktivieren"
null_value_label: "Kein Wert"
error_could_not_resolve_version_name: "Versionsbezeichner konnte nicht aufgelöst werden"
error_could_not_resolve_user_name: "Benutzername konnte nicht aufgelöst werden"

@ -336,6 +336,7 @@ en:
type: "Type"
updatedAt: "Updated on"
versionName: "Version"
version: "Version"
query:
column_names: "Columns"
group_by: "Group results by"
@ -391,13 +392,14 @@ en:
empty: No relation exists
delete: Delete relation
inplace:
button_edit: "Edit %{attribute}"
button_save: "Save"
button_save_and_send: "Save and send email"
button_cancel: "Cancel"
button_edit: "%{attribute}: Edit"
button_save: "%{attribute}: Save"
button_save_and_send: "%{attribute}: Save and send email"
button_cancel: "%{attribute}: Cancel"
link_formatting_help: "Text formatting"
btn_preview_enable: "Preview"
btn_preview_disable: "Disable preview"
null_value_label: "No value"
error_could_not_resolve_version_name: "Couldn't resolve version name"
error_could_not_resolve_user_name: "Couldn't resolve user name"

@ -83,6 +83,8 @@ storage config above like this:
* [`omniauth_direct_login_provider`](#omniauth-direct-login-provider) (default: nil)
* [`disable_password_login`](#disable-password-login) (default: false)
* [`attachments_storage`](#attachments-storage) (default: file)
* [`hidden_menu_items`](#hidden-menu-items) (default: {})
* [`disabled_modules`](#disabled-modules) (default: [])
### disable password login
@ -147,6 +149,65 @@ In the case of fog you only have to configure everything under `fog`, however. D
to `fog` just yet. Instead leave it as `file`. This is because the current attachments storage is used as the source
for the migration.
### hidden menu items
*default: {}*
You can disable specific menu items in the menu sidebar for each main menu (such as Administration and Projects).
The following example disables all menu items except 'Users', 'Groups' and 'Custom fields' under 'Administration':
```
hidden_menu_items:
admin_menu:
- roles
- types
- statuses
- workflows
- enumerations
- settings
- ldap_authentication
- colors
- project_types
- export_card_configurations
- plugins
- info
```
The configuration can be overridden through environment variables.
You have to define one variable for each menu.
For instance 'Roles' and 'Types' under 'Administration' can be disabled by defining the following variable:
```
OPENPROJECT_HIDDEN__MENU__ITEMS_ADMIN__MENU='roles types'
```
### disabled modules
*default: []*
Modules may be disabled through the configuration.
Just give a list of the module names either as an array or as a string with values separated by spaces.
**Array example:**
```
disabled_modules:
- backlogs
- meetings
```
**String example:**
```
disabled_modules: backlogs meetings
```
The option to use a string is mostly relevant for when you want to override the disabled modules via ENV variables:
```
OPENPROJECT_DISABLED__MODULES='backlogs meetings'
```
## Email configuration
* `email_delivery_method`: The way emails should be delivered. Possible values: `smtp` or `sendmail`
@ -168,3 +229,5 @@ for the migration.
* `cache_memcache_server`: The memcache server host and IP (default: `127.0.0.1:11211`)
* `cache_expires_in`: Expiration time for memcache entries (default: `0`, no expiry)
* `cache_namespace`: Namespace for cache keys, useful when multiple applications use a single memcache server (default: none)

@ -600,9 +600,18 @@ Updates an activity's comment and, on success, returns the updated activity.
"elements": [
{
"_links": {
"self": { "href": "/api/v3/categories/10" },
"project": { "href": "/api/v3/projects/11" },
"defaultAssignee": { "href": "/api/v3/users/42" }
"self": {
"href": "/api/v3/categories/10",
"title": "Category with assignee"
},
"project": {
"href": "/api/v3/projects/11",
"title": "Example project"
},
"defaultAssignee": {
"href": "/api/v3/users/42",
"title": "John Sheppard"
}
},
"_type": "Category",
"id": 10,
@ -655,9 +664,18 @@ Updates an activity's comment and, on success, returns the updated activity.
{
"_links": {
"self": { "href": "/api/v3/categories/10" },
"project": { "href": "/api/v3/projects/11" },
"defaultAssignee": { "href": "/api/v3/users/42" }
"self": {
"href": "/api/v3/categories/10",
"title": "Category with assignee"
},
"project": {
"href": "/api/v3/projects/11",
"title": "Example project"
},
"defaultAssignee": {
"href": "/api/v3/users/42",
"title": "John Sheppard"
}
},
"_type": "Category",
"id": 10,
@ -2673,6 +2691,10 @@ the human readable name of custom fields.*
"href": "/api/v3/work_packages/1298",
"title": "nisi eligendi officiis eos delectus quis voluptas dolores"
},
"category": {
"href": "/api/v3/categories/1298",
"title": "eligend isi"
},
"children": [
{
"href": "/api/v3/work_packages/1529",

@ -74,19 +74,22 @@ module.exports = function() {
embedded: false,
placeholder: '-',
displayStrategy: 'user',
attributeTitle: I18n.t('js.work_packages.properties.assignee')
},
responsible: {
type: 'select2',
attribute: 'responsible',
embedded: false,
placeholder: '-',
displayStrategy: 'user'
displayStrategy: 'user',
attributeTitle: I18n.t('js.work_packages.properties.responsible')
},
status: {
type: 'select2',
attribute: 'status.name',
embedded: true,
placeholder: '-'
placeholder: '-',
attributeTitle: I18n.t('js.work_packages.properties.status')
},
versionName: {
type: 'select2',

@ -608,8 +608,10 @@ module.exports = function($timeout) {
bustVerticalOffsetCache: function(tree) {
tree.iterateWithChildren(function(node) {
var currentElement = node.getDOMElement();
currentElement.removeAttr("data-vertical-offset");
currentElement.removeAttr("data-vertical-bottom-offset");
if (currentElement) {
currentElement.removeAttr("data-vertical-offset");
currentElement.removeAttr("data-vertical-bottom-offset");
}
});
},
rebuildForeground: function(tree) {
@ -655,6 +657,9 @@ module.exports = function($timeout) {
tree.iterateWithChildren(function(node, indent, index) {
var currentElement = node.getDOMElement();
if (!currentElement) {
return;
}
var currentOffset = timeline.getRelativeVerticalOffset(currentElement);
var previousElement, previousEnd, groupHeight;
var groupingChanged = false;

@ -854,8 +854,10 @@ module.exports = function($q, FilterQueryStringBuilder, Color, HistoricalPlannin
TimelineLoader.prototype.registerPlanningElementsByID = function (ids) {
this.inChunks(ids, function (planningElementIdsOfPacket, i) {
var projectPrefix = PathHelper.staticBase +
this.options.api_prefix;
var projectPrefix = this.globalPrefix +
PathHelper.projectsPath() +
"/" +
this.options.project_id;
// load current planning elements.
this.loader.register(
@ -865,7 +867,6 @@ module.exports = function($q, FilterQueryStringBuilder, Color, HistoricalPlannin
planningElementIdsOfPacket.join(',')},
{ storeIn: PlanningElement.identifier }
);
// load historical planning elements.
// TODO: load historical PEs here!
if (this.options.target_time) {

@ -85,9 +85,9 @@ module.exports = function(
$scope.isBusy = false;
$scope.readValue = '';
$scope.editTitle = I18n.t('js.inplace.button_edit', { attribute: $scope.attributeTitle });
$scope.saveTitle = I18n.t('js.inplace.button_save');
$scope.saveAndSendTitle = I18n.t('js.inplace.button_save_and_send');
$scope.cancelTitle = I18n.t('js.inplace.button_cancel');
$scope.saveTitle = I18n.t('js.inplace.button_save', { attribute: $scope.attributeTitle });
$scope.saveAndSendTitle = I18n.t('js.inplace.button_save_and_send', { attribute: $scope.attributeTitle });
$scope.cancelTitle = I18n.t('js.inplace.button_cancel', { attribute: $scope.attributeTitle });
$scope.error = null;
$scope.options = [];
@ -104,6 +104,8 @@ module.exports = function(
$scope.acceptErrors = acceptErrors;
$scope.pathHelper = PathHelper;
$scope.nullValueLabel = I18n.t('js.inplace.null_value_label');
activate();
function activate() {

@ -192,7 +192,7 @@ module.exports = function($sce, $http, $timeout, AutoCompleteHelper, TextileServ
scope.$on('focusSelect2', function() {
$timeout(function() {
element.find('.select2-choice').trigger('click');
}, 0, false);
});
});
},
startEditing: setOptions,

@ -27,7 +27,7 @@
"hyperagent": "manwithtwowatches/hyperagent#v0.4.2",
"lodash": "~2.4.1",
"foundation-apps": "~1.0.2",
"ui-select": "angular-ui/ui-select#~0.9.5"
"ui-select": "0xF013/ui-select#1a67dea0f6076e8f33354bf573bf5482539f289f"
},
"devDependencies": {
"mocha": "~1.14.0",

@ -1,6 +1,22 @@
<div class="inplace-editor type-{{type}} attribute-{{attribute}}" ng-class="{busy: isBusy, preview: isPreview, editable: isEditable}" ng-disabled="isBusy">
<div class="ined-read-value" ng-class="{ default: placeholderSet, editable: isEditable }" ng-hide="isEditing">
<span class="read-value-wrapper" ng-include src="getDisplayTemplateUrl()"></span>
<div class="inplace-editor type-{{type}} attribute-{{attribute}}" ng-class="{busy: isBusy, preview: isPreview, editable: isEditable}" aria-busy="{{ isBusy }}" ng-disabled="!isEditable">
<div class="ined-read-value" ng-class="{ default: placeholderSet, editable: isEditable }" ng-hide="isEditing" ng-switch="type">
<span class="read-value-wrapper" ng-switch-when="wiki_textarea" ng-bind-html="readValue"></span>
<span class="read-value-wrapper" ng-switch-default>
<span ng-if="!isUserLink">
<span ng-if="attribute == 'version.name'">
<span ng-if="entity.links.version">
<a href="{{pathHelper.versionPath(entity.embedded.version.props.id)}}">
{{entity.links.version.props.title}}
</a>
</span>
<span ng-if="!entity.links.version">
{{readValue}}
</span>
</span>
<span ng-if="attribute != 'version.name'" ng-bind="readValue"></span>
</span>
<user-field ng-if="isUserLink" user="readValue"></user-field>
</span>
<span ng-if="isEditable" class="editing-link-wrapper">
<accessible-by-keyboard execute="startEditing()">
<icon-wrapper icon-name="edit" icon-title="{{ editTitle }}">
@ -19,7 +35,7 @@
</div>
</div>
<div class="ined-dashboard">
<div class="ined-errors" ng-show="error" role="alert" ng-bind="error"></div>
<div class="ined-errors" ng-show="error" role="alert" ng-bind="error" aria-live="polite"></div>
<div class="ined-controls" ng-hide="isBusy">
<accessible-by-keyboard execute="editForm.$valid && submit(false)" class="ined-edit-save">
<icon-wrapper icon-name="yes" icon-title="{{ saveTitle }}">

@ -2,10 +2,12 @@
name="value"
ng-disabled="isBusy"
ng-model="dataObject.value"
title="{{ editTitle }}"
reset-search-input="true"
theme="select2">
<ui-select-match>{{$select.selected.name}}</ui-select-match>
<ui-select-match>{{ $select.selected.name }}</ui-select-match>
<ui-select-choices
repeat="item.href as item in options | filter: $select.search">
<div ng-bind-html="item.name | highlight: $select.search"></div>
<div aria-label="{{ item.name || nullValueLabel }}" ng-bind-html="item.name | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>

@ -2,4 +2,5 @@
name="value"
type="text"
ng-disabled="isBusy"
title="{{ editTitle }}"
ng-model="dataObject.value" />

@ -5,5 +5,6 @@
name="value"
ng-disabled="isBusy"
ng-model="dataObject.value"
title="{{ editTitle }}"
data-wp_autocomplete_url="{{ autocompletePath }}">
</textarea>

@ -54,6 +54,7 @@
ined-entity="workPackage"
ined-attribute="{{inplaceProperties[propertyData.property].attribute}}"
ined-attribute-embedded="inplaceProperties[propertyData.property].embedded"
ined-attribute-title="{{ inplaceProperties[propertyData.property].attributeTitle }}"
placeholder="{{inplaceProperties[propertyData.property].placeholder}}">
</div>
</div>

@ -51,8 +51,38 @@ module API
exec_context: :decorator,
render_nil: false
def self.self_link(path: nil, title_getter: -> (*) { represented.name })
link :self do
path = _type.underscore unless path
link_object = { href: api_v3_paths.send(path, represented.id) }
link_object[:title] = instance_eval(&title_getter)
link_object
end
end
def self.linked_property(property,
path: property,
association: property,
title_getter: -> (*) { represented.send(association).name },
show_if: -> (*) { true })
link property do
next unless instance_eval(&show_if)
value = represented.send(association)
link_object = { href: (api_v3_paths.send(path, value.id) if value) }
link_object[:title] = instance_eval(&title_getter) if value
link_object
end
end
private
def datetime_formatter
API::V3::Utilities::DateTimeFormatter
end
def _type; end
end
end

@ -67,13 +67,34 @@ module API
end
def authenticate
raise API::Errors::Unauthenticated if current_user.nil? || current_user.anonymous? if Setting.login_required?
if Setting.login_required? && (current_user.nil? || current_user.anonymous?)
raise API::Errors::Unauthenticated
end
end
def authorize(permission, context: nil, global: false, user: current_user, &block)
is_authorized = AuthorizationService.new(permission,
context: context,
global: global,
user: user).call
return true if is_authorized
if block_given?
yield block
else
raise API::Errors::Unauthorized
end
false
end
def authorize(permission, context: nil, global: false, user: current_user, allow: true)
is_authorized = AuthorizationService.new(permission, context: context, global: global, user: user).call
raise API::Errors::Unauthorized unless is_authorized && allow
is_authorized
def authorize_by_with_raise(&_block)
if yield
true
else
raise API::Errors::Unauthorized
end
end
def running_in_test_env?

@ -29,7 +29,8 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
API::V3::Utilities::DateTimeFormatter
module API
module V3

@ -29,7 +29,6 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3

@ -32,16 +32,20 @@ module API
module Categories
class CategoriesAPI < Grape::API
resources :categories do
before do
@categories = @project.categories
end
get do
self_link = api_v3_paths.categories(@project.identifier)
namespace ':id' do
before do
@category = Category.find(params[:id])
authorize(:view_project, context: @category.project) do
raise API::Errors::NotFound.new(
I18n.t('api_v3.errors.code_404',
type: I18n.t('activerecord.models.category'),
id: params[:id]))
end
end
CategoryCollectionRepresenter.new(@categories,
@categories.count,
self_link)
get do
CategoryRepresenter.new(@category)
end
end
end
end

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

@ -34,6 +34,27 @@ module API
module V3
module Categories
class CategoryRepresenter < ::API::Decorators::Single
link :self do
{
href: api_v3_paths.category(represented.id),
title: "#{represented.name}"
}
end
link :project do
{
href: api_v3_paths.project(represented.project.id),
title: represented.project.name
}
end
link :user do
{
href: api_v3_paths.user(represented.assigned_to.id),
title: represented.assigned_to.name
} if represented.assigned_to
end
property :id, render_nil: true
property :name, render_nil: true

@ -34,12 +34,8 @@ module API
module V3
module Priorities
class PriorityRepresenter < ::API::Decorators::Single
link :self do
{
href: api_v3_paths.priority(represented.id),
title: represented.name
}
end
self_link
property :id, render_nil: true
property :name

@ -29,20 +29,13 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3
module Projects
class ProjectRepresenter < ::API::Decorators::Single
include API::V3::Utilities
link :self do
{
href: api_v3_paths.project(represented.id),
title: "#{represented.name}"
}
end
self_link
link 'categories' do
{ href: api_v3_paths.categories(represented.id) }
@ -61,10 +54,12 @@ module API
property :created_on,
as: 'createdAt',
getter: -> (*) { DateTimeFormatter::format_datetime(created_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) }
property :updated_on,
as: 'updatedAt',
getter: -> (*) { DateTimeFormatter::format_datetime(updated_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) }
property :type, getter: -> (*) { project_type.try(:name) }, render_nil: true

@ -38,16 +38,22 @@ module API
namespace ':id' do
before do
@project = Project.find(params[:id])
authorize(:view_project, context: @project) do
raise API::Errors::NotFound.new(
I18n.t('api_v3.errors.code_404',
type: I18n.t('activerecord.models.project'),
id: params[:id]))
end
end
get do
authorize(:view_project, context: @project)
ProjectRepresenter.new(@project)
end
mount API::V3::Projects::AvailableAssigneesAPI
mount API::V3::Projects::AvailableResponsiblesAPI
mount API::V3::Categories::CategoriesAPI
mount API::V3::Categories::CategoriesByProjectAPI
mount API::V3::Versions::ProjectsVersionsAPI
end
end

@ -33,28 +33,26 @@ module API
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])
@representer = ::API::V3::Queries::QueryRepresenter.new(@query)
end
helpers do
def allowed_to_manage_stars?
# TODO: find a better way
action = env['api.endpoint'].options[:path].first
QueryPolicy.new(current_user).allowed?(@query, action)
def authorize_by_policy(action)
authorize_by_with_raise do
QueryPolicy.new(current_user).allowed?(@query, action)
end
end
end
patch :star do
# TODO Replace by QueryPolicy
authorize({ controller: :queries, action: :star }, context: @query.project, allow: allowed_to_manage_stars?)
authorize_by_policy(:star)
# Query name is not user-visible, but apparently used as CSS class. WTF.
# Normalizing the query name can result in conflicts and empty names in case all
# characters are filtered out. A random name doesn't have these problems.
@ -66,8 +64,8 @@ module API
end
patch :unstar do
# TODO Replace by QueryPolicy
authorize({ controller: :queries, action: :unstar }, context: @query.project, allow: allowed_to_manage_stars?)
authorize_by_policy(:unstar)
query_menu_item = @query.query_menu_item
return @representer if @query.query_menu_item.nil?
query_menu_item.destroy
@ -75,7 +73,6 @@ module API
@representer
end
end
end
end
end

@ -38,6 +38,7 @@ module API
mount ::API::V3::Activities::ActivitiesAPI
mount ::API::V3::Attachments::AttachmentsAPI
mount ::API::V3::Categories::CategoriesAPI
mount ::API::V3::Priorities::PrioritiesAPI
mount ::API::V3::Projects::ProjectsAPI
mount ::API::V3::Queries::QueriesAPI

@ -31,12 +31,8 @@ module API
module V3
module Statuses
class StatusRepresenter < ::API::Decorators::Single
link :self do
{
href: api_v3_paths.status(represented.id),
title: "#{represented.name}"
}
end
self_link
property :id, render_nil: true
property :name

@ -29,21 +29,14 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3
module Users
class UserRepresenter < ::API::Decorators::Single
include AvatarHelper
include API::V3::Utilities
link :self do
{
href: api_v3_paths.user(represented.id),
title: "#{represented.name} - #{represented.login}"
}
end
self_link
link :lock do
{
@ -89,10 +82,12 @@ module API
exec_context: :decorator
property :created_on,
as: 'createdAt',
getter: -> (*) { DateTimeFormatter::format_datetime(created_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) }
property :updated_on,
as: 'updatedAt',
getter: -> (*) { DateTimeFormatter::format_datetime(updated_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) }
property :status, getter: -> (*) { status_name }, render_nil: true
def _type

@ -62,6 +62,10 @@ module API
"#{project(project_id)}/categories"
end
def self.category(id)
"#{root}/categories/#{id}"
end
def self.preview_textile(link)
preview_markup(:textile, link)
end

@ -29,27 +29,18 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3
module Versions
class VersionRepresenter < ::API::Decorators::Single
include API::V3::Utilities
link :self do
{
href: api_v3_paths.version(represented.id),
title: "#{represented.name}"
}
end
self_link
link :definingProject do
{
href: api_v3_paths.project(represented.project.id),
title: represented.project.name
} if represented.project.visible?(current_user)
end
linked_property :definingProject,
path: :project,
association: :project,
show_if: -> (*) { represented.project.visible?(current_user) }
link :availableInProjects do
{
@ -71,19 +62,27 @@ module API
render_nil: true
property :start_date,
getter: -> (*) { DateTimeFormatter::format_date(start_date, allow_nil: true) },
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_date(represented.start_date, allow_nil: true)
},
render_nil: true
property :due_date,
as: 'endDate',
getter: -> (*) { DateTimeFormatter::format_date(due_date, allow_nil: true) },
exec_context: :decorator,
getter: -> (*) {
datetime_formatter.format_date(represented.due_date, allow_nil: true)
},
render_nil: true
property :status, render_nil: true
property :created_on,
as: 'createdAt',
getter: -> (*) { DateTimeFormatter::format_datetime(created_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) }
property :updated_on,
as: 'updatedAt',
getter: -> (*) { DateTimeFormatter::format_datetime(updated_on) }
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) }
def _type
'Version'

@ -29,7 +29,6 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3
@ -38,7 +37,6 @@ module API
class WorkPackagePayloadRepresenter < Roar::Decorator
include Roar::JSON::HAL
include Roar::Hypermedia
include API::V3::Utilities
self.as_strategy = ::API::Utilities::CamelCasingStrategy.new
@ -86,10 +84,10 @@ module API
property :start_date,
exec_context: :decorator,
getter: -> (*) {
DateTimeFormatter::format_date(represented.start_date, allow_nil: true)
datetime_formatter.format_date(represented.start_date, allow_nil: true)
},
setter: -> (value, *) {
represented.start_date = DateTimeFormatter::parse_date(value,
represented.start_date = datetime_formatter.parse_date(value,
'startDate',
allow_nil: true)
},
@ -97,10 +95,10 @@ module API
property :due_date,
exec_context: :decorator,
getter: -> (*) {
DateTimeFormatter::format_date(represented.due_date, allow_nil: true)
datetime_formatter.format_date(represented.due_date, allow_nil: true)
},
setter: -> (value, *) {
represented.due_date = DateTimeFormatter::parse_date(value,
represented.due_date = datetime_formatter.parse_date(value,
'dueDate',
allow_nil: true)
},
@ -120,6 +118,10 @@ module API
private
def datetime_formatter
API::V3::Utilities::DateTimeFormatter
end
def work_package_attribute_links_representer(represented)
::API::V3::WorkPackages::Form::WorkPackageAttributeLinksRepresenter.new represented
end

@ -29,20 +29,13 @@
require 'roar/decorator'
require 'roar/json/hal'
require 'api/v3/utilities/date_time_formatter'
module API
module V3
module WorkPackages
class WorkPackageRepresenter < ::API::Decorators::Single
include API::V3::Utilities
link :self do
{
href: api_v3_paths.work_package(represented.id),
title: represented.subject
}
end
self_link title_getter: -> (*) { represented.subject }
link :update do
{
@ -92,33 +85,11 @@ module API
} if current_user_allowed_to(:move_work_packages)
end
link :status do
{
href: api_v3_paths.status(represented.status_id),
title: represented.status.name
}
end
link :author do
{
href: api_v3_paths.user(represented.author.id),
title: "#{represented.author.name} - #{represented.author.login}"
} unless represented.author.nil?
end
link :responsible do
{
href: api_v3_paths.user(represented.responsible.id),
title: "#{represented.responsible.name} - #{represented.responsible.login}"
} unless represented.responsible.nil?
end
linked_property :status
link :assignee do
{
href: api_v3_paths.user(represented.assigned_to.id),
title: "#{represented.assigned_to.name} - #{represented.assigned_to.login}"
} unless represented.assigned_to.nil?
end
linked_property :author, path: :user
linked_property :responsible, path: :user
linked_property :assignee, path: :user, association: :assigned_to
link :availableWatchers do
{
@ -189,12 +160,10 @@ module API
} if current_user_allowed_to(:add_work_package_notes)
end
link :parent do
{
href: api_v3_paths.work_package(represented.parent.id),
title: represented.parent.subject
} unless represented.parent.nil? || !represented.parent.visible?
end
linked_property :parent,
path: :work_package,
title_getter: -> (*) { represented.parent.subject },
show_if: -> (*) { represented.parent.nil? || represented.parent.visible? }
link :timeEntries do
{
@ -204,20 +173,15 @@ module API
} if current_user_allowed_to(:view_time_entries)
end
link :version do
{
href: api_v3_paths.version(represented.fixed_version.id),
title: "#{represented.fixed_version.to_s_for_project(represented.project)}"
} if represented.fixed_version &&
version_policy.allowed?(represented.fixed_version, :show)
end
linked_property :category
link :priority do
{
href: api_v3_paths.priority(represented.priority.id),
title: represented.priority.name
}
end
linked_property :version,
association: :fixed_version,
title_getter: -> (*) {
represented.fixed_version.to_s_for_project(represented.project)
}
linked_property :priority
links :children do
visible_children.map do |child|
@ -242,29 +206,31 @@ module API
render_nil: true
property :start_date,
exec_context: :decorator,
getter: -> (*) do
DateTimeFormatter::format_date(start_date, allow_nil: true)
datetime_formatter.format_date(represented.start_date, allow_nil: true)
end,
render_nil: true
property :due_date,
exec_context: :decorator,
getter: -> (*) do
DateTimeFormatter::format_date(due_date, allow_nil: true)
datetime_formatter.format_date(represented.due_date, allow_nil: true)
end,
render_nil: true
property :estimated_time,
exec_context: :decorator,
getter: -> (*) do
DateTimeFormatter::format_duration_from_hours(represented.estimated_hours,
datetime_formatter.format_duration_from_hours(represented.estimated_hours,
allow_nil: true)
end,
exec_context: :decorator,
render_nil: true,
writeable: false
property :spent_time,
exec_context: :decorator,
getter: -> (*) do
DateTimeFormatter::format_duration_from_hours(represented.spent_hours)
datetime_formatter.format_duration_from_hours(represented.spent_hours)
end,
writeable: false,
exec_context: :decorator,
if: -> (_) { current_user_allowed_to(:view_time_entries) }
property :percentage_done,
render_nil: true,
@ -279,8 +245,12 @@ module API
property :project_id, getter: -> (*) { project.id }
property :project_name, getter: -> (*) { project.try(:name) }
property :parent_id, writeable: true
property :created_at, getter: -> (*) { DateTimeFormatter::format_datetime(created_at) }
property :updated_at, getter: -> (*) { DateTimeFormatter::format_datetime(updated_at) }
property :created_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.created_at) }
property :updated_at,
exec_context: :decorator,
getter: -> (*) { datetime_formatter.format_datetime(represented.updated_at) }
collection :custom_properties, exec_context: :decorator, render_nil: true

@ -31,19 +31,18 @@ module API
module WorkPackages
class WorkPackagesAPI < Grape::API
resources :work_packages do
params do
requires :id, desc: 'Work package id'
end
namespace ':id' do
helpers do
attr_reader :work_package
def write_work_package_attributes
if request_body
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter
.new(@work_package, enforce_lock_version_validation: true)
payload = ::API::V3::WorkPackages::Form::WorkPackagePayloadRepresenter.new(
@work_package,
enforce_lock_version_validation: true)
begin
payload.from_json(request_body.to_json)
@ -60,17 +59,20 @@ module API
def write_request_valid?
contract = WorkPackageContract.new(@representer.represented, current_user)
contract_valid = contract.validate
represented_valid = @representer.represented.valid?
return true if contract_valid && represented_valid
# We need to merge the contract errors with the model errors in
# order to have them available at one place.
unless contract.validate & @representer.represented.valid?
contract.errors.keys.each do |key|
contract.errors[key].each do |message|
@representer.represented.errors.add(key, message)
end
contract.errors.keys.each do |key|
contract.errors[key].each do |message|
@representer.represented.errors.add(key, message)
end
end
@representer.represented.errors.count == 0
false
end
end
@ -104,11 +106,12 @@ module API
end
resource :activities do
helpers do
def save_work_package(work_package)
if work_package.save
representer = ::API::V3::Activities::ActivityRepresenter.new(work_package.journals.last, current_user: current_user)
representer = ::API::V3::Activities::ActivityRepresenter.new(
work_package.journals.last,
current_user: current_user)
representer
else
@ -121,21 +124,24 @@ module API
requires :comment, type: String
end
post do
authorize({ controller: :journals, action: :new }, context: @work_package.project)
authorize({ controller: :journals, action: :new },
context: @work_package.project) do
raise API::Errors::NotFound.new(
I18n.t('api_v3.errors.code_404',
type: I18n.t('activerecord.models.work_package'),
id: params[:id]))
end
@work_package.journal_notes = params[:comment]
save_work_package(@work_package)
end
end
mount ::API::V3::WorkPackages::WatchersAPI
mount ::API::V3::Relations::RelationsAPI
mount ::API::V3::WorkPackages::Form::FormAPI
end
end
end
end

@ -74,7 +74,10 @@ module OpenProject
'disable_password_login' => false,
'omniauth_direct_login_provider' => nil,
'disable_password_choice' => false
'disable_password_choice' => false,
'disabled_modules' => [], # allow to disable default modules
'hidden_menu_items' => {}
}
@config = nil

@ -70,6 +70,18 @@ module OpenProject
available_file_uploaders[OpenProject::Configuration.attachments_storage.to_sym]
end
def hidden_menu_items
menus = self['hidden_menu_items'].map do |label, nodes|
[label, array(nodes)]
end
Hash[menus]
end
def disabled_modules
array self['disabled_modules']
end
def available_file_uploaders
{
fog: ::FogFileUploader,
@ -79,6 +91,18 @@ module OpenProject
private
##
# Yields the given configuration value as an array.
# Either the value already is an array or a string with values separated by spaces.
# In the latter case the string will be split and the values returned as an array.
def array(value)
if value =~ / /
value.split ' '
else
value
end
end
def true?(value)
['true', true].include? value # check string to accommodate ENV override
end

@ -0,0 +1,50 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++project_module
module OpenProject::Plugins
class ModuleHandler
@@disabled_modules = []
class << self
def disable_modules(module_names)
@@disabled_modules += Array(module_names).map(&:to_sym)
end
def disable(disabled_modules)
disabled_modules.map do |module_name|
Redmine::AccessControl.remove_modules_permissions(module_name)
end
end
end
OpenProject::Application.config.to_prepare do
OpenProject::Plugins::ModuleHandler.disable(@@disabled_modules)
end
end
end

@ -76,6 +76,23 @@ module Redmine
def modules_permissions(modules)
@permissions.select { |p| p.project_module.nil? || modules.include?(p.project_module.to_s) }
end
def remove_modules_permissions(module_name)
permissions = @permissions
module_permissions = permissions.select { |p| p.project_module.to_s == module_name.to_s }
clear_caches
@permissions = permissions - module_permissions
end
def clear_caches
@available_project_modules = nil
@public_permissions = nil
@members_only_permissions = nil
@loggedin_only_permissions = nil
end
end
class Mapper

@ -220,7 +220,7 @@ module Redmine::MenuManager::MenuHelper
def menu_items_for(menu, project=nil)
items = []
Redmine::MenuManager.items(menu).root.children.each do |node|
if allowed_node?(node, User.current, project)
if allowed_node?(node, User.current, project) && visible_node?(menu, node)
if block_given?
yield node
else
@ -265,4 +265,14 @@ module Redmine::MenuManager::MenuHelper
return true
end
end
def visible_node?(menu, node)
@hidden_menu_items ||= OpenProject::Configuration.hidden_menu_items
if @hidden_menu_items.length > 0
hidden_nodes = @hidden_menu_items[menu.to_s] || []
!hidden_nodes.include? node.name.to_s
else
true
end
end
end

@ -39,8 +39,8 @@ FactoryGirl.define do
sequence(:mail) { |n| "bob#{n}.bobbit@bob.com" }
password 'adminADMIN!'
password_confirmation 'adminADMIN!'
created_on { Time.now }
updated_on { Time.now }
created_on Time.now
updated_on Time.now
mail_notification(Redmine::VERSION::MAJOR > 0 ? 'all' : true)

@ -38,8 +38,8 @@ FactoryGirl.define do
sequence(:subject) { |n| "WorkPackage No. #{n}" }
description { |i| "Description for '#{i.subject}'" }
author factory: :user
created_at { Time.now }
updated_at { Time.now }
created_at Time.now
updated_at Time.now
callback(:after_build) do |work_package, evaluator|
work_package.type = work_package.project.types.first unless work_package.type

@ -0,0 +1,68 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'Admin menu items' do
let(:user) { FactoryGirl.create :admin }
before do
User.stub(:current).and_return user
end
after do
OpenProject::Configuration['hidden_menu_items'] = []
end
describe 'displaying all the menu items' do
it 'hides the specified admin menu items' do
visit admin_path
expect(page).to have_selector('a', text: I18n.t('label_user_plural'))
expect(page).to have_selector('a', text: I18n.t('label_project_plural'))
expect(page).to have_selector('a', text: I18n.t('label_role_plural'))
expect(page).to have_selector('a', text: I18n.t('label_type_plural'))
end
end
describe 'hiding menu items' do
before do
OpenProject::Configuration['hidden_menu_items'] = { 'admin_menu' => ['roles', 'types'] }
end
it 'hides the specified admin menu items' do
visit admin_path
expect(page).to have_selector('a', text: I18n.t('label_user_plural'))
expect(page).to have_selector('a', text: I18n.t('label_project_plural'))
expect(page).not_to have_selector('a', text: I18n.t('label_role_plural'))
expect(page).not_to have_selector('a', text: I18n.t('label_type_plural'))
end
end
end

@ -30,21 +30,53 @@ require 'spec_helper'
describe ::API::V3::Categories::CategoryRepresenter do
let(:category) { FactoryGirl.build(:category) }
let(:user) { FactoryGirl.build(:user) }
let(:representer) { described_class.new(category) }
context 'generation' do
subject(:generated) { representer.to_json }
it { should include_json('Category'.to_json).at_path('_type') }
shared_examples_for 'category has core values' do
it { is_expected.to include_json('Category'.to_json).at_path('_type') }
xit { should have_json_type(Object).at_path('_links') }
xit 'should link to self' do
expect(subject).to have_json_path('_links/self/href')
it { is_expected.to have_json_type(Object).at_path('_links') }
it 'should link to self' do
expect(subject).to have_json_path('_links/self/href')
end
it 'should display its name as title in self' do
expect(subject).to have_json_path('_links/self/title')
end
it 'should link to its project' do
expect(subject).to have_json_path('_links/project/href')
end
it 'should display its project title' do
expect(subject).to have_json_path('_links/project/title')
end
it { is_expected.to have_json_path('id') }
it { is_expected.to have_json_path('name') }
end
describe 'category' do
it { should have_json_path('id') }
it { should have_json_path('name') }
context 'default assignee not set' do
it_behaves_like 'category has core values'
it 'should not link to an assignee' do
expect(subject).to_not have_json_path('_links/user')
end
end
context 'default assignee set' do
let(:category) {
FactoryGirl.build(:category, assigned_to: user)
}
it_behaves_like 'category has core values'
it 'should link to its default assignee' do
expect(subject).to have_json_path('_links/user/href')
end
it 'should display the name of its default assignee' do
expect(subject).to have_json_path('_links/user/title')
end
end
end
end

@ -61,6 +61,9 @@ describe ::API::V3::Projects::ProjectRepresenter do
it 'should link to self' do
expect(subject).to have_json_path('_links/self/href')
end
it 'should have a title for link to self' do
expect(subject).to have_json_path('_links/self/title')
end
describe 'categories' do
it { should have_json_path('_links/categories') }

@ -61,13 +61,11 @@ describe ::API::V3::Statuses::StatusRepresenter do
it { is_expected.to have_json_type(Object).at_path('_links') }
describe 'self' do
let(:href) { "/api/v3/statuses/#{status.id}".to_json }
it { is_expected.to have_json_path('_links/self/href') }
it { is_expected.to have_json_path('_links/self/title') }
it { is_expected.to be_json_eql(href).at_path('_links/self/href') }
it { is_expected.to be_json_eql(status.name.to_json).at_path('_links/self/title') }
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { "/api/v3/statuses/#{status.id}" }
let(:title) { status.name }
end
end
end
end

@ -48,3 +48,29 @@ shared_examples_for 'action link' do
it { expect(subject).to have_json_path("_links/#{action}/href") }
end
end
shared_examples_for 'has a titled link' do
it { is_expected.to be_json_eql(href.to_json).at_path("_links/#{link}/href") }
it { is_expected.to be_json_eql(title.to_json).at_path("_links/#{link}/title") }
end
shared_examples_for 'has an untitled link' do
it { is_expected.to be_json_eql(href.to_json).at_path("_links/#{link}/href") }
it { is_expected.to_not have_json_path("_links/#{link}/title") }
end
shared_examples_for 'has an empty link' do
it { is_expected.to be_json_eql(nil.to_json).at_path("_links/#{link}/href") }
it 'has no embedded resource' do
is_expected.to_not have_json_path("_embedded/#{link}")
end
end
shared_examples_for 'has no link' do
it { is_expected.to_not have_json_path("_links/#{link}") }
it 'has no embedded resource' do
is_expected.to_not have_json_path("_embedded/#{link}")
end
end

@ -27,7 +27,6 @@
#++
require 'spec_helper'
require 'api/v3/utilities/date_time_formatter'
describe :DateTimeFormatter do
subject { ::API::V3::Utilities::DateTimeFormatter }

@ -89,6 +89,14 @@ describe ::API::V3::Utilities::PathHelper do
it { is_expected.to match(/^\/api\/v3\/projects\/42\/categories/) }
end
describe '#category' do
subject { helper.category 42 }
it_behaves_like 'api v3 path'
it { is_expected.to match(/^\/api\/v3\/categories\/42/) }
end
describe '#preview_textile' do
subject { helper.preview_textile '/api/v3/work_packages/42' }

@ -41,40 +41,47 @@ describe ::API::V3::Versions::VersionRepresenter do
it { should include_json('Version'.to_json).at_path('_type') }
context 'links' do
describe 'links' do
it { should have_json_type(Object).at_path('_links') }
it 'to self' do
path = api_v3_paths.version(version.id)
expect(subject).to be_json_eql(path.to_json).at_path('_links/self/href')
describe 'to self' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.version(version.id) }
let(:title) { version.name }
end
end
context 'to the defining project' do
let(:path) { api_v3_paths.project(version.project.id) }
it 'exists if the user has the permission to see the project' do
allow(version.project).to receive(:visible?).with(user).and_return(true)
subject = representer.to_json
expect(subject).to be_json_eql(path.to_json).at_path('_links/definingProject/href')
describe 'to the defining project' do
context 'if the user has the permission to see the project' do
before do
allow(version.project).to receive(:visible?).with(user).and_return(true)
end
it_behaves_like 'has a titled link' do
let(:link) { 'definingProject' }
let(:href) { api_v3_paths.project(version.project.id) }
let(:title) { version.project.name }
end
end
it 'does not exist if the user lacks the permission to see the project' do
allow(version.project).to receive(:visible?).with(user).and_return(false)
subject = representer.to_json
context 'if the user lacks the permission to see the project' do
before do
allow(version.project).to receive(:visible?).with(user).and_return(false)
end
expect(subject).to_not have_json_path('_links/definingProject/href')
it_behaves_like 'has no link' do
let(:link) { 'definingProject' }
end
end
end
it 'to available projects' do
path = api_v3_paths.versions_projects(version.id)
expect(subject).to be_json_eql(path.to_json).at_path('_links/availableInProjects/href')
describe 'to available projects' do
it_behaves_like 'has an untitled link' do
let(:link) { 'availableInProjects' }
let(:href) { api_v3_paths.versions_projects(version.id) }
end
end
end

@ -31,9 +31,7 @@ require 'spec_helper'
describe ::API::V3::WorkPackages::WorkPackageRepresenter do
include ::API::V3::Utilities::PathHelper
let(:member) do
FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
end
let(:member) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
let(:current_user) { member }
let(:representer) { described_class.new(work_package, current_user: current_user) }
@ -45,11 +43,9 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
due_date: Date.today.to_datetime,
created_at: DateTime.now,
updated_at: DateTime.now,
category: category,
done_ratio: 50,
estimated_hours: 6.0)
}
let(:category) { FactoryGirl.build(:category) }
let(:project) { work_package.project }
let(:permissions) {
[
@ -184,7 +180,7 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
context 'no view_time_entries permission' do
before do
allow(user).to receive(:allowed_to?).with(:view_time_entries, anything)
.and_return(false)
.and_return(false)
end
it { is_expected.to_not have_json_path('spentTime') }
@ -239,9 +235,10 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
describe '_links' do
it { is_expected.to have_json_type(Object).at_path('_links') }
it 'should link to self' do
expect(subject).to have_json_path('_links/self/href')
expect(subject).to have_json_path('_links/self/title')
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { "/api/v3/work_packages/#{work_package.id}" }
let(:title) { work_package.subject }
end
describe 'update links' do
@ -268,12 +265,59 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
end
describe 'status' do
let(:link) { "/api/v3/statuses/#{work_package.status_id}".to_json }
let(:title) { "#{work_package.status.name}".to_json }
it_behaves_like 'has a titled link' do
let(:link) { 'status' }
let(:href) { "/api/v3/statuses/#{work_package.status_id}" }
let(:title) { work_package.status.name }
end
end
describe 'author' do
it_behaves_like 'has a titled link' do
let(:link) { 'author' }
let(:href) { "/api/v3/users/#{work_package.author.id}" }
let(:title) { work_package.author.name }
end
end
describe 'assignee' do
context 'assignee is set' do
let(:work_package) {
FactoryGirl.build(:work_package, assigned_to: FactoryGirl.build(:user))
}
it { is_expected.to be_json_eql(link).at_path('_links/status/href') }
it_behaves_like 'has a titled link' do
let(:link) { 'assignee' }
let(:href) { "/api/v3/users/#{work_package.assigned_to.id}" }
let(:title) { work_package.assigned_to.name }
end
end
it { is_expected.to be_json_eql(title).at_path('_links/status/title') }
context 'assignee is not set' do
it_behaves_like 'has an empty link' do
let(:link) { 'assignee' }
end
end
end
describe 'responsible' do
context 'responsible is set' do
let(:work_package) {
FactoryGirl.build(:work_package, responsible: FactoryGirl.build(:user))
}
it_behaves_like 'has a titled link' do
let(:link) { 'responsible' }
let(:href) { "/api/v3/users/#{work_package.responsible.id}" }
let(:title) { work_package.responsible.name }
end
end
context 'responsible is not set' do
it_behaves_like 'has an empty link' do
let(:link) { 'responsible' }
end
end
end
describe 'version' do
@ -281,47 +325,58 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
let(:href_path) { '_links/version/href' }
context 'no version set' do
it 'has no version linked' do
is_expected.to_not have_json_path(href_path)
end
it 'has no version embedded' do
is_expected.to_not have_json_path(embedded_path)
it_behaves_like 'has an empty link' do
let(:link) { 'version' }
end
end
context 'version set' do
let!(:version) { FactoryGirl.create :version, project: project }
let(:expected_url) { api_v3_paths.version(version.id).to_json }
before do
work_package.fixed_version = version
end
it 'has a link to the version' do
is_expected.to be_json_eql(expected_url).at_path(href_path)
it_behaves_like 'has a titled link' do
let(:link) { 'version' }
let(:href) { api_v3_paths.version(version.id) }
let(:title) { version.to_s_for_project(project) }
end
it 'has the version embedded' do
is_expected.to be_json_eql('Version'.to_json).at_path("#{embedded_path}/_type")
is_expected.to be_json_eql(version.name.to_json).at_path("#{embedded_path}/name")
end
end
end
context ' but is not accessible due to permissions' do
before do
policy = double('VersionPolicy')
allow(policy).to receive(:allowed?).with(version, :show).and_return(false)
representer.instance_variable_set(:@version_policy, policy)
end
describe 'category' do
let(:embedded_path) { '_embedded/category' }
let(:href_path) { '_links/category/href' }
it 'has no version linked' do
is_expected.to_not have_json_path(href_path)
end
context 'no category set' do
it_behaves_like 'has an empty link' do
let(:link) { 'category' }
end
end
it 'has the version embedded as the user has the view work package permission' do
is_expected.to be_json_eql('Version'.to_json).at_path("#{embedded_path}/_type")
is_expected.to be_json_eql(version.name.to_json).at_path("#{embedded_path}/name")
end
context 'category set' do
let!(:category) { FactoryGirl.create :category, project: project }
before do
work_package.category = category
end
it_behaves_like 'has a titled link' do
let(:link) { 'category' }
let(:href) { api_v3_paths.category(category.id) }
let(:title) { category.name }
end
it 'has the category embedded' do
is_expected.to have_json_type(Hash).at_path('_embedded/category')
is_expected.to be_json_eql('Category'.to_json).at_path("#{embedded_path}/_type")
is_expected.to be_json_eql(category.name.to_json).at_path("#{embedded_path}/name")
end
end
end
@ -479,17 +534,42 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true)
end
context 'parent' do
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
parent_id: forbidden_work_package.id)
}
let!(:forbidden_work_package) do
FactoryGirl.create(:work_package, project: forbidden_project)
describe 'parent' do
let(:visible_parent) { FactoryGirl.create(:work_package, project: project) }
let(:invisible_parent) { FactoryGirl.create(:work_package, project: forbidden_project) }
let(:work_package) { FactoryGirl.create(:work_package, project: project) }
context 'no parent' do
it_behaves_like 'has an empty link' do
let(:link) { 'parent' }
end
end
context 'parent is visible' do
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
parent_id: visible_parent.id)
}
it_behaves_like 'has a titled link' do
let(:link) { 'parent' }
let(:href) { api_v3_paths.work_package(visible_parent.id) }
let(:title) { visible_parent.subject }
end
end
it { expect(subject).to_not have_json_path('_links/parent') }
context 'parent not visible' do
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
parent_id: invisible_parent.id)
}
it_behaves_like 'has no link' do
let(:link) { 'parent' }
end
end
end
context 'children' do
@ -601,11 +681,6 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
it { is_expected.not_to have_json_path('_embedded/watchers') }
end
end
describe 'category' do
it { is_expected.to have_json_type(Hash).at_path('_embedded/category') }
it { is_expected.to be_json_eql(%{Category}.to_json).at_path('_embedded/category/_type') }
end
end
end
end

@ -0,0 +1,49 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe OpenProject::Plugins::ModuleHandler do
let!(:all_former_permissions) { Redmine::AccessControl.permissions }
before do
disabled_modules = OpenProject::Plugins::ModuleHandler.disable_modules('repository')
OpenProject::Plugins::ModuleHandler.disable(disabled_modules)
end
after do
raise 'Test outdated' unless Redmine::AccessControl.instance_variable_defined?(:@permissions)
Redmine::AccessControl.instance_variable_set(:@permissions, all_former_permissions)
Redmine::AccessControl.clear_caches
end
context '#disable' do
it 'should disable repository module' do
expect(Redmine::AccessControl.available_project_modules).to_not include(:repository)
end
end
end

@ -0,0 +1,73 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Redmine::AccessControl do
describe '.remove_modules_permissions' do
let!(:all_former_permissions) { Redmine::AccessControl.permissions }
let!(:former_repository_permissions) do
module_permissions = Redmine::AccessControl.modules_permissions(['repository'])
module_permissions.select do |permission|
permission.project_module == :repository
end
end
subject { Redmine::AccessControl }
before do
Redmine::AccessControl.remove_modules_permissions(:repository)
end
after do
raise 'Test outdated' unless Redmine::AccessControl.instance_variable_defined?(:@permissions)
Redmine::AccessControl.instance_variable_set(:@permissions, all_former_permissions)
Redmine::AccessControl.clear_caches
end
it 'removes from global permissions' do
expect(subject.permissions).to_not include(former_repository_permissions)
end
it 'removes from public permissions' do
expect(subject.public_permissions).to_not include(former_repository_permissions)
end
it 'removes from members only permissions' do
expect(subject.members_only_permissions).to_not include(former_repository_permissions)
end
it 'removes from loggedin only permissions' do
expect(subject.loggedin_only_permissions).to_not include(former_repository_permissions)
end
it 'should disable repository module' do
expect(subject.available_project_modules).to_not include(:repository)
end
end
end

@ -32,30 +32,92 @@ require 'rack/test'
describe 'API v3 Category resource' do
include Rack::Test::Methods
let(:current_user) { FactoryGirl.create(:user) }
let(:role) { FactoryGirl.create(:role, permissions: []) }
let(:project) { FactoryGirl.create(:project, is_public: false) }
let(:categories) { FactoryGirl.create_list(:category, 3, project: project) }
let(:other_categories) { FactoryGirl.create_list(:category, 2) }
let(:role) { FactoryGirl.create(:role, permissions: [:view_project]) }
let(:private_project) { FactoryGirl.create(:project, is_public: false) }
let(:public_project) { FactoryGirl.create(:project, is_public: true) }
let(:anonymous_user) { FactoryGirl.create(:user) }
let(:privileged_user) do
FactoryGirl.create(:user,
member_in_project: private_project,
member_through_role: role)
end
let!(:categories) { FactoryGirl.create_list(:category, 3, project: private_project) }
let!(:other_categories) { FactoryGirl.create_list(:category, 2, project: public_project) }
let!(:user_categories) do
FactoryGirl.create_list(:category,
2,
project: private_project,
assigned_to: privileged_user)
end
describe 'categories by project' do
subject(:response) { last_response }
context 'logged in user' do
let(:get_path) { "/api/v3/projects/#{private_project.id}/categories" }
before do
allow(User).to receive(:current).and_return privileged_user
get get_path
end
it_behaves_like 'API V3 collection response', 5, 5, 'Category'
end
context 'not logged in user' do
let(:get_path) { "/api/v3/projects/#{private_project.id}/categories" }
before do
allow(User).to receive(:current).and_return anonymous_user
get get_path
end
it_behaves_like 'not found' do
let(:id) { "#{private_project.id}" }
let(:type) { 'Project' }
end
end
end
describe '#get' do
describe 'categories/:id' do
subject(:response) { last_response }
context 'logged in user' do
let(:get_path) { "/api/v3/projects/#{project.id}/categories" }
let(:get_path) { "/api/v3/categories/#{other_categories.first.id}" }
before do
allow(User).to receive(:current).and_return current_user
member = FactoryGirl.build(:member, user: current_user, project: project)
member.role_ids = [role.id]
member.save!
allow(User).to receive(:current).and_return privileged_user
get get_path
end
context 'valid priority id' do
it 'should return HTTP 200' do
expect(response.status).to eql(200)
end
end
context 'invalid priority id' do
let(:get_path) { '/api/v3/categories/bogus' }
it_behaves_like 'not found' do
let(:id) { 'bogus' }
let(:type) { 'Category' }
end
end
end
categories
other_categories
context 'not logged in user' do
let(:get_path) { '/api/v3/categories/bogus' }
before do
allow(User).to receive(:current).and_return anonymous_user
get get_path
end
it_behaves_like 'API V3 collection response', 3, 3, 'Category'
it_behaves_like 'not found' do
let(:id) { 'bogus' }
let(:type) { 'Category' }
end
end
end
end

@ -71,7 +71,10 @@ describe 'API v3 Project resource' do
let(:another_project) { FactoryGirl.create(:project, is_public: false) }
let(:get_path) { "/api/v3/projects/#{another_project.id}" }
it_behaves_like 'unauthorized access'
it_behaves_like 'not found' do
let(:id) { "#{another_project.id}" }
let(:type) { 'Project' }
end
end
end
end

@ -27,7 +27,7 @@
#++
shared_examples_for 'safeguarded API' do
it { expect(response.response_code).to eq(403) }
it { expect(response.response_code).to eq(404) }
end
shared_examples_for 'valid activity request' do

@ -494,6 +494,7 @@ h4. things we like
shared_examples_for 'handling people' do |property|
let(:user_parameter) { { _links: { property => { href: user_href } } } }
let(:href_path) { "_links/#{property}/href" }
describe 'nil' do
let(:user_href) { nil }
@ -502,14 +503,14 @@ h4. things we like
it { expect(response.status).to eq(200) }
it { expect(response.body).not_to have_json_path("_links/#{property}") }
it { expect(response.body).to be_json_eql(nil.to_json).at_path(href_path) }
it_behaves_like 'lock version updated'
end
describe 'valid' do
shared_examples_for 'valid user assignment' do
let(:title) { "#{assigned_user.name} - #{assigned_user.login}".to_json }
let(:title) { "#{assigned_user.name}".to_json }
it { expect(response.status).to eq(200) }

Loading…
Cancel
Save