[26069] Allow passing of me-value link for principal filters again

The 'me' value is currently automatically normalized to the current
user, making it impossible to store queries that reference the current
user whenever accessing the query as before.
pull/5901/head
Oliver Günther 7 years ago
parent c02b1514db
commit fdfb41b327
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 14
      app/models/queries/filters/base.rb
  2. 2
      app/models/queries/work_packages/filter/assigned_to_filter.rb
  3. 2
      app/models/queries/work_packages/filter/assignee_or_group_filter.rb
  4. 2
      app/models/queries/work_packages/filter/author_filter.rb
  5. 41
      app/models/queries/work_packages/filter/principal_base_filter.rb
  6. 2
      app/models/queries/work_packages/filter/responsible_filter.rb
  7. 2
      app/models/queries/work_packages/filter/watcher_filter.rb
  8. 8
      app/models/user.rb
  9. 2
      docs/api/apiv3/endpoints/queries.apib
  10. 2
      docs/api/apiv3/endpoints/users.apib
  11. 35
      frontend/app/components/api/api-v3/hal-resources/query-filter-instance-resource.service.ts
  12. 212
      frontend/app/components/common/path-heleper/path-helper.service.js
  13. 6
      frontend/app/components/common/path-heleper/path-helper.service.test.ts
  14. 201
      frontend/app/components/common/path-heleper/path-helper.service.ts
  15. 17
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.test.ts
  16. 33
      frontend/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.directive.ts
  17. 2
      frontend/app/components/wp-edit-form/work-package-filter-values.ts
  18. 3
      frontend/app/components/wp-query/url-params-helper.ts
  19. 2
      frontend/app/services/status-service.js
  20. 2
      frontend/tests/unit/tests/timelines/models/planning-element-test.js
  21. 2
      lib/api/utilities/resource_link_parser.rb
  22. 14
      lib/api/v3/queries/filters/query_filter_instance_representer.rb
  23. 15
      lib/api/v3/users/users_api.rb
  24. 0
      spec/features/work_packages/table/queries/filter_spec.rb
  25. 107
      spec/features/work_packages/table/queries/me_filter_spec.rb
  26. 0
      spec/features/work_packages/table/queries/query_history_spec.rb
  27. 0
      spec/features/work_packages/table/queries/query_menu_spec.rb
  28. 10
      spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb
  29. 9
      spec/models/user_spec.rb
  30. 17
      spec/requests/api/v3/user/user_resource_spec.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

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

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

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

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

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

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

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

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

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

@ -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);
}
}

@ -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';
},
};
}

@ -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_;
}));

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

@ -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 = '<filter-toggled-multiselect-value icon-name="cool-icon.png" filter="filter"></filter-toggled-multiselect-value>';
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);
});
});
});

@ -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);
if (!(currentUser && currentUser.$href)) {
return;
}
me.name = this.I18n.t('js.label_me');
let me:HalResource = new HalResource({
_links: {
self: {
href: this.PathHelper.apiV3UserMePath(),
title: this.I18n.t('js.label_me')
}
}
}, true);
options.unshift(me);
}
options.unshift(me);
}
}

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

@ -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) {
}

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

@ -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() {

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

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

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

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

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

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

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

Loading…
Cancel
Save