diff --git a/app/models/queries/filters/base.rb b/app/models/queries/filters/base.rb index 30e47622781..932e2597e66 100644 --- a/app/models/queries/filters/base.rb +++ b/app/models/queries/filters/base.rb @@ -137,6 +137,20 @@ class Queries::Filters::Base [] end + # Hash representation of the value objects + # used to output model. + def value_objects_hash + value_objects.map do |value_object| + { + id: value_object.id, + value: value_object, + name: value_object.name, + href: nil, # Generated by path helper + identifier: value_object.class.name.demodulize.underscore + } + end + end + def operator_class operator_strategy end diff --git a/app/models/queries/work_packages/filter/assigned_to_filter.rb b/app/models/queries/work_packages/filter/assigned_to_filter.rb index e9c08f57632..c5555c62453 100644 --- a/app/models/queries/work_packages/filter/assigned_to_filter.rb +++ b/app/models/queries/work_packages/filter/assigned_to_filter.rb @@ -38,7 +38,7 @@ class Queries::WorkPackages::Filter::AssignedToFilter < values += principal_loader.group_values end - me_value + values.sort + me_allowed_value + values.sort end end diff --git a/app/models/queries/work_packages/filter/assignee_or_group_filter.rb b/app/models/queries/work_packages/filter/assignee_or_group_filter.rb index eab367cca36..3915f7e884a 100644 --- a/app/models/queries/work_packages/filter/assignee_or_group_filter.rb +++ b/app/models/queries/work_packages/filter/assignee_or_group_filter.rb @@ -38,7 +38,7 @@ class Queries::WorkPackages::Filter::AssigneeOrGroupFilter < values += principal_loader.group_values end - me_value + values.sort + me_allowed_value + values.sort end end diff --git a/app/models/queries/work_packages/filter/author_filter.rb b/app/models/queries/work_packages/filter/author_filter.rb index f65261ea76b..b06135ac3f2 100644 --- a/app/models/queries/work_packages/filter/author_filter.rb +++ b/app/models/queries/work_packages/filter/author_filter.rb @@ -32,7 +32,7 @@ class Queries::WorkPackages::Filter::AuthorFilter < Queries::WorkPackages::Filter::PrincipalBaseFilter def allowed_values @author_values ||= begin - me_value + principal_loader.user_values + me_allowed_value + principal_loader.user_values end end diff --git a/app/models/queries/work_packages/filter/principal_base_filter.rb b/app/models/queries/work_packages/filter/principal_base_filter.rb index 676218cb0c0..86189c7f173 100644 --- a/app/models/queries/work_packages/filter/principal_base_filter.rb +++ b/app/models/queries/work_packages/filter/principal_base_filter.rb @@ -34,9 +34,26 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter < User.current.logged? || allowed_values.any? end - def value_objects - prepared_values = values.map { |value| value == 'me' ? User.current.id : value } + def value_objects_hash + objects = super + # Replace me value identifier + if has_me_value? + search = User.current.id + objects.map! do |value_object| + if value_object[:id] == search + value_object[:id] = 'me' + value_object[:name] = I18n.t(:label_me) + break + end + end + end + + objects + end + + def value_objects + prepared_values = values.map { |value| value == me_value ? User.current.id : value } Principal.where(id: prepared_values) end @@ -44,18 +61,32 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter < true end + def principal_resource? + true + end + + def has_me_value? + values.include? me_value + end + def where operator_strategy.sql_for_field(values_replaced, self.class.model.table_name, self.class.key) end private - def me_value + def me_allowed_value values = [] - values << [I18n.t(:label_me), 'me'] if User.current.logged? + if User.current.logged? + values << [I18n.t(:label_me), me_value] + end values end + def me_value + 'me'.freeze + end + def principal_loader @principal_loader ||= ::Queries::WorkPackages::Filter::PrincipalLoader.new(project) end @@ -63,7 +94,7 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter < def values_replaced vals = values.clone - if vals.delete('me') + if vals.delete(me_value) if User.current.logged? vals.push(User.current.id.to_s) else diff --git a/app/models/queries/work_packages/filter/responsible_filter.rb b/app/models/queries/work_packages/filter/responsible_filter.rb index 5e3bdd0a0f5..7c55d4bf98f 100644 --- a/app/models/queries/work_packages/filter/responsible_filter.rb +++ b/app/models/queries/work_packages/filter/responsible_filter.rb @@ -32,7 +32,7 @@ class Queries::WorkPackages::Filter::ResponsibleFilter < def allowed_values @allowed_values ||= begin values = principal_loader.user_values - me_value + values + me_allowed_value + values end end diff --git a/app/models/queries/work_packages/filter/watcher_filter.rb b/app/models/queries/work_packages/filter/watcher_filter.rb index 4ef7d09784c..4b1f6175e93 100644 --- a/app/models/queries/work_packages/filter/watcher_filter.rb +++ b/app/models/queries/work_packages/filter/watcher_filter.rb @@ -38,7 +38,7 @@ class Queries::WorkPackages::Filter::WatcherFilter < # TODO: this could be differentiated # more, e.g. all users could watch issues in public projects, # but won't necessarily be shown here - values = me_value + values = me_allowed_value if User.current.allowed_to?(:view_work_package_watchers, project, global: project.nil?) values += principal_loader.user_values end diff --git a/app/models/user.rb b/app/models/user.rb index 071ba188eeb..1a37d3642f5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -132,6 +132,7 @@ class User < Principal validates_confirmation_of :password, allow_nil: true validates_inclusion_of :mail_notification, in: MAIL_NOTIFICATION_OPTIONS.map(&:first), allow_blank: true + validate :login_is_not_special_value validate :password_meets_requirements after_save :update_password @@ -705,6 +706,13 @@ class User < Principal protected + # Login must not be special value 'me' + def login_is_not_special_value + if login.present? && login == 'me' + errors.add(:login, :invalid) + end + end + # Password requirement validation based on settings def password_meets_requirements # Passwords are stored hashed as UserPasswords, diff --git a/docs/api/apiv3/endpoints/queries.apib b/docs/api/apiv3/endpoints/queries.apib index 0d1a55f93bf..45f47c85365 100644 --- a/docs/api/apiv3/endpoints/queries.apib +++ b/docs/api/apiv3/endpoints/queries.apib @@ -262,7 +262,7 @@ Retreive an individual query as identified by the id parameter. Then end point a + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown. - + timelineLabels = `{}` (optional: object, `{}`) ... Overridden labels in the timeline view + + timelineLabels = `{}` (optional, object, `{}`) ... Overridden labels in the timeline view + showHierarchies = `true` (optional, boolean, `true`) ... Indicates whether the hierarchy mode should be enabled. + Response 200 (application/hal+json) diff --git a/docs/api/apiv3/endpoints/users.apib b/docs/api/apiv3/endpoints/users.apib index aa0d3681d6b..02f36b06878 100644 --- a/docs/api/apiv3/endpoints/users.apib +++ b/docs/api/apiv3/endpoints/users.apib @@ -100,7 +100,7 @@ Please note that custom fields are not yet supported by the api although the bac ## View user [GET] + Parameters - + id (required, integer, `1`) ... User id + + id (required, integer or `me`, `1`) ... User id. Use `me` to reference current user, if any. + Response 200 (application/hal+json) diff --git a/frontend/app/components/api/api-v3/hal-resources/query-filter-instance-resource.service.ts b/frontend/app/components/api/api-v3/hal-resources/query-filter-instance-resource.service.ts index cb78ae1e03c..df8a5c47c1a 100644 --- a/frontend/app/components/api/api-v3/hal-resources/query-filter-instance-resource.service.ts +++ b/frontend/app/components/api/api-v3/hal-resources/query-filter-instance-resource.service.ts @@ -33,8 +33,8 @@ import {QueryOperatorResource} from './query-operator-resource.service'; import {QueryFilterInstanceSchemaResource} from './query-filter-instance-schema-resource.service'; interface QueryFilterInstanceResourceEmbedded { - filter: QueryFilterResource; - schema: QueryFilterInstanceSchemaResource; + filter:QueryFilterResource; + schema:QueryFilterInstanceSchemaResource; } interface QueryFilterInstanceResourceLinks extends QueryFilterInstanceResourceEmbedded { @@ -42,14 +42,14 @@ interface QueryFilterInstanceResourceLinks extends QueryFilterInstanceResourceEm export class QueryFilterInstanceResource extends HalResource { - public $embedded: QueryFilterInstanceResourceEmbedded; - public $links: QueryFilterInstanceResourceLinks; + public $embedded:QueryFilterInstanceResourceEmbedded; + public $links:QueryFilterInstanceResourceLinks; - public filter: QueryFilterResource; - public operator: QueryOperatorResource; - public values: HalResource[]|string[]; - public schema: QueryFilterInstanceSchemaResource; - private memoizedCurrentSchemas: {[key: string]: QueryFilterInstanceSchemaResource} = {}; + public filter:QueryFilterResource; + public operator:QueryOperatorResource; + public values:HalResource[]|string[]; + public schema:QueryFilterInstanceSchemaResource; + private memoizedCurrentSchemas:{ [key:string]:QueryFilterInstanceSchemaResource } = {}; public get id():string { return this.filter.id; @@ -80,13 +80,13 @@ export class QueryFilterInstanceResource extends HalResource { let operator = (schema.operator.allowedValues as HalResource[])[0]; let filter = (schema.filter.allowedValues as HalResource[])[0]; let source:any = { - name: filter.name, - _links: { - filter: filter.$plain()._links.self, - schema: schema.$plain()._links.self, - operator: operator.$plain()._links.self - } - } + name: filter.name, + _links: { + filter: filter.$plain()._links.self, + schema: schema.$plain()._links.self, + operator: operator.$plain()._links.self + } + } if (this.definesAllowedValues(schema)) { source._links['values'] = []; @@ -98,7 +98,6 @@ export class QueryFilterInstanceResource extends HalResource { newFilter.schema = schema; - return newFilter; } @@ -108,7 +107,7 @@ export class QueryFilterInstanceResource extends HalResource { private static definesAllowedValues(schema:QueryFilterInstanceSchemaResource) { return _.some(schema._dependencies[0].dependencies, - (dependency:any) => dependency.values && dependency.values._links && dependency.values._links.allowedValues ); + (dependency:any) => dependency.values && dependency.values._links && dependency.values._links.allowedValues); } } diff --git a/frontend/app/components/common/path-heleper/path-helper.service.js b/frontend/app/components/common/path-heleper/path-helper.service.js deleted file mode 100644 index 1e8301eaf21..00000000000 --- a/frontend/app/components/common/path-heleper/path-helper.service.js +++ /dev/null @@ -1,212 +0,0 @@ -// -- 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. -// ++ - -angular - .module('openproject.helpers') - .factory('PathHelper', PathHelper); - -function PathHelper() { - var PathHelper, - appBasePath = window.appBasePath ? window.appBasePath : ''; - - return PathHelper = { - staticBase: appBasePath, - - apiV2: appBasePath + '/api/v2', - apiV3: appBasePath + '/api/v3', - - activityPath: function() { - return PathHelper.staticBase + '/activity'; - }, - boardPath: function(projectIdentifier, boardIdentifier) { - return PathHelper.projectBoardsPath(projectIdentifier) + '/' + boardIdentifier; - }, - keyboardShortcutsHelpPath: function() { - return PathHelper.staticBase + '/help/keyboard_shortcuts'; - }, - messagePath: function(messageIdentifier) { - return PathHelper.staticBase + '/topics/' + messageIdentifier; - }, - myPagePath: function() { - return PathHelper.staticBase + '/my/page'; - }, - projectsPath: function() { - return PathHelper.staticBase + '/projects'; - }, - projectPath: function(projectIdentifier) { - return PathHelper.projectsPath() + '/' + projectIdentifier; - }, - projectActivityPath: function(projectIdentifier) { - return PathHelper.projectPath(projectIdentifier) + '/activity'; - }, - projectBoardsPath: function(projectIdentifier) { - return PathHelper.projectPath(projectIdentifier) + '/boards'; - }, - projectCalendarPath: function(projectId) { - return PathHelper.projectPath(projectId) + '/work_packages/calendar'; - }, - projectNewsPath: function(projectId) { - return PathHelper.projectPath(projectId) + '/news'; - }, - projectTimelinesPath: function(projectId) { - return PathHelper.projectPath(projectId) + '/timelines'; - }, - projectTimeEntriesPath: function(projectIdentifier) { - return PathHelper.projectPath(projectIdentifier) + '/time_entries'; - }, - projectWikiPath: function(projectId) { - return PathHelper.projectPath(projectId) + '/wiki'; - }, - projectWorkPackagePath: function(projectId, wpId) { - return PathHelper.projectWorkPackagesPath(projectId) + '/' + wpId; - }, - projectWorkPackagesPath: function(projectId) { - return PathHelper.projectPath(projectId) + '/work_packages'; - }, - projectWorkPackageNewPath: function(projectId) { - return PathHelper.projectWorkPackagesPath(projectId) + '/new'; - }, - queryPath: function(queryIdentifier) { - return PathHelper.staticBase + '/queries/' + queryIdentifier; - }, - timeEntriesPath: function(workPackageId) { - var suffix = '/time_entries'; - - if (workPackageId) { - return PathHelper.workPackagePath(workPackageId) + suffix; - } else { - return PathHelper.staticBase + suffix; // time entries root path - } - }, - timeEntryPath: function(timeEntryIdentifier) { - return PathHelper.staticBase + '/time_entries/' + timeEntryIdentifier; - }, - timeEntryEditPath: function(timeEntryIdentifier) { - return PathHelper.timeEntryPath(timeEntryIdentifier) + '/edit'; - }, - usersPath: function() { - return PathHelper.staticBase + '/users'; - }, - userPath: function(id) { - return PathHelper.usersPath() + '/' + id; - }, - versionsPath: function() { - return PathHelper.staticBase + '/versions'; - }, - versionPath: function(versionId) { - return PathHelper.versionsPath() + '/' + versionId; - }, - workPackagesPath: function() { - return PathHelper.staticBase + '/work_packages'; - }, - workPackagePath: function(id) { - return PathHelper.staticBase + '/work_packages/' + id; - }, - workPackageCopyPath: function(workPackageId) { - return PathHelper.workPackagePath(workPackageId) + '/copy'; - }, - workPackageDetailsCopyPath: function(projectIdentifier, workPackageId) { - return PathHelper.projectWorkPackagesPath(projectIdentifier) + '/details/' + workPackageId + '/copy'; - }, - workPackagesBulkDeletePath: function() { - return PathHelper.workPackagesPath() + '/bulk'; - }, - workPackagesBulkEditPath: function(workPackageIds) { - var query = _.reduce(workPackageIds, function(idsString, id) { - idsString += 'id[]=' + id + '&'; - return idsString; - }, '').slice(0, -1); - - return PathHelper.workPackagesBulkDeletePath + '/edit?' + query; - }, - workPackageJsonAutoCompletePath: function(projectId) { - var path = PathHelper.workPackagesPath() + '/auto_complete.json'; - if (projectId) { - path += '?project_id=' + projectId - } - - return path; - }, - - // API V2 - apiV2ProjectsPath: function() { - return PathHelper.apiV2 + '/projects'; - }, - - // API V3 - apiConfigurationPath: function() { - return PathHelper.apiV3 + '/configuration'; - }, - apiQueryStarPath: function(queryId) { - return PathHelper.apiV3QueryPath(queryId) + '/star'; - }, - apiQueryUnstarPath: function(queryId) { - return PathHelper.apiV3QueryPath(queryId) + '/unstar'; - }, - apiV3QueryPath: function(queryId) { - return PathHelper.apiV3 + '/queries/' + queryId; - }, - apiV3WorkPackagePath: function(workPackageId) { - return PathHelper.apiV3 + '/work_packages/' + workPackageId; - }, - apiV3WorkPackagesPath: function(workPackageId) { - return PathHelper.apiV3 + '/work_packages'; - }, - apiV3WorkPackageFormPath: function(projectIdentifier) { - return PathHelper.apiV3WorkPackagesPath() + '/form'; - }, - apiV3ProjectPath: function(projectIdentifier) { - return PathHelper.apiV3 + '/projects/' + projectIdentifier; - }, - apiV3AvailableProjectsPath: function() { - return PathHelper.apiV3WorkPackagesPath() + '/available_projects'; - }, - apiv3ProjectWorkPackagesPath: function(projectIdentifier) { - return PathHelper.apiV3ProjectPath(projectIdentifier) + '/work_packages'; - }, - apiV3ProjectCategoriesPath: function(projectIdentifier) { - return PathHelper.apiV3ProjectPath(projectIdentifier) + '/categories'; - }, - apiV3TypePath: function(typeId) { - return PathHelper.apiV3 + '/types/' + typeId; - }, - apiV3UserPath: function(userId) { - return PathHelper.apiV3 + '/users/' + userId; - }, - apiStatusesPath: function() { - return PathHelper.apiV3 + '/statuses'; - }, - apiProjectWorkPackageTypesPath: function(projectIdentifier) { - return PathHelper.apiV3ProjectPath(projectIdentifier) + '/types'; - }, - apiWorkPackageTypesPath: function() { - return PathHelper.apiV3 + '/types'; - }, - - }; -} diff --git a/frontend/app/components/common/path-heleper/path-helper.service.test.js b/frontend/app/components/common/path-heleper/path-helper.service.test.ts similarity index 92% rename from frontend/app/components/common/path-heleper/path-helper.service.test.js rename to frontend/app/components/common/path-heleper/path-helper.service.test.ts index 66fe45cdb74..0dc184d0726 100644 --- a/frontend/app/components/common/path-heleper/path-helper.service.test.js +++ b/frontend/app/components/common/path-heleper/path-helper.service.test.ts @@ -26,11 +26,13 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ +import {PathHelperService} from './path-helper.service'; + describe('PathHelper', function() { - var PathHelper; + var PathHelper:PathHelperService; beforeEach(angular.mock.module('openproject.helpers')); - beforeEach(inject(function(_PathHelper_) { + beforeEach(inject(function(_PathHelper_:PathHelperService) { PathHelper = _PathHelper_; })); diff --git a/frontend/app/components/common/path-heleper/path-helper.service.ts b/frontend/app/components/common/path-heleper/path-helper.service.ts new file mode 100644 index 00000000000..977ef172715 --- /dev/null +++ b/frontend/app/components/common/path-heleper/path-helper.service.ts @@ -0,0 +1,201 @@ +// -- 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. +// ++ + +export class PathHelperService { + public readonly appBasePath:string; + + constructor(public $window:ng.IWindowService) { + this.appBasePath = $window.appBasePath ? $window.appBasePath : ''; + } + + public get staticBase() { + return this.appBasePath; + } + + public get apiV2() { + return this.appBasePath + '/api/v2'; + } + + public get apiV3() { + return this.appBasePath + '/api/v3'; + } + + public boardPath(projectIdentifier:string, boardIdentifier:string) { + return this.projectBoardsPath(projectIdentifier) + '/' + boardIdentifier; + } + + public keyboardShortcutsHelpPath() { + return this.staticBase + '/help/keyboard_shortcuts'; + } + + public myPagePath() { + return this.staticBase + '/my/page'; + } + + public projectsPath() { + return this.staticBase + '/projects'; + } + + public projectPath(projectIdentifier:string) { + return this.projectsPath() + '/' + projectIdentifier; + } + + public projectActivityPath(projectIdentifier:string) { + return this.projectPath(projectIdentifier) + '/activity'; + } + + public projectBoardsPath(projectIdentifier:string) { + return this.projectPath(projectIdentifier) + '/boards'; + } + + public projectCalendarPath(projectId:string) { + return this.projectPath(projectId) + '/work_packages/calendar'; + } + + public projectNewsPath(projectId:string) { + return this.projectPath(projectId) + '/news'; + } + + public projectTimelinesPath(projectId:string) { + return this.projectPath(projectId) + '/timelines'; + } + + public projectTimeEntriesPath(projectIdentifier:string) { + return this.projectPath(projectIdentifier) + '/time_entries'; + } + + public projectWikiPath(projectId:string) { + return this.projectPath(projectId) + '/wiki'; + } + + public projectWorkPackagePath(projectId:string, wpId:string|number) { + return this.projectWorkPackagesPath(projectId) + '/' + wpId; + } + + public projectWorkPackagesPath(projectId:string) { + return this.projectPath(projectId) + '/work_packages'; + } + + public projectWorkPackageNewPath(projectId:string) { + return this.projectWorkPackagesPath(projectId) + '/new'; + } + + public timeEntriesPath(workPackageId:string|number) { + var suffix = '/time_entries'; + + if (workPackageId) { + return this.workPackagePath(workPackageId) + suffix; + } else { + return this.staticBase + suffix; // time entries root path + } + } + + public timeEntryPath(timeEntryIdentifier:string) { + return this.staticBase + '/time_entries/' + timeEntryIdentifier; + } + + public usersPath() { + return this.staticBase + '/users'; + } + + public userPath(id:string|number) { + return this.usersPath() + '/' + id; + } + + public versionsPath() { + return this.staticBase + '/versions'; + } + + public workPackagesPath() { + return this.staticBase + '/work_packages'; + } + + public workPackagePath(id:string|number) { + return this.staticBase + '/work_packages/' + id; + } + + public workPackageCopyPath(workPackageId:string|number) { + return this.workPackagePath(workPackageId) + '/copy'; + } + + public workPackageDetailsCopyPath(projectIdentifier:string, workPackageId:string|number) { + return this.projectWorkPackagesPath(projectIdentifier) + '/details/' + workPackageId + '/copy'; + } + + public workPackagesBulkDeletePath() { + return this.workPackagesPath() + '/bulk'; + } + + public workPackageJsonAutoCompletePath(projectId:string) { + var path = this.workPackagesPath() + '/auto_complete.json'; + if (projectId) { + path += '?project_id=' + projectId + } + + return path; + } + + // API V2 + public apiV2ProjectsPath() { + return this.apiV2 + '/projects'; + } + + // API V3 + public apiConfigurationPath() { + return this.apiV3 + '/configuration'; + } + + public apiV3WorkPackagePath(workPackageId:string|number) { + return this.apiV3 + '/work_packages/' + workPackageId; + } + + public apiV3ProjectPath(projectIdentifier:string) { + return this.apiV3 + '/projects/' + projectIdentifier; + } + + public apiV3ProjectCategoriesPath(projectIdentifier:string) { + return this.apiV3ProjectPath(projectIdentifier) + '/categories'; + } + + public apiV3UserPath(userId:string|number) { + return this.apiV3 + '/users/' + userId; + } + + public apiV3UserMePath() { + return this.apiV3UserPath('me'); + } + + public apiV3StatusesPath() { + return this.apiV3 + '/statuses'; + } +} + +angular + .module('openproject.helpers') + .service('PathHelper', PathHelperService); + diff --git a/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts b/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts index 0ac06ff2481..6a58b060df1 100644 --- a/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts +++ b/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts @@ -40,12 +40,13 @@ describe('toggledMultiselect Directive', function() { 'openproject.templates', 'openproject.services')); - beforeEach(inject(function($rootScope:any, $compile:any) { + beforeEach(inject(function($rootScope:any, $compile:any, $injector:any) { var html = ''; element = angular.element(html); rootScope = $rootScope; scope = $rootScope.$new(); + (window as any).ngInjector = $injector; allowedValues = [ { @@ -60,6 +61,7 @@ describe('toggledMultiselect Directive', function() { compile = function() { $compile(element)(scope); + angular.element(document.body).append(element); scope.$apply(); controller = element.controller('filterToggledMultiselectValue'); @@ -72,6 +74,7 @@ describe('toggledMultiselect Directive', function() { })); afterEach(angular.mock.inject(() => { I18n.t.restore(); + element.remove(); })); describe('with values', function() { @@ -126,10 +129,10 @@ describe('toggledMultiselect Directive', function() { expect(options.length).to.equal(2); expect(options[0].value).to.equal(allowedValues[0].$href); - expect(options[0].innerText).to.equal(allowedValues[0].name); + expect(options[0].textContent).to.equal(allowedValues[0].name); expect(options[1].value).to.equal(allowedValues[1].$href); - expect(options[1].innerText).to.equal(allowedValues[1].name); + expect(options[1].textContent).to.equal(allowedValues[1].name); }); xit('should render a link that toggles multi-select', function() { @@ -211,13 +214,15 @@ describe('toggledMultiselect Directive', function() { var options = select.find('option'); expect(options.length).to.equal(3); - expect(options[0].innerText).to.equal('PLACEHOLDER'); + expect(options[0].textContent).to.equal('PLACEHOLDER'); + console.error(options[1].textContent) + console.error(options[2].textContent) expect(options[1].value).to.equal(allowedValues[0].$href); - expect(options[1].innerText).to.equal(allowedValues[0].name); + expect(options[1].textContent).to.equal(allowedValues[0].name); expect(options[2].value).to.equal(allowedValues[1].$href); - expect(options[2].innerText).to.equal(allowedValues[1].name); + expect(options[2].textContent).to.equal(allowedValues[1].name); }); }); }); diff --git a/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.ts b/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.ts index dc4b020d415..0c4fbc8f657 100644 --- a/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.ts +++ b/frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.ts @@ -31,11 +31,18 @@ import {filtersModule} from '../../../angular-modules'; import {HalResource} from '../../api/api-v3/hal-resources/hal-resource.service'; import {UserResource} from '../../api/api-v3/hal-resources/user-resource.service'; import {CollectionResource} from '../../api/api-v3/hal-resources/collection-resource.service'; -import {QueryFilterInstanceResource} from '../../api/api-v3/hal-resources/query-filter-instance-resource.service'; +import { + QueryFilterInstanceResource +} from '../../api/api-v3/hal-resources/query-filter-instance-resource.service'; import {RootDmService} from '../../api/api-v3/hal-resource-dms/root-dm.service'; import {RootResource} from '../../api/api-v3/hal-resources/root-resource.service'; +import {PathHelperService} from '../../common/path-heleper/path-helper.service'; +import {$injectFields} from '../../angular/angular-injector-bridge.functions'; export class ToggledMultiselectController { + // Injected + public PathHelper:PathHelperService; + public isMultiselect: boolean; public filter:QueryFilterInstanceResource; @@ -47,6 +54,7 @@ export class ToggledMultiselectController { private I18n:op.I18n, private $q:ng.IQService, private RootDm:RootDmService) { + $injectFields(this, 'PathHelper'); this.isMultiselect = this.isValueMulti(true); this.text = { @@ -115,7 +123,7 @@ export class ToggledMultiselectController { let options = (resources[0] as CollectionResource).elements; if (isUserResource) { - this.addMeValue(options, (resources[1] as RootResource).user) + this.addMeValue(options, (resources[1] as RootResource).user); } this.availableOptions = options; @@ -123,17 +131,20 @@ export class ToggledMultiselectController { } private addMeValue(options:HalResource[], currentUser:UserResource) { - let currentUserHref = currentUser.$href; - - let me = _.find(options, user => user.$href === currentUser.$href); - - if (me) { - me = angular.copy(me); - - me.name = this.I18n.t('js.label_me'); - - options.unshift(me); + if (!(currentUser && currentUser.$href)) { + return; } + + let me:HalResource = new HalResource({ + _links: { + self: { + href: this.PathHelper.apiV3UserMePath(), + title: this.I18n.t('js.label_me') + } + } + }, true); + + options.unshift(me); } } diff --git a/frontend/app/components/wp-edit-form/work-package-filter-values.ts b/frontend/app/components/wp-edit-form/work-package-filter-values.ts index 5aa3434f0d9..c9fd0c49090 100644 --- a/frontend/app/components/wp-edit-form/work-package-filter-values.ts +++ b/frontend/app/components/wp-edit-form/work-package-filter-values.ts @@ -43,7 +43,7 @@ export class WorkPackageFilterValues { }); } - private setAllowedValueFor(form:FormResourceInterface, field:string, value:string | HalResource) { + private setAllowedValueFor(form:FormResourceInterface, field:string, value:string|HalResource) { return this.allowedValuesFor(form, field).then((allowedValues) => { let newValue; diff --git a/frontend/app/components/wp-query/url-params-helper.ts b/frontend/app/components/wp-query/url-params-helper.ts index 3d4242dce8b..1736440e501 100644 --- a/frontend/app/components/wp-query/url-params-helper.ts +++ b/frontend/app/components/wp-query/url-params-helper.ts @@ -28,10 +28,11 @@ import {QuerySortByResource} from "../api/api-v3/hal-resources/query-sort-by-resource.service"; import {QueryResource} from "../api/api-v3/hal-resources/query-resource.service"; +import {PathHelperService} from '../common/path-heleper/path-helper.service'; export class UrlParamsHelperService { - public constructor(public PaginationService:any) { + public constructor(public PaginationService:any, public PathHelper:PathHelperService) { } diff --git a/frontend/app/services/status-service.js b/frontend/app/services/status-service.js index c8dd0b929ba..caf7c4e9a65 100644 --- a/frontend/app/services/status-service.js +++ b/frontend/app/services/status-service.js @@ -30,7 +30,7 @@ module.exports = function($http, PathHelper) { var StatusService = { getStatuses: function() { - return StatusService.doQuery(PathHelper.apiStatusesPath()); + return StatusService.doQuery(PathHelper.apiV3StatusesPath()); }, doQuery: function(url, params) { diff --git a/frontend/tests/unit/tests/timelines/models/planning-element-test.js b/frontend/tests/unit/tests/timelines/models/planning-element-test.js index 6814d118bec..5aa2781f010 100644 --- a/frontend/tests/unit/tests/timelines/models/planning-element-test.js +++ b/frontend/tests/unit/tests/timelines/models/planning-element-test.js @@ -344,7 +344,7 @@ describe('Planning Element', function(){ describe('url', function () { beforeEach(function() { - PathHelper.staticBase = '/vtu'; + sinon.stub(PathHelper, 'staticBase', { get: function () { return '/vtu' }}); }); afterEach(function() { diff --git a/lib/api/utilities/resource_link_parser.rb b/lib/api/utilities/resource_link_parser.rb index 65a2ae014b9..ef4d88fd2b0 100644 --- a/lib/api/utilities/resource_link_parser.rb +++ b/lib/api/utilities/resource_link_parser.rb @@ -29,7 +29,7 @@ module API module Utilities - module ResourceLinkParser + class ResourceLinkParser # N.B. valid characters for URL path segments as of # http://tools.ietf.org/html/rfc3986#section-3.3 SEGMENT_CHARACTER = '(\w|[-~!$&\'\(\)*+\.,:;=@]|%[0-9A-Fa-f]{2})'.freeze diff --git a/lib/api/v3/queries/filters/query_filter_instance_representer.rb b/lib/api/v3/queries/filters/query_filter_instance_representer.rb index 247658c2144..79cdcf07470 100644 --- a/lib/api/v3/queries/filters/query_filter_instance_representer.rb +++ b/lib/api/v3/queries/filters/query_filter_instance_representer.rb @@ -77,17 +77,17 @@ module API link: ->(*) { next unless represented.ar_object_filter? - represented.value_objects.map do |value_object| - href = begin - api_v3_paths.send(value_object.class.name.demodulize.underscore, value_object.id) - rescue - Rails.logger.error "Failed to get href for value_object #{value_object}" + represented.value_objects_hash.map do |value_object| + value_object[:href] ||= begin + api_v3_paths.send(value_object[:identifier], value_object[:id]) + rescue => e + Rails.logger.error "Failed to get href for value_object #{value_object}: #{e}" nil end { - href: href, - title: value_object.name + href: value_object[:href], + title: value_object[:name] } end }, diff --git a/lib/api/v3/users/users_api.rb b/lib/api/v3/users/users_api.rb index 1e0d79fc2ce..65a6c3868d4 100644 --- a/lib/api/v3/users/users_api.rb +++ b/lib/api/v3/users/users_api.rb @@ -48,6 +48,14 @@ module API end end + def current_user_if_logged + if User.current.logged? + User.current + else + fail ::API::Errors::Unauthorized + end + end + def allow_only_admin unless current_user.admin? fail ::API::Errors::Unauthorized @@ -87,7 +95,12 @@ module API helpers ::API::V3::Users::UpdateUser before do - @user = User.find_by_unique!(params[:id]) + @user = + if params[:id] == 'me' + current_user_if_logged + else + User.find_by_unique!(params[:id]) + end end get do diff --git a/spec/features/work_packages/table/filter_spec.rb b/spec/features/work_packages/table/queries/filter_spec.rb similarity index 100% rename from spec/features/work_packages/table/filter_spec.rb rename to spec/features/work_packages/table/queries/filter_spec.rb diff --git a/spec/features/work_packages/table/queries/me_filter_spec.rb b/spec/features/work_packages/table/queries/me_filter_spec.rb new file mode 100644 index 00000000000..55c585a73d0 --- /dev/null +++ b/spec/features/work_packages/table/queries/me_filter_spec.rb @@ -0,0 +1,107 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe 'filter me value', js: true do + let(:project) { FactoryGirl.create :project, is_public: true } + let(:role) { FactoryGirl.create :existing_role, permissions: [:view_work_packages] } + let(:admin) { FactoryGirl.create :admin } + let(:user) { FactoryGirl.create :user } + let(:wp_admin) { FactoryGirl.create :work_package, project: project, assigned_to: admin } + let(:wp_user) { FactoryGirl.create :work_package, project: project, assigned_to: user } + + let(:wp_table) { ::Pages::WorkPackagesTable.new(project) } + let(:filters) { ::Components::WorkPackages::Filters.new } + + before do + login_as admin + project.add_member! admin, role + project.add_member! user, role + end + + context 'as anonymous', with_settings: { login_required?: false } do + let(:assignee_query) do + query = FactoryGirl.create(:query, + name: 'Assignee Query', + project: project, + user: user) + + query.add_filter('assigned_to_id', '=', ['me']) + query.save!(validate: false) + + query + end + + + it 'shows an error visiting a query with a me value' do + wp_table.visit_query assignee_query + wp_table.expect_notification(type: :error, + message: I18n.t('js.work_packages.faulty_query.description')) + end + end + + context 'logged in' do + before do + wp_admin + wp_user + + login_as(admin) + end + + it 'shows the one work package filtering for myself' do + wp_table.visit! + wp_table.expect_work_package_listed(wp_admin, wp_user) + + # Add and save query with me filter + filters.open + filters.remove_filter 'status' + filters.add_filter_by('Assignee', 'is', 'me') + + wp_table.expect_work_package_not_listed(wp_user) + wp_table.expect_work_package_listed(wp_admin) + + wp_table.save_as('Me query') + loading_indicator_saveguard + + # Expect correct while saving + wp_table.expect_title 'Me query' + query = Query.last + expect(query.filters.first.values).to eq ['me'] + filters.expect_filter_by('Assignee', 'is', 'me') + + # Revisit query + wp_table.visit_query query + wp_table.expect_work_package_not_listed(wp_user) + wp_table.expect_work_package_listed(wp_admin) + + filters.open + filters.expect_filter_by('Assignee', 'is', 'me') + end + end +end diff --git a/spec/features/work_packages/table/query_history_spec.rb b/spec/features/work_packages/table/queries/query_history_spec.rb similarity index 100% rename from spec/features/work_packages/table/query_history_spec.rb rename to spec/features/work_packages/table/queries/query_history_spec.rb diff --git a/spec/features/work_packages/table/query_menu_spec.rb b/spec/features/work_packages/table/queries/query_menu_spec.rb similarity index 100% rename from spec/features/work_packages/table/query_menu_spec.rb rename to spec/features/work_packages/table/queries/query_menu_spec.rb diff --git a/spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb b/spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb index 63d7ba781cb..b037e435570 100644 --- a/spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb +++ b/spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb @@ -61,13 +61,21 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do before do allow(User) .to receive(:current) - .and_return(assignee) + .and_return(assignee) end it 'returns the work package' do is_expected .to match_array [work_package] end + + it 'returns the corrected value object' do + objects = instance.value_objects_hash + + expect(objects.size).to eq(1) + expect(objects.first[:id]).to eq 'me' + expect(objects.first[:name]).to eq 'me' + end end context 'for the me value with another user being logged in' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ec58d94f499..d4b5ddb57b4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -61,6 +61,15 @@ describe User, type: :model do end end + describe 'a user with an invalid login' do + let(:login) { 'me' } + + it 'is invalid' do + user.login = login + expect(user).not_to be_valid + end + end + describe 'a user with and overly long login (> 256 chars)' do it 'is invalid' do user.login = 'a' * 257 diff --git a/spec/requests/api/v3/user/user_resource_spec.rb b/spec/requests/api/v3/user/user_resource_spec.rb index 6691d1785d9..8b1262d2b22 100644 --- a/spec/requests/api/v3/user/user_resource_spec.rb +++ b/spec/requests/api/v3/user/user_resource_spec.rb @@ -180,6 +180,15 @@ describe 'API v3 User resource', type: :request do let(:type) { 'User' } end end + + context 'requesting current user' do + let(:get_path) { api_v3_paths.user 'me' } + + it 'should response with 200' do + expect(subject.status).to eq(200) + expect(subject.body).to be_json_eql(user.name.to_json).at_path('name') + end + end end context 'get with login' do @@ -296,6 +305,14 @@ describe 'API v3 User resource', type: :request do let(:current_user) { FactoryGirl.create :anonymous } it_behaves_like 'deletion is not allowed' + + context 'requesting current user' do + let(:get_path) { api_v3_paths.user 'me' } + + it 'should response with 403' do + expect(subject.status).to eq(403) + end + end end end end