Signed-off-by: Alex Coles <alex@alexbcoles.com> Conflicts: frontend/app/ui_components/index.js frontend/app/ui_components/with-dropdown-directive.js frontend/public/templates/work_packages.list.htmlpull/2294/head
commit
d8b155377a
@ -0,0 +1,157 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Version::ProjectSharing |
||||
# Returns all projects the version is available in |
||||
def projects |
||||
Project.scoped.joins(project_sharing_join) |
||||
end |
||||
|
||||
private |
||||
|
||||
def project_sharing_join |
||||
projects = Project.scoped |
||||
projects_table = projects.arel_table |
||||
versions_table = Version.scoped.arel_table |
||||
|
||||
sharing_inner_select = project_sharing_select(versions_table) |
||||
|
||||
join_condition = project_sharing_join_condition(sharing_inner_select, projects_table) |
||||
join_on = projects_table.create_on(join_condition) |
||||
|
||||
projects_table.create_join(sharing_inner_select, join_on) |
||||
end |
||||
|
||||
def project_sharing_select(versions_table) |
||||
sharing_select = if sharing == 'tree' |
||||
project_sharing_tree_select(versions_table) |
||||
else |
||||
project_sharing_default_select(versions_table) |
||||
end |
||||
|
||||
sharing_id_condition = versions_table[:id].eq(id) |
||||
|
||||
sharing_select |
||||
.where(sharing_id_condition) |
||||
.as('sharing') |
||||
end |
||||
|
||||
def project_sharing_tree_select(versions_table) |
||||
hierarchy_table = Project.scoped.arel_table |
||||
|
||||
roots_table = Project.arel_table.alias('roots') |
||||
roots_join_condition = project_sharing_tree_root_join_condition(roots_table, hierarchy_table) |
||||
sharing_select = join_project_and_version(hierarchy_table, versions_table) |
||||
|
||||
sharing_select |
||||
.join(roots_table) |
||||
.on(roots_join_condition) |
||||
|
||||
necessary_sharing_fields(sharing_select, |
||||
roots_table, |
||||
versions_table) |
||||
end |
||||
|
||||
def project_sharing_default_select(versions_table) |
||||
hierarchy_table = Project.scoped.arel_table |
||||
|
||||
sharing_select = join_project_and_version(hierarchy_table, versions_table) |
||||
|
||||
necessary_sharing_fields(sharing_select, |
||||
hierarchy_table, |
||||
versions_table) |
||||
end |
||||
|
||||
def necessary_sharing_fields(sharing_select, projects_table, versions_table) |
||||
sharing_select |
||||
.project(projects_table[:id], |
||||
versions_table[:id].as('version_id'), |
||||
projects_table[:lft], |
||||
projects_table[:rgt], |
||||
versions_table[:sharing]) |
||||
end |
||||
|
||||
def join_project_and_version(projects_table, versions_table) |
||||
join_condition = projects_table[:id].eq(versions_table[:project_id]) |
||||
|
||||
projects_table |
||||
.join(versions_table) |
||||
.on(join_condition) |
||||
end |
||||
|
||||
def project_sharing_tree_root_join_condition(roots_table, hierarchy_table) |
||||
roots_table[:lft].lteq(hierarchy_table[:lft]) |
||||
.and(roots_table[:rgt].gteq(hierarchy_table[:rgt])) |
||||
.and(roots_table[:parent_id].eq(nil)) |
||||
end |
||||
|
||||
def project_sharing_join_condition(sharing_table, projects_table) |
||||
case self[:sharing] |
||||
when 'tree' |
||||
project_sharing_tree_join_condition(sharing_table, projects_table) |
||||
when 'descendants' |
||||
project_sharing_descendants_join_condition(sharing_table, projects_table) |
||||
when 'hierarchy' |
||||
project_sharing_hierarchy_join_condition(sharing_table, projects_table) |
||||
when 'system' |
||||
Arel::Nodes::True.new |
||||
else |
||||
sharing_table[:id].eq(projects_table[:id]) |
||||
end |
||||
end |
||||
|
||||
def project_sharing_tree_join_condition(sharing_table, projects_table) |
||||
projects_table[:lft].gteq(sharing_table[:lft]) |
||||
.and(projects_table[:rgt].lteq(sharing_table[:rgt])) |
||||
end |
||||
|
||||
def project_sharing_descendants_join_condition(sharing_table, projects_table) |
||||
project_sharing_equal_condition(sharing_table, projects_table) |
||||
.or(project_sharing_below_condition(sharing_table, projects_table)) |
||||
end |
||||
|
||||
def project_sharing_hierarchy_join_condition(sharing_table, projects_table) |
||||
project_sharing_descendants_join_condition(sharing_table, projects_table) |
||||
.or(project_sharing_above_condition(sharing_table, projects_table)) |
||||
end |
||||
|
||||
def project_sharing_equal_condition(sharing_table, projects_table) |
||||
sharing_table[:id].eq(projects_table[:id]) |
||||
end |
||||
|
||||
def project_sharing_above_condition(sharing_table, projects_table) |
||||
projects_table[:lft].lt(sharing_table[:lft]) |
||||
.and(projects_table[:rgt].gt(sharing_table[:rgt])) |
||||
end |
||||
|
||||
def project_sharing_below_condition(sharing_table, projects_table) |
||||
projects_table[:lft].gt(sharing_table[:lft]) |
||||
.and(projects_table[:rgt].lt(sharing_table[:rgt])) |
||||
end |
||||
end |
@ -0,0 +1,56 @@ |
||||
#-- 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. |
||||
#++ |
||||
|
||||
class VersionPolicy < BasePolicy |
||||
private |
||||
|
||||
def cache |
||||
@cache ||= Hash.new do |hash, version| |
||||
# copy checks for the move_work_packages permission. This makes |
||||
# sense only because the work_packages/moves controller handles |
||||
# copying multiple work packages. |
||||
hash[version] = { |
||||
show: show_allowed?(version) |
||||
} |
||||
end |
||||
end |
||||
|
||||
def show_allowed?(version) |
||||
@show_cache ||= Hash.new do |hash, queried_version| |
||||
permissions = [:view_work_packages, :manage_versions] |
||||
|
||||
hash[queried_version] = permissions.any? do |permission| |
||||
allowed_condition = Project.allowed_to_condition(user, permission) |
||||
|
||||
queried_version.projects.where(allowed_condition).exists? |
||||
end |
||||
end |
||||
|
||||
@show_cache[version] |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
## |
||||
# Goes through all attachments looking for ones whose 'file' column, which is the new column |
||||
# used by the carrierwave-based attachments, is not set. |
||||
# |
||||
# For every one of those attachments the migration then sets the 'file' column to |
||||
# whatever the value of the legacy column 'filename' is. If that one is empty too |
||||
# it falls back to the 'disk_filename' column. This one was not meant to be displayed |
||||
# to users but it's better than nothing, especially when trying to identify corrupt attachments. |
||||
# |
||||
# If *that* column is empty too, the attachment is broken beyond repair and will be dropped. |
||||
# |
||||
# Note: Just because the 'file' column is restored doesn't mean the actual file exists. |
||||
# Rather the 'file' column being empty means precisely that the file is missing. |
||||
# By still writing the filename into the file column the attachment can at least |
||||
# be displayed, if not downloaded. |
||||
# |
||||
# Important: The migration is irreversible. |
||||
class PatchCorruptAttachments < ActiveRecord::Migration |
||||
def up |
||||
Attachment.all.each do |attachment| |
||||
patch_attachment attachment |
||||
end |
||||
end |
||||
|
||||
def down |
||||
puts "Won't revert this migration as it would mean breaking valid attachments. \ |
||||
We could break the attachments with missing files again by deleting their |
||||
file column to restore the state before the migration. But that doesn't help.".squish |
||||
end |
||||
|
||||
def patch_attachment(attachment) |
||||
attributes = attachment.attributes |
||||
if attributes['file'].blank? |
||||
# fall back to disk filename if necessary |
||||
file = attributes['filename'].presence || attributes['disk_filename'].presence |
||||
|
||||
if file |
||||
attachment.update_column :file, file |
||||
puts "updated attachment #{attachment.id}'s file column: #{file}" |
||||
else |
||||
# this really shouldn't happen - but just in case it does, it is more sensible |
||||
# to just delete the attachment because it will just break things |
||||
puts "could not patch #{attachment.inspect} - missing file name information - \ |
||||
it's hopeless ... deleting it".squish |
||||
attachment.destroy |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,129 +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.
|
||||
//++
|
||||
|
||||
// TODO move to UI components
|
||||
module.exports = function ($rootScope, $window, ESC_KEY, FocusHelper) { |
||||
|
||||
function position(dropdown, trigger) { |
||||
var hOffset = 0, |
||||
vOffset = 0; |
||||
|
||||
if( dropdown.length === 0 || !trigger ) return; |
||||
|
||||
// Styling logic taken from jQuery-dropdown plugin: https://github.com/plapier/jquery-dropdown
|
||||
// (dual MIT/GPL-Licensed)
|
||||
|
||||
// Position the dropdown relative-to-parent or relative-to-document
|
||||
if (dropdown.hasClass('dropdown-relative')) { |
||||
var leftPosition = dropdown.hasClass('dropdown-anchor-right') ? |
||||
trigger.position().left - (dropdown.outerWidth(true) - trigger.outerWidth(true)) - parseInt(trigger.css('margin-right')) + hOffset : |
||||
trigger.position().left + parseInt(trigger.css('margin-left')) + hOffset; |
||||
|
||||
if (dropdown.hasClass('dropdown-up')) { |
||||
var dropdownHeight = dropdown.outerHeight(true); |
||||
|
||||
dropdown.css({ |
||||
left: leftPosition, |
||||
top: trigger.position().top - dropdownHeight + parseInt(trigger.css('margin-top')) - vOffset |
||||
}); |
||||
} else { |
||||
var topBottomMargins = parseInt(trigger.css('margin-top')) + |
||||
parseInt(trigger.css('margin-bottom')); |
||||
|
||||
dropdown.css({ |
||||
left: leftPosition, |
||||
top: trigger.position().top + trigger.outerHeight(true) - topBottomMargins + vOffset |
||||
}); |
||||
} |
||||
} else { |
||||
dropdown.css({ |
||||
left: dropdown.hasClass('dropdown-anchor-right') ? |
||||
trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth()) + hOffset : trigger.offset().left + hOffset, |
||||
top: trigger.offset().top + trigger.outerHeight() + vOffset |
||||
}); |
||||
} |
||||
} |
||||
|
||||
function accessDropdown(dropdown) { |
||||
var links = dropdown.find('a'); |
||||
|
||||
if (links.length > 0) { |
||||
angular.element(links[0]).focus(); |
||||
} |
||||
|
||||
angular.element(dropdown).trap(); |
||||
} |
||||
|
||||
return { |
||||
restrict: 'EA', |
||||
scope: { |
||||
dropdownId: '@', |
||||
focusElementId: '@' |
||||
}, |
||||
link: function (scope, element, attributes) { |
||||
var dropdown = jQuery("#" + attributes.dropdownId), |
||||
trigger; |
||||
|
||||
$rootScope.$on('hideAllDropdowns', function(event){ |
||||
jQuery('.dropdown').hide(); |
||||
}); |
||||
|
||||
angular.element($window).on('resize', function(event) { |
||||
if(dropdown.is(':visible')) { |
||||
position(dropdown, trigger); |
||||
} |
||||
}); |
||||
|
||||
element.on('click', function (event) { |
||||
var showDropdown = dropdown.is(':hidden'); |
||||
|
||||
trigger = jQuery(this); |
||||
|
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
|
||||
scope.$emit('hideAllDropdowns'); |
||||
if (showDropdown) dropdown.show(); |
||||
|
||||
position(dropdown, trigger); |
||||
accessDropdown(dropdown); |
||||
|
||||
if(attributes.focusElementId) { |
||||
angular.element('#' + attributes.focusElementId).focus(); |
||||
} |
||||
}); |
||||
|
||||
angular.element(dropdown).on('keyup', function(event) { |
||||
if (event.keyCode === ESC_KEY) { |
||||
scope.$emit('hideAllDropdowns'); |
||||
FocusHelper.focusElement(element); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
}; |
@ -0,0 +1,140 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
|
||||
angular.module('openproject.workPackages') |
||||
.factory('ColumnContextMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
controller: 'ColumnContextMenuController', |
||||
controllerAs: 'contextMenu', |
||||
templateUrl: '/templates/work_packages/menus/column_context_menu.html', |
||||
container: '.work-packages--list-table-area' |
||||
}); |
||||
} |
||||
]) |
||||
.controller('ColumnContextMenuController', [ |
||||
'$scope', |
||||
'ColumnContextMenu', |
||||
'I18n', |
||||
'QueryService', |
||||
'WorkPackagesTableHelper', |
||||
'WorkPackagesTableService', |
||||
'columnsModal', |
||||
require('./column-context-menu-controller') |
||||
]) |
||||
.factory('SettingsDropdownMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
controller: 'SettingsDropdownMenuController', |
||||
templateUrl: '/templates/work_packages/menus/settings_dropdown_menu.html', |
||||
container: '#toolbar' |
||||
}); |
||||
} |
||||
]) |
||||
.controller('SettingsDropdownMenuController', [ |
||||
'$scope', |
||||
'I18n', |
||||
'columnsModal', |
||||
'exportModal', |
||||
'saveModal', |
||||
'settingsModal', |
||||
'shareModal', |
||||
'sortingModal', |
||||
'groupingModal', |
||||
'QueryService', |
||||
'AuthorisationService', |
||||
'$window', |
||||
'$state', |
||||
'$timeout', require('./settings-dropdown-menu-controller') |
||||
]) |
||||
.factory('TasksDropdownMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
controller: 'TasksDropdownMenuController', |
||||
templateUrl: '/templates/work_packages/menus/tasks_dropdown_menu.html', |
||||
container: '#toolbar' |
||||
}); |
||||
} |
||||
]) |
||||
.controller('TasksDropdownMenuController', [ |
||||
'$scope', |
||||
'PathHelper', require('./tasks-dropdown-menu-controller') |
||||
]) |
||||
.constant('PERMITTED_CONTEXT_MENU_ACTIONS', [ |
||||
'edit', 'watch', 'log_time', |
||||
'duplicate', 'move', 'copy', 'delete' |
||||
]) |
||||
.factory('WorkPackageContextMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
controller: 'WorkPackageContextMenuController', |
||||
controllerAs: 'contextMenu', |
||||
templateUrl: '/templates/work_packages/menus/work_package_context_menu.html' |
||||
}); |
||||
} |
||||
]) |
||||
.controller('WorkPackageContextMenuController', [ |
||||
'$scope', |
||||
'WorkPackagesTableHelper', |
||||
'WorkPackageContextMenuHelper', |
||||
'WorkPackageService', |
||||
'WorkPackagesTableService', |
||||
'I18n', |
||||
'$window', |
||||
'PERMITTED_CONTEXT_MENU_ACTIONS', |
||||
require('./work-package-context-menu-controller') |
||||
]) |
||||
.factory('DetailsMoreDropdownMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
templateUrl: '/templates/work_packages/menus/details_more_dropdown_menu.html', |
||||
container: '.work-packages--details-toolbar' |
||||
}); |
||||
} |
||||
]) |
||||
.factory('QuerySelectDropdownMenu', [ |
||||
'ngContextMenu', |
||||
function(ngContextMenu) { |
||||
return ngContextMenu({ |
||||
templateUrl: '/templates/work_packages/menus/query_select_dropdown_menu.html', |
||||
container: '.title-container', |
||||
controller: 'QuerySelectDropdownMenuController' |
||||
}); |
||||
} |
||||
]) |
||||
.controller('QuerySelectDropdownMenuController', [ |
||||
'$scope', |
||||
'$sce', 'LABEL_MAX_CHARS', 'KEY_CODES', |
||||
require('./query-select-dropdown-menu-controller') |
||||
]); |
@ -0,0 +1,204 @@ |
||||
module.exports = function($scope, $sce, LABEL_MAX_CHARS, KEY_CODES) { |
||||
var scope = $scope; |
||||
scope.$watch('groups', refreshFilteredGroups); |
||||
scope.$watch('selectedId', selectTitle); |
||||
|
||||
function refreshFilteredGroups() { |
||||
if (scope.groups) { |
||||
initFilteredModels(); |
||||
} |
||||
} |
||||
|
||||
function selectTitle() { |
||||
angular.forEach(scope.filteredGroups, function(group) { |
||||
if (group.models.length) { |
||||
angular.forEach(group.models, function(model){ |
||||
model.highlighted = model.id == scope.selectedId; |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function initFilteredModels() { |
||||
scope.filteredGroups = angular.copy(scope.groups); |
||||
angular.forEach(scope.filteredGroups, function(group) { |
||||
group.models = group.models.map(function(model){ |
||||
return { |
||||
label: model[0], |
||||
labelHtml: $sce.trustAsHtml(truncate(model[0], LABEL_MAX_CHARS)), |
||||
id: model[1], |
||||
highlighted: false |
||||
}; |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
function labelHtml(label, filterBy) { |
||||
var html; |
||||
filterBy = filterBy.toLowerCase(); |
||||
label = truncate(label, LABEL_MAX_CHARS); |
||||
if (label.toLowerCase().indexOf(filterBy) >= 0) { |
||||
html = label.substr(0, label.toLowerCase().indexOf(filterBy)) + |
||||
'<span class=\'filter-selection\'>' + |
||||
label.substr(label.toLowerCase().indexOf(filterBy), filterBy.length) + |
||||
'</span>' + label.substr(label.toLowerCase().indexOf(filterBy) + filterBy.length); |
||||
} else { |
||||
html = label; |
||||
} |
||||
return $sce.trustAsHtml(html); |
||||
} |
||||
|
||||
function truncate(text, chars) { |
||||
if (text.length > chars) { |
||||
return text.substr(0, chars) + '...'; |
||||
} |
||||
return text; |
||||
} |
||||
|
||||
function modelIndex(models) { |
||||
return models.map(function(model) { |
||||
return model.id; |
||||
}).indexOf(scope.selectedId); |
||||
} |
||||
|
||||
function performSelect() { |
||||
scope.transitionMethod(scope.selectedId); |
||||
} |
||||
|
||||
function nextNonEmptyGroup(groups, currentGroupIndex) { |
||||
currentGroupIndex = (currentGroupIndex === undefined) ? -1 : currentGroupIndex; |
||||
while (currentGroupIndex < groups.length - 1) { |
||||
if (groups[currentGroupIndex + 1].models.length) { |
||||
return groups[currentGroupIndex + 1]; |
||||
} |
||||
currentGroupIndex = currentGroupIndex + 1; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
function previousNonEmptyGroup(groups, currentGroupIndex) { |
||||
while (currentGroupIndex > 0) { |
||||
if(groups[currentGroupIndex - 1].models.length) { |
||||
return groups[currentGroupIndex - 1]; |
||||
} |
||||
currentGroupIndex = currentGroupIndex - 1; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
function getModelPosition(groups) { |
||||
for (var groupIdx = 0; groupIdx < groups.length; groupIdx++) { |
||||
var models = groups[groupIdx].models; |
||||
var modelIdx = modelIndex(models); |
||||
if(modelIdx >= 0) { |
||||
return { |
||||
group: groupIdx, |
||||
model: modelIdx |
||||
}; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
function selectNext() { |
||||
var groups = scope.filteredGroups, |
||||
nextGroup; |
||||
if(!scope.selectedId) { |
||||
nextGroup = nextNonEmptyGroup(groups); |
||||
scope.selectedId = nextGroup ? nextGroup.models[0].id : 0; |
||||
} else { |
||||
var position = getModelPosition(groups, scope.selectedId); |
||||
if (!position) { |
||||
return; |
||||
} |
||||
var models = groups[position.group].models; |
||||
|
||||
if(position.model == models.length - 1){ // It is the last in the group
|
||||
nextGroup = nextNonEmptyGroup(groups, position.group); |
||||
if(nextGroup) { |
||||
scope.selectedId = nextGroup.models[0].id; |
||||
} |
||||
} else { |
||||
scope.selectedId = models[position.model + 1].id; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function selectPrevious() { |
||||
var groups = scope.filteredGroups; |
||||
if (scope.selectedId) { |
||||
var position = getModelPosition(groups, scope.selectedId); |
||||
if (!position) { |
||||
return; |
||||
} |
||||
var models = groups[position.group].models; |
||||
|
||||
if (position.model === 0) { // It is the last in the group
|
||||
var previousGroup = previousNonEmptyGroup(groups, position.group); |
||||
if(previousGroup) { |
||||
scope.selectedId = previousGroup.models[previousGroup.models.length - 1].id; |
||||
} |
||||
} else { |
||||
scope.selectedId = models[position.model - 1].id; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function preventDefault(event) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
} |
||||
|
||||
angular.element('#title-filter').bind('click', function(event) { |
||||
preventDefault(event); |
||||
}); |
||||
|
||||
scope.handleSelection = function(event) { |
||||
switch(event.which) { |
||||
case KEY_CODES.enter: |
||||
performSelect(); |
||||
preventDefault(event); |
||||
break; |
||||
case KEY_CODES.down: |
||||
selectNext(); |
||||
preventDefault(event); |
||||
break; |
||||
case KEY_CODES.up: |
||||
selectPrevious(); |
||||
preventDefault(event); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
scope.reload = function(modelId, newTitle) { |
||||
scope.selectedTitle = newTitle; |
||||
scope.reloadMethod(modelId); |
||||
scope.$emit('hideAllDropdowns'); |
||||
}; |
||||
|
||||
scope.filterModels = function(filterBy) { |
||||
initFilteredModels(); |
||||
|
||||
scope.selectedId = 0; |
||||
angular.forEach(scope.filteredGroups, function(group) { |
||||
if (filterBy.length) { |
||||
group.filterBy = filterBy; |
||||
group.models = group.models.filter(function(model){ |
||||
return model.label.toLowerCase().indexOf(filterBy.toLowerCase()) >= 0; |
||||
}); |
||||
|
||||
if (group.models.length) { |
||||
angular.forEach(group.models, function(model){ |
||||
model['labelHtml'] = labelHtml(model.label, filterBy); |
||||
}); |
||||
if (!scope.selectedId) { |
||||
group.models[0].highlighted = true; |
||||
scope.selectedId = group.models[0].id; |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
}; |
@ -0,0 +1,163 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
module.exports = function( |
||||
$scope, I18n, columnsModal, |
||||
exportModal, saveModal, settingsModal, |
||||
shareModal, sortingModal, groupingModal, |
||||
QueryService, AuthorisationService, |
||||
$window, $state, $timeout) { |
||||
$scope.$watch('query.displaySums', function(newValue) { |
||||
$timeout(function() { |
||||
$scope.displaySumsLabel = (newValue) ? I18n.t('js.toolbar.settings.hide_sums') |
||||
: I18n.t('js.toolbar.settings.display_sums'); |
||||
}); |
||||
}); |
||||
|
||||
$scope.saveQuery = function(event){ |
||||
if($scope.query.isNew()){ |
||||
if( allowQueryAction(event, 'create') ){ |
||||
$scope.$emit('hideAllDropdowns'); |
||||
saveModal.activate(); |
||||
} |
||||
} else { |
||||
if( allowQueryAction(event, 'update') ) { |
||||
QueryService.saveQuery() |
||||
.then(function(data){ |
||||
$scope.$emit('flashMessage', data.status); |
||||
$state.go('work-packages.list', |
||||
{ 'query_id': $scope.query.id, 'query_props': null }, |
||||
{ notify: false }); |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
$scope.deleteQuery = function(event){ |
||||
if( allowQueryAction(event, 'delete') && preventNewQueryAction(event) && deleteConfirmed() ){ |
||||
QueryService.deleteQuery() |
||||
.then(function(data){ |
||||
settingsModal.deactivate(); |
||||
$scope.$emit('flashMessage', data.status); |
||||
$state.go('work-packages.list', |
||||
{ 'query_id': null, 'query_props': null }, |
||||
{ reload: true }); |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
// Modals
|
||||
$scope.showSaveAsModal = function(event){ |
||||
if( allowQueryAction(event, 'create') ) { |
||||
showExistingQueryModal.call(saveModal, event); |
||||
} |
||||
}; |
||||
|
||||
$scope.showShareModal = function(event){ |
||||
if (allowQueryAction(event, 'publicize') || allowQueryAction(event, 'star')) { |
||||
showExistingQueryModal.call(shareModal, event); |
||||
} |
||||
}; |
||||
|
||||
$scope.showSettingsModal = function(event){ |
||||
if( allowQueryAction(event, 'update') ) { |
||||
showExistingQueryModal.call(settingsModal, event); |
||||
} |
||||
}; |
||||
|
||||
$scope.showExportModal = function(event){ |
||||
if( allowWorkPackageAction(event, 'export') ) { |
||||
showModal.call(exportModal); |
||||
} |
||||
}; |
||||
|
||||
$scope.showColumnsModal = function(){ |
||||
showModal.call(columnsModal); |
||||
}; |
||||
|
||||
$scope.showGroupingModal = function(){ |
||||
showModal.call(groupingModal); |
||||
}; |
||||
|
||||
$scope.showSortingModal = function(){ |
||||
showModal.call(sortingModal); |
||||
}; |
||||
|
||||
$scope.toggleDisplaySums = function(){ |
||||
$scope.$emit('hideAllDropdowns'); |
||||
$scope.query.displaySums = !$scope.query.displaySums; |
||||
|
||||
// This eventually calls the resize event handler defined in the
|
||||
// WorkPackagesTable directive and ensures that the sum row at the
|
||||
// table footer is properly displayed.
|
||||
angular.element($window).trigger('resize'); |
||||
}; |
||||
|
||||
function preventNewQueryAction(event){ |
||||
if (event && $scope.query.isNew()) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
function showModal() { |
||||
$scope.$emit('hideAllDropdowns'); |
||||
this.activate(); |
||||
} |
||||
|
||||
function showExistingQueryModal(event) { |
||||
if( preventNewQueryAction(event) ){ |
||||
$scope.$emit('hideAllDropdowns'); |
||||
this.activate(); |
||||
} |
||||
} |
||||
|
||||
function allowQueryAction(event, action) { |
||||
return allowAction(event, 'query', action); |
||||
} |
||||
|
||||
function allowWorkPackageAction(event, action) { |
||||
return allowAction(event, 'work_package', action); |
||||
} |
||||
|
||||
function allowAction(event, modelName, action) { |
||||
if(AuthorisationService.can(modelName, action)){ |
||||
return true; |
||||
} else { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
function deleteConfirmed() { |
||||
return $window.confirm(I18n.t('js.text_query_destroy_confirmation')); |
||||
} |
||||
}; |
@ -1,169 +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.
|
||||
//++
|
||||
|
||||
module.exports = function(I18n, columnsModal, exportModal, saveModal, settingsModal, shareModal, sortingModal, groupingModal, QueryService, AuthorisationService, $window, $state, $timeout){ |
||||
|
||||
return { |
||||
restrict: 'AE', |
||||
scope: true, |
||||
link: function(scope, element, attributes) { |
||||
angular.element($window).bind('click', function() { |
||||
scope.$emit('hideAllDropdowns'); |
||||
}); |
||||
|
||||
scope.$watch('query.displaySums', function(newValue, oldValue) { |
||||
$timeout(function() { |
||||
scope.displaySumsLabel = (newValue) ? I18n.t('js.toolbar.settings.hide_sums') |
||||
: I18n.t('js.toolbar.settings.display_sums'); |
||||
}); |
||||
}); |
||||
|
||||
scope.saveQuery = function(event){ |
||||
if(scope.query.isNew()){ |
||||
if( allowQueryAction(event, 'create') ){ |
||||
scope.$emit('hideAllDropdowns'); |
||||
saveModal.activate(); |
||||
} |
||||
} else { |
||||
if( allowQueryAction(event, 'update') ) { |
||||
QueryService.saveQuery() |
||||
.then(function(data){ |
||||
scope.$emit('flashMessage', data.status); |
||||
$state.go('work-packages.list', |
||||
{ query_id: scope.query.id, query_props: null }, |
||||
{ notify: false }); |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
scope.deleteQuery = function(event){ |
||||
if( allowQueryAction(event, 'delete') && preventNewQueryAction(event) && deleteConfirmed() ){ |
||||
QueryService.deleteQuery() |
||||
.then(function(data){ |
||||
settingsModal.deactivate(); |
||||
scope.$emit('flashMessage', data.status); |
||||
$state.go('work-packages.list', |
||||
{ query_id: null, query_props: null }, |
||||
{ reload: true }); |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
// Modals
|
||||
scope.showSaveAsModal = function(event){ |
||||
if( allowQueryAction(event, 'create') ) { |
||||
showExistingQueryModal.call(saveModal, event); |
||||
} |
||||
}; |
||||
|
||||
scope.showShareModal = function(event){ |
||||
if (allowQueryAction(event, 'publicize') || allowQueryAction(event, 'star')) { |
||||
showExistingQueryModal.call(shareModal, event); |
||||
} |
||||
}; |
||||
|
||||
scope.showSettingsModal = function(event){ |
||||
if( allowQueryAction(event, 'update') ) { |
||||
showExistingQueryModal.call(settingsModal, event); |
||||
} |
||||
}; |
||||
|
||||
scope.showExportModal = function(event){ |
||||
if( allowWorkPackageAction(event, 'export') ) { |
||||
showModal.call(exportModal); |
||||
} |
||||
}; |
||||
|
||||
scope.showColumnsModal = function(){ |
||||
showModal.call(columnsModal); |
||||
}; |
||||
|
||||
scope.showGroupingModal = function(){ |
||||
showModal.call(groupingModal); |
||||
}; |
||||
|
||||
scope.showSortingModal = function(){ |
||||
showModal.call(sortingModal); |
||||
}; |
||||
|
||||
scope.toggleDisplaySums = function(){ |
||||
scope.$emit('hideAllDropdowns'); |
||||
scope.query.displaySums = !scope.query.displaySums; |
||||
|
||||
// This eventually calls the resize event handler defined in the
|
||||
// WorkPackagesTable directive and ensures that the sum row at the
|
||||
// table footer is properly displayed.
|
||||
angular.element($window).trigger('resize'); |
||||
}; |
||||
|
||||
function preventNewQueryAction(event){ |
||||
if (event && scope.query.isNew()) { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
function showModal() { |
||||
scope.$emit('hideAllDropdowns'); |
||||
this.activate(); |
||||
} |
||||
|
||||
function showExistingQueryModal(event) { |
||||
if( preventNewQueryAction(event) ){ |
||||
scope.$emit('hideAllDropdowns'); |
||||
this.activate(); |
||||
} |
||||
} |
||||
|
||||
function allowQueryAction(event, action) { |
||||
return allowAction(event, 'query', action); |
||||
} |
||||
|
||||
function allowWorkPackageAction(event, action) { |
||||
return allowAction(event, 'work_package', action); |
||||
} |
||||
|
||||
function allowAction(event, modelName, action) { |
||||
if(AuthorisationService.can(modelName, action)){ |
||||
return true; |
||||
} else { |
||||
event.preventDefault(); |
||||
event.stopPropagation(); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
function deleteConfirmed() { |
||||
return $window.confirm(I18n.t('js.text_query_destroy_confirmation')); |
||||
} |
||||
} |
||||
}; |
||||
}; |
@ -1,36 +1,12 @@ |
||||
<div class="title-container"> |
||||
<div class="text"> |
||||
<h2 title="{{ selectedTitle }}"> |
||||
<span with-dropdown dropdown-id="querySelectDropdown" focus-element-id="title-filter"> |
||||
<span has-dropdown-menu target="QuerySelectDropdownMenu" |
||||
locals="selectedTitle,groups,transitionMethod"> |
||||
<accessible-by-keyboard> |
||||
{{ selectedTitle | characters:50 }}<i class="icon-pulldown-arrow1 icon-button"></i> |
||||
</accessible-by-keyboard> |
||||
</span> |
||||
</h2> |
||||
</div> |
||||
|
||||
<div class="dropdown dropdown-relative" id="querySelectDropdown"> |
||||
<div class="search-query-wrapper"> |
||||
<input type="search" |
||||
ng-model="filterBy" |
||||
ng-change="filterModels(filterBy)" |
||||
ng-keydown="handleSelection($event)" |
||||
id="title-filter"><i id="magnifier" class="icon-search"></i> |
||||
</input> |
||||
</div> |
||||
|
||||
<div class="dropdown-scrollable"> |
||||
<div class="query-menu-container" ng-if="group.models" ng-repeat="group in filteredGroups"> |
||||
<ul class="query-menu"> |
||||
<div class="title-group-header">{{ group.name }}</div> |
||||
<li ng-repeat="model in group.models" |
||||
ng-class="{'selected': model.highlighted }" |
||||
ui-sref="work-packages.list({ query_id: model.id, query_props: undefined })"> |
||||
<a href title="{{ model.label }}" ng-bind-html="model.labelHtml"></a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
||||
|
@ -1,51 +1,52 @@ |
||||
<div id="column-context-menu" |
||||
class="action-menu dropdown-relative" |
||||
role="menu" |
||||
class="dropdown-relative dropdown action-menu" |
||||
ng-class="{'dropdown-anchor-right': column && column.name !== 'id'}"> |
||||
<ul class="menu"> |
||||
<li ng-if="canSort()" ng-click="sortAscending(column.name)"> |
||||
<a focus href=""> |
||||
<ul class="dropdown-menu"> |
||||
<li ng-if="canSort()" role="menuitem"> |
||||
<a role="menuitem" focus href="" ng-click="sortAscending(column.name)"> |
||||
<i class="icon-action-menu icon-sort-ascending"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.sort_ascending')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-if="canSort()" ng-click="sortDescending(column.name)"> |
||||
<a href=""> |
||||
<li ng-if="canSort()"> |
||||
<a role="menuitem" href="" ng-click="sortDescending(column.name)"> |
||||
<i class="icon-action-menu icon-sort-descending"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.sort_descending')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-if="isGroupable" ng-click="groupBy(column.name)"> |
||||
<a focus="focusFeature('group')" href=""> |
||||
<li ng-if="isGroupable"> |
||||
<a role="menuitem" focus="focusFeature('group')" href="" ng-click="groupBy(column.name)"> |
||||
<i class="icon-action-menu icon-group-by2"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.group')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-if="canMoveLeft()" ng-click="moveLeft(column.name)"> |
||||
<a focus="focusFeature('moveLeft')" href=""> |
||||
<li ng-if="canMoveLeft()"> |
||||
<a role="menuitem" focus="focusFeature('moveLeft')" href="" ng-click="moveLeft(column.name)"> |
||||
<i class="icon-action-menu icon-column-left"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.move_column_left')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-if="canMoveRight()" ng-click="moveRight(column.name)"> |
||||
<a focus="focusFeature('moveRight')" href=""> |
||||
<li ng-if="canMoveRight()"> |
||||
<a role="menuitem" focus="focusFeature('moveRight')" href="" ng-click="moveRight(column.name)"> |
||||
<i class="icon-action-menu icon-column-right"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.move_column_right')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-if="canBeHidden()" ng-click="hideColumn(column.name)"> |
||||
<a focus="focusFeature('hide')" href=""> |
||||
<li ng-if="canBeHidden()"> |
||||
<a role="menuitem" focus="focusFeature('hide')" href="" ng-click="hideColumn(column.name)"> |
||||
<i class="icon-action-menu icon-delete2"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.hide_column')"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li ng-click="insertColumns()"> |
||||
<a focus="focusFeature('insert')" href=""> |
||||
<li> |
||||
<a role="menuitem" focus="focusFeature('insert')" href="" ng-click="insertColumns()"> |
||||
<i class="icon-action-menu icon-columns"></i> |
||||
<span ng-bind="I18n.t('js.work_packages.query.insert_columns')"/> |
||||
</a> |
@ -0,0 +1,14 @@ |
||||
<div class="dropdown dropdown-relative dropdown-anchor-right dropdown-anchor-top" id="moreDropdown" role="menu"> |
||||
<ul class="dropdown-menu" ng-if="actionsAvailable"> |
||||
<li ng-repeat="(action, properties) in permittedActions" |
||||
class="{{action}}"> |
||||
<!-- The hrefs with empty URLs are necessary for IE10 to focus these links |
||||
properly. Thus, don't remove the hrefs or the empty URLs! --> |
||||
<a role="menuitem" href="" focus="{{ !$index }}" |
||||
ng-click="triggerMoreMenuAction(action, properties.link)" |
||||
ng-class="['icon-context'].concat(properties.css)" |
||||
ng-bind="I18n.t('js.button_' + action)"> |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,22 @@ |
||||
<div class="dropdown dropdown-relative" id="querySelectDropdown"> |
||||
<div class="search-query-wrapper"> |
||||
<input type="search" focus="" |
||||
ng-model="filterBy" |
||||
ng-change="filterModels(filterBy)" |
||||
ng-keydown="handleSelection($event)" |
||||
id="title-filter"><i id="magnifier" class="icon-search"></i> |
||||
</input> |
||||
</div> |
||||
|
||||
<div class="dropdown-scrollable"> |
||||
<div class="query-menu-container" ng-if="group.models" ng-repeat="group in filteredGroups"> |
||||
<ul class="query-menu"> |
||||
<div class="title-group-header">{{ group.name }}</div> |
||||
<li ng-repeat="model in group.models" |
||||
ng-class="{'selected': model.highlighted }"> |
||||
<a href="" ui-sref="work-packages.list({ query_id: model.id, query_props: undefined })" title="{{ model.label }}" ng-bind-html="model.labelHtml"></a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,50 @@ |
||||
<div class="dropdown dropdown-relative dropdown-anchor-right" id="settingsDropdown" role="menu"> |
||||
<!-- The hrefs with empty URLs are necessary for IE10 to focus these links |
||||
properly. Thus, don't remove the hrefs or the empty URLs! --> |
||||
<ul class="dropdown-menu"> |
||||
<li> |
||||
<a role="menuitem" focus="" href="" ng-click="showColumnsModal()"><i class="icon-action-menu icon-columns"></i>{{ I18n.t('js.toolbar.settings.columns') }}</a> |
||||
</li> |
||||
<li><a href="" ng-click="showSortingModal()"><i class="icon-action-menu icon-sort-by2"></i>{{ I18n.t('js.toolbar.settings.sort_by') }}</a></li> |
||||
<li><a href="" ng-click="showGroupingModal()"><i class="icon-action-menu icon-group-by2"></i>{{ I18n.t('js.toolbar.settings.group_by') }}</a></li> |
||||
<li> |
||||
<a role="menuitem" role="menuitem" href="" ng-click="toggleDisplaySums()"> |
||||
<i ng-if="query.displaySums" class="icon-action-menu icon-yes"></i><i ng-if="!query.displaySums" class="icon-action-menu no-icon"></i> |
||||
<accessible-element visible-text="I18n.t('js.toolbar.settings.display_sums')" |
||||
readable-text="displaySumsLabel"> |
||||
</accessible-element> |
||||
</a> |
||||
</li> |
||||
<li class="dropdown-divider"></li> |
||||
<li><a role="menuitem" href="" ng-click="saveQuery($event)" |
||||
inaccessible-by-tab="(!query.isDirty() && cannot('query', 'update')) || (query.isNew() && cannot('query', 'create'))" |
||||
ng-class="{'inactive': (!query.isDirty() && cannot('query', 'update')) || (query.isNew() && cannot('query', 'create'))}"> |
||||
<i class="icon-action-menu icon-save1"></i>{{ I18n.t('js.toolbar.settings.save') }}</a> |
||||
</li> |
||||
<li><a role="menuitem" href="" ng-click="showSaveAsModal($event)" |
||||
inaccessible-by-tab="query.isNew() || cannot('query', 'create')" |
||||
ng-class="{'inactive': query.isNew() || cannot('query', 'create')}"> |
||||
<i class="icon-action-menu icon-save1"></i>{{ I18n.t('js.toolbar.settings.save_as') }}</a> |
||||
</li> |
||||
<li><a role="menuitem" href="" ng-click="deleteQuery($event)" |
||||
inaccessible-by-tab="cannot('query', 'delete')" |
||||
ng-class="{'inactive': cannot('query', 'delete')}"> |
||||
<i class="icon-action-menu icon-delete"></i>{{ I18n.t('js.toolbar.settings.delete') }}</a> |
||||
</li> |
||||
<li><a role="menuitem" href="" ng-click="showExportModal($event)" |
||||
inaccessible-by-tab="cannot('work_package', 'export')" |
||||
ng-class="{'inactive': cannot('work_package', 'export')}"> |
||||
<i class="icon-action-menu icon-export"></i>{{ I18n.t('js.toolbar.settings.export') }}</a> |
||||
</li> |
||||
<li><a role="menuitem" href="" ng-click="showShareModal($event)" |
||||
inaccessible-by-tab="cannot('query', 'publicize') && cannot('query', 'star')" |
||||
ng-class="{'inactive': (cannot('query', 'publicize') && cannot('query', 'star'))}"> |
||||
<i class="icon-action-menu icon-publish"></i>{{ I18n.t('js.toolbar.settings.share') }}</a> |
||||
</li> |
||||
<li><a role="menuitem" href="" ng-click="showSettingsModal($event)" |
||||
inaccessible-by-tab="cannot('query', 'update')" |
||||
ng-class="{'inactive': cannot('query', 'update')}"> |
||||
<i class="icon-action-menu icon-settings"></i>{{ I18n.t('js.toolbar.settings.page_settings') }}</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,9 @@ |
||||
<div class="dropdown action-menu dropdown-relative dropdown-anchor-right" id="tasksDropdown" role="menu"> |
||||
<ul class="dropdown-menu"> |
||||
<li ng-repeat="type in availableTypes"> |
||||
<a role="menuitem" focus="{{ !$index }}" ng-href="{{ workPackageNewPath(type.id) }}"> |
||||
{{type.name}} |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,18 @@ |
||||
<div id="work-package-context-menu" class="action-menu dropdown" role="menu"> |
||||
<ul class="dropdown-menu"> |
||||
<li class="open" |
||||
feature-flag="detailsView"> |
||||
<a role="menuitem" focus="isDetailsViewLinkPresent()" ui-sref="work-packages.list.details.overview({workPackageId: row.object.id})"> |
||||
<i ng-class="['icon-action-menu', 'icon-table-detail-view']"></i> |
||||
<span ng-bind="I18n.t('js.button_open_details')"/> |
||||
</a> |
||||
</li> |
||||
<li ng-repeat="(action, link) in permittedActions" |
||||
class="{{action}}"> |
||||
<a role="menuitem" focus="$index == 0 && !isDetailsViewLinkPresent()" href="" ng-click="triggerContextMenuAction(action, link)"> |
||||
<i ng-class="['icon-action-menu', 'icon-' + action]"></i> |
||||
<span ng-bind="I18n.t('js.button_' + action)"/> |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -1,34 +0,0 @@ |
||||
<div id="work-package-context-menu" class="action-menu"> |
||||
<ul class="menu"> |
||||
<li class="open" |
||||
feature-flag="detailsView" |
||||
ui-sref="work-packages.list.details.overview({workPackageId: row.object.id})"> |
||||
<a focus="isDetailsViewLinkVisible()" href=""> |
||||
<i ng-class="['icon-action-menu', 'icon-table-detail-view']"></i> |
||||
<span ng-bind="I18n.t('js.button_open_details')"/> |
||||
</a> |
||||
</li> |
||||
<li ng-repeat="(action, link) in permittedActions" |
||||
ng-click="triggerContextMenuAction(action, link)" |
||||
class="{{action}}"> |
||||
<a focus="$index == 0 && !isDetailsViewLinkVisible()" href="" ng-click="deleteWorkPackages()"> |
||||
<i ng-class="['icon-action-menu', 'icon-' + action]"></i> |
||||
<span ng-bind="I18n.t('js.button_' + action)"/> |
||||
</a> |
||||
</li> |
||||
|
||||
<li class="folder priority" ng-hide="hideResourceActions"> |
||||
<a href="" class="context_item">TODO Priority</a> |
||||
<i class="icon-pulldown-arrow4 icon-submenu"></i> |
||||
<ul class="sub-menu"> |
||||
<li><a href="" class=" disabled">Immediate</a></li> |
||||
<li><a href="" class=" disabled">Urgent</a></li> |
||||
<li><a href="" class=" disabled">High</a></li> |
||||
<li><a href="" class=" disabled">Normal</a></li> |
||||
<li><a href="" class=" disabled">Low</a></li> |
||||
<li><a href="" class=" disabled">Pointless</a></li> |
||||
</ul> |
||||
<div class="submenu"></div> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -1,66 +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.
|
||||
//++
|
||||
|
||||
/*jshint expr: true*/ |
||||
|
||||
describe('dropdown Directive', function() { |
||||
var compile, element, rootScope, scope; |
||||
|
||||
beforeEach(angular.mock.module('openproject.uiComponents')); |
||||
beforeEach(module('openproject.templates')); |
||||
|
||||
beforeEach(inject(function($rootScope, $compile) { |
||||
var html; |
||||
html = '<div dropdown></div>'; |
||||
|
||||
element = angular.element(html); |
||||
rootScope = $rootScope; |
||||
scope = $rootScope.$new(); |
||||
scope.doNotShow = true; |
||||
|
||||
compile = function() { |
||||
$compile(element)(scope); |
||||
scope.$digest(); |
||||
}; |
||||
})); |
||||
|
||||
describe('element', function() { |
||||
beforeEach(function() { |
||||
compile(); |
||||
|
||||
}); |
||||
|
||||
it('should preserve its div', function() { |
||||
expect(element.prop('tagName')).to.equal('DIV'); |
||||
}); |
||||
|
||||
it('should be in a collapsed state', function() { |
||||
expect(element.is(":visible")).to.be.false; |
||||
}); |
||||
}); |
||||
}); |
@ -1,65 +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.
|
||||
//++
|
||||
|
||||
/*jshint expr: true*/ |
||||
|
||||
describe('withDropdown Directive', function() { |
||||
var compile, element, rootScope, scope; |
||||
|
||||
beforeEach(angular.mock.module('openproject.uiComponents')); |
||||
beforeEach(module('openproject.templates')); |
||||
|
||||
beforeEach(inject(function($rootScope, $compile) { |
||||
var html; |
||||
html = '<div with-dropdown dropdown-id="2"></div>'; |
||||
|
||||
element = angular.element(html); |
||||
rootScope = $rootScope; |
||||
scope = $rootScope.$new(); |
||||
scope.doNotShow = true; |
||||
|
||||
compile = function() { |
||||
$compile(element)(scope); |
||||
scope.$digest(); |
||||
}; |
||||
})); |
||||
|
||||
describe('element', function() { |
||||
beforeEach(function() { |
||||
compile(); |
||||
}); |
||||
|
||||
it('should preserve its div', function() { |
||||
expect(element.prop('tagName')).to.equal('DIV'); |
||||
}); |
||||
|
||||
it('should be in a collapsed state', function() { |
||||
expect(element.is(":visible")).to.be.false; |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,53 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'roar/decorator' |
||||
require 'roar/json/hal' |
||||
|
||||
module API |
||||
module Decorators |
||||
class Single < Roar::Decorator |
||||
include Roar::JSON::HAL |
||||
include Roar::Hypermedia |
||||
include API::V3::Utilities::PathHelper |
||||
|
||||
attr_reader :context |
||||
class_attribute :as_strategy |
||||
self.as_strategy = API::Utilities::CamelCasingStrategy.new |
||||
|
||||
def initialize(model, context = {}) |
||||
@context = context |
||||
|
||||
super(model) |
||||
end |
||||
|
||||
property :_type, exec_context: :decorator |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,43 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'roar/decorator' |
||||
require 'roar/json' |
||||
require 'roar/json/collection' |
||||
require 'roar/json/hal' |
||||
|
||||
module API |
||||
module V3 |
||||
module Projects |
||||
class ProjectCollectionRepresenter < ::API::Decorators::Collection |
||||
element_decorator ::API::V3::Projects::ProjectRepresenter |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,51 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module API |
||||
module V3 |
||||
module Versions |
||||
class ProjectsVersionsAPI < Grape::API |
||||
resources :versions do |
||||
before do |
||||
@versions = @project.shared_versions.all |
||||
|
||||
authorize_any [:view_work_packages, :manage_versions], @project |
||||
end |
||||
|
||||
get do |
||||
VersionCollectionRepresenter.new(@versions, |
||||
@versions.count, |
||||
api_v3_paths.versions(@project.identifier), |
||||
context: { current_user: current_user }) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,51 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module API |
||||
module V3 |
||||
module Versions |
||||
class VersionsProjectsAPI < Grape::API |
||||
resources :projects do |
||||
before do |
||||
@projects = @version.projects.visible(current_user).all |
||||
|
||||
# Authorization for accessing the version is done in the versions |
||||
# endpoint into which this endpoint is embedded. |
||||
end |
||||
|
||||
get do |
||||
Projects::ProjectCollectionRepresenter.new(@projects, |
||||
@projects.count, |
||||
api_v3_paths.versions_projects(@version.id)) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,81 @@ |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Login', type: :feature do |
||||
after do |
||||
User.current = nil |
||||
enable_test_auth_protection |
||||
end |
||||
|
||||
let(:user_password) { 'bob1!' * 4 } |
||||
let(:user) do |
||||
FactoryGirl.create(:user, |
||||
force_password_change: false, |
||||
first_login: false, |
||||
login: 'bob', |
||||
mail: 'bob@example.com', |
||||
firstname: 'Bo', |
||||
lastname: 'B', |
||||
password: user_password, |
||||
password_confirmation: user_password, |
||||
) |
||||
end |
||||
|
||||
let(:other_user) { FactoryGirl.create(:user) } |
||||
|
||||
it 'enforces the current user to be set correctly on each api request' do |
||||
# login to set the session |
||||
visit signin_path |
||||
within('#login-form') do |
||||
fill_in('username', with: user.login) |
||||
fill_in('password', with: user_password) |
||||
click_link_or_button I18n.t(:button_login) |
||||
end |
||||
|
||||
# simulate another user having used the process |
||||
# which would cause User.current to be set |
||||
User.current = other_user |
||||
|
||||
# disable a hack in the API's authenticate method |
||||
# which would cause authentication to not work |
||||
disable_test_auth_protection |
||||
|
||||
# taking /api/v3 as it does not run any authorization |
||||
visit '/api/v3' |
||||
expect(User.current).to eql(user) |
||||
end |
||||
|
||||
def disable_test_auth_protection |
||||
ENV['CAPYBARA_DISABLE_TEST_AUTH_PROTECTION'] = 'true' |
||||
end |
||||
|
||||
def enable_test_auth_protection |
||||
ENV.delete 'CAPYBARA_DISABLE_TEST_AUTH_PROTECTION' |
||||
end |
||||
end |
@ -0,0 +1,41 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe ::API::V3::Projects::ProjectCollectionRepresenter do |
||||
let(:self_link) { '/api/v3/versions/1/projects' } |
||||
let(:projects) { FactoryGirl.build_list(:project, 3) } |
||||
let(:representer) { described_class.new(projects, 42, self_link) } |
||||
|
||||
context 'generation' do |
||||
subject(:collection) { representer.to_json } |
||||
|
||||
it_behaves_like 'API V3 collection decorated', 42, 3, 'versions/1/projects', 'Project' |
||||
end |
||||
end |
@ -0,0 +1,80 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe "API v3 project's versions resource" do |
||||
include Rack::Test::Methods |
||||
|
||||
let(:current_user) do |
||||
user = FactoryGirl.create(:user, |
||||
member_in_project: project, |
||||
member_through_role: role) |
||||
|
||||
allow(User).to receive(:current).and_return user |
||||
|
||||
user |
||||
end |
||||
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) } |
||||
let(:project) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:other_project) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:versions) { FactoryGirl.create_list(:version, 4, project: project) } |
||||
let(:other_versions) { FactoryGirl.create_list(:version, 2) } |
||||
|
||||
subject(:response) { last_response } |
||||
|
||||
describe '#get (index)' do |
||||
let(:get_path) { "/api/v3/projects/#{project.id}/versions" } |
||||
|
||||
context 'logged in user' do |
||||
before do |
||||
current_user |
||||
|
||||
versions |
||||
other_versions |
||||
|
||||
get get_path |
||||
end |
||||
|
||||
it_behaves_like 'API V3 collection response', 4, 4, 'Version' |
||||
end |
||||
|
||||
context 'logged in user without permission' do |
||||
let(:role) { FactoryGirl.create(:role, permissions: []) } |
||||
|
||||
before do |
||||
current_user |
||||
|
||||
get get_path |
||||
end |
||||
|
||||
it_behaves_like 'unauthorized access' |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,97 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe "API v3 version's projects resource" do |
||||
include Rack::Test::Methods |
||||
|
||||
let(:current_user) do |
||||
user = FactoryGirl.create(:user, |
||||
member_in_project: project, |
||||
member_through_role: role) |
||||
|
||||
allow(User).to receive(:current).and_return user |
||||
|
||||
user |
||||
end |
||||
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) } |
||||
let(:role_without_permissions) { FactoryGirl.create(:role, permissions: []) } |
||||
let(:project) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:project2) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:project3) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:project4) { FactoryGirl.create(:project, is_public: false) } |
||||
let(:version) { FactoryGirl.create(:version, project: project, sharing: 'system') } |
||||
|
||||
subject(:response) { last_response } |
||||
|
||||
describe '#get (index)' do |
||||
let(:get_path) { "/api/v3/versions/#{version.id}/projects" } |
||||
|
||||
context 'logged in user with permissions' do |
||||
before do |
||||
current_user |
||||
|
||||
# this is to be included |
||||
FactoryGirl.create(:member, user: current_user, |
||||
project: project2, |
||||
roles: [role]) |
||||
# this is to be included as the user is a member of the project, the |
||||
# lack of permissions is irrelevant. |
||||
FactoryGirl.create(:member, user: current_user, |
||||
project: project3, |
||||
roles: [role_without_permissions]) |
||||
# project4 should NOT be included |
||||
project4 |
||||
|
||||
get get_path |
||||
end |
||||
|
||||
it_behaves_like 'API V3 collection response', 3, 3, 'Project' |
||||
|
||||
it 'includes only the projects which the user can see' do |
||||
id_in_response = JSON.parse(response.body)['_embedded']['elements'].map { |p| p['id'] } |
||||
|
||||
expect(id_in_response).to match_array [project.id, project2.id, project3.id] |
||||
end |
||||
end |
||||
|
||||
context 'logged in user without permissions' do |
||||
let(:role) { role_without_permissions } |
||||
|
||||
before do |
||||
current_user |
||||
|
||||
get get_path |
||||
end |
||||
|
||||
it_behaves_like 'unauthorized access' |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue