mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[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.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
+17
-18
@@ -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';
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
+4
-2
@@ -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);
|
||||
|
||||
+11
-6
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user