diff --git a/app/assets/javascripts/angular/helpers/components/api-helper.js b/app/assets/javascripts/angular/helpers/components/api-helper.js index 1faef73d3d..9c110c5e8e 100644 --- a/app/assets/javascripts/angular/helpers/components/api-helper.js +++ b/app/assets/javascripts/angular/helpers/components/api-helper.js @@ -38,11 +38,12 @@ angular.module('openproject.workPackages.helpers') }, getErrorMessage: function(error) { - var response = JSON.parse(error.responseText); if(error.status == 422) { - return response.description + response.errors.map(function(error) { - return " \"" + error.key + "\"" + " " + error.messages[0]; - }).join('. '); + var response = JSON.parse(error.responseText); + return response.errors.join('. '); + } + if(error.status == 500) { + return error.statusText; } }, diff --git a/app/assets/javascripts/angular/helpers/components/work-packages-helper.js b/app/assets/javascripts/angular/helpers/components/work-packages-helper.js index d9ab6be007..998a6669d1 100644 --- a/app/assets/javascripts/angular/helpers/components/work-packages-helper.js +++ b/app/assets/javascripts/angular/helpers/components/work-packages-helper.js @@ -28,7 +28,7 @@ angular.module('openproject.workPackages.helpers') -.factory('WorkPackagesHelper', ['dateFilter', 'currencyFilter', 'CustomFieldHelper', function(dateFilter, currencyFilter, CustomFieldHelper) { +.factory('WorkPackagesHelper', ['TimezoneService', 'currencyFilter', 'CustomFieldHelper', function(TimezoneService, currencyFilter, CustomFieldHelper) { var WorkPackagesHelper = { getRowObjectContent: function(object, option) { var content; @@ -97,15 +97,20 @@ angular.module('openproject.workPackages.helpers') formatValue: function(value, dataType) { switch(dataType) { case 'datetime': - return value ? dateFilter(WorkPackagesHelper.parseDateTime(value), 'medium') : ''; + var dateTime; + if (value) { + dateTime = TimezoneService.formattedDate(value) + " " + TimezoneService.formattedTime(value); + } + return dateTime || ''; case 'date': - return value ? dateFilter(WorkPackagesHelper.parseDateTime(value), 'mediumDate') : ''; + return value ? TimezoneService.formattedDate(value) : ''; case 'currency': return currencyFilter(value, 'EUR '); default: return value; } }, + formatWorkPackageProperty: function(value, propertyName) { var mappings = { dueDate: 'date', @@ -139,7 +144,7 @@ angular.module('openproject.workPackages.helpers') for (var x = 0; x < children.length; x++) { var child = children[x]; - result.push(child.fetch()); + result.push(child); } } @@ -163,15 +168,6 @@ angular.module('openproject.workPackages.helpers') return result; }, - getRelatedWorkPackage: function(workPackage, relation) { - var self = workPackage.links.self.href; - if (relation.links.relatedTo.href == self) { - return relation.links.relatedFrom.fetch(); - } else { - return relation.links.relatedTo.fetch(); - } - }, - //Note: The following methods are display helpers and so don't really belong here but are shared between // directives so it's probably the best place for them just now. getState: function(workPackage) { diff --git a/app/assets/javascripts/angular/openproject-app.js b/app/assets/javascripts/angular/openproject-app.js index a243ee101a..35b1a1675f 100644 --- a/app/assets/javascripts/angular/openproject-app.js +++ b/app/assets/javascripts/angular/openproject-app.js @@ -39,6 +39,7 @@ angular.module('openproject.models', [ 'openproject.workPackages.config', 'openproject.services' ]); +angular.module('openproject.viewModels', ['openproject.services']); // timelines angular.module('openproject.timelines', [ @@ -82,6 +83,7 @@ angular.module('openproject.workPackages.filters', [ angular.module('openproject.workPackages.config', []); angular.module('openproject.workPackages.controllers', [ 'openproject.models', + 'openproject.viewModels', 'openproject.workPackages.helpers', 'openproject.services', 'openproject.workPackages.config', diff --git a/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js b/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js index bb1a263a86..e47a71098d 100644 --- a/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js +++ b/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js @@ -38,6 +38,15 @@ angular.module('openproject.workPackages.controllers') precedes: "Relation::Precedes", follows: "Relation::Follows" }) +.constant('RELATION_IDENTIFIERS', { + relatedTo: "relates", + duplicates: "duplicates", + duplicated: "duplicated", + blocks: "blocks", + blocked: "blocked", + precedes: "precedes", + follows: "follows" +}) .controller('WorkPackageDetailsController', [ '$scope', @@ -46,10 +55,14 @@ angular.module('openproject.workPackages.controllers') 'I18n', 'VISIBLE_LATEST', 'RELATION_TYPES', + 'RELATION_IDENTIFIERS', '$q', 'WorkPackagesHelper', 'ConfigurationService', - function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, $q, WorkPackagesHelper, ConfigurationService) { + 'CommonRelationsHandler', + 'ChildrenRelationsHandler', + 'ParentRelationsHandler', + function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, RELATION_IDENTIFIERS, $q, WorkPackagesHelper, ConfigurationService, CommonRelationsHandler, ChildrenRelationsHandler, ParentRelationsHandler) { $scope.$on('$stateChangeSuccess', function(event, toState){ latestTab.registerState(toState.name); }); @@ -101,18 +114,23 @@ angular.module('openproject.workPackages.controllers') // relations $q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parents) { - $scope.wpParent = parents.length ? parents[0] : null; + var relationsHandler = new ParentRelationsHandler(workPackage, parents); + $scope.wpParent = relationsHandler; }); $q.all(WorkPackagesHelper.getChildren(workPackage)).then(function(children) { - $scope.wpChildren = children; + var relationsHandler = new ChildrenRelationsHandler(workPackage, children); + $scope.wpChildren = relationsHandler; }); for (var key in RELATION_TYPES) { if (RELATION_TYPES.hasOwnProperty(key)) { (function(key) { $q.all(WorkPackagesHelper.getRelationsOfType(workPackage, RELATION_TYPES[key])).then(function(relations) { - $scope[key] = relations; + var relationsHandler = new CommonRelationsHandler(workPackage, + relations, + RELATION_IDENTIFIERS[key]); + $scope[key] = relationsHandler; }); })(key); } diff --git a/app/assets/javascripts/angular/work_packages/tabs/work-package-parent-directive.js b/app/assets/javascripts/angular/work_packages/tabs/add-work-package-child-directive.js similarity index 56% rename from app/assets/javascripts/angular/work_packages/tabs/work-package-parent-directive.js rename to app/assets/javascripts/angular/work_packages/tabs/add-work-package-child-directive.js index 8ab009a643..6134f18813 100644 --- a/app/assets/javascripts/angular/work_packages/tabs/work-package-parent-directive.js +++ b/app/assets/javascripts/angular/work_packages/tabs/add-work-package-child-directive.js @@ -29,44 +29,9 @@ // TODO move to UI components angular.module('openproject.workPackages.tabs') -.directive('workPackageParent', [ - 'I18n', - 'PathHelper', - 'WorkPackageService', - 'WorkPackagesHelper', - '$timeout', - function(I18n, PathHelper, WorkPackageService, WorkPackagesHelper, $timeout) { +.directive('addWorkPackageChild', [function() { return { restrict: 'E', - replace: true, - scope: { - title: '@', - workPackage: '=', - parent: '=', - btnTitle: '@buttonTitle', - btnIcon: '@buttonIcon' - }, - templateUrl: '/templates/work_packages/tabs/_work_package_parent.html', - link: function(scope, element, attrs) { - scope.I18n = I18n; - scope.getState = WorkPackagesHelper.getState; - scope.getFullIdentifier = WorkPackagesHelper.getFullIdentifier; - - var setExpandState = function() { - scope.expand = !!scope.parent; - }; - - scope.$watch('parent', function() { - setExpandState(); - }); - - scope.$watch('expand', function(newVal, oldVal) { - scope.stateClass = WorkPackagesHelper.collapseStateIcon(!newVal); - }); - - scope.toggleExpand = function() { - scope.expand = !scope.expand; - }; - } + templateUrl: '/templates/work_packages/tabs/_add_work_package_child.html', }; }]); diff --git a/app/assets/javascripts/angular/work_packages/tabs/add-work-package-relation-directive.js b/app/assets/javascripts/angular/work_packages/tabs/add-work-package-relation-directive.js new file mode 100644 index 0000000000..3f35154d6b --- /dev/null +++ b/app/assets/javascripts/angular/work_packages/tabs/add-work-package-relation-directive.js @@ -0,0 +1,37 @@ +//-- 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. +//++ + +// TODO move to UI components +angular.module('openproject.workPackages.tabs') + +.directive('addWorkPackageRelation', [function() { + return { + restrict: 'E', + templateUrl: '/templates/work_packages/tabs/_add_work_package_relation.html', + }; +}]); diff --git a/app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js b/app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js index d6a4d777b0..17c8b16c2d 100644 --- a/app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js +++ b/app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js @@ -44,7 +44,7 @@ angular.module('openproject.workPackages.tabs') scope.userPath = PathHelper.staticUserPath; scope.canDeleteRelation = !!scope.relation.links.remove; - WorkPackagesHelper.getRelatedWorkPackage(scope.workPackage, scope.relation).then(function(relatedWorkPackage){ + scope.handler.getRelatedWorkPackage(scope.workPackage, scope.relation).then(function(relatedWorkPackage){ scope.relatedWorkPackage = relatedWorkPackage; scope.fullIdentifier = WorkPackagesHelper.getFullIdentifier(relatedWorkPackage); scope.state = WorkPackagesHelper.getState(relatedWorkPackage); diff --git a/app/assets/javascripts/angular/work_packages/tabs/work-package-children-directive.js b/app/assets/javascripts/angular/work_packages/tabs/work-package-children-directive.js deleted file mode 100644 index 42d3bb4eea..0000000000 --- a/app/assets/javascripts/angular/work_packages/tabs/work-package-children-directive.js +++ /dev/null @@ -1,84 +0,0 @@ -//-- 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. -//++ - -// TODO move to UI components -angular.module('openproject.workPackages.tabs') - -.directive('workPackageChildren', [ - 'I18n', - 'PathHelper', - 'WorkPackageService', - 'WorkPackagesHelper', - '$timeout', - function(I18n, PathHelper, WorkPackageService, WorkPackagesHelper, $timeout) { - return { - restrict: 'E', - replace: true, - scope: { - title: '@', - workPackage: '=', - children: '=', - btnTitle: '@buttonTitle', - btnIcon: '@buttonIcon' - }, - templateUrl: '/templates/work_packages/tabs/_work_package_children.html', - link: function(scope, element, attrs) { - scope.I18n = I18n; - scope.userPath = PathHelper.staticUserPath; - scope.workPackagePath = PathHelper.staticWorkPackagePath; - scope.getState = WorkPackagesHelper.getState; - scope.getFullIdentifier = WorkPackagesHelper.getFullIdentifier; - - var setExpandState = function() { - scope.expand = scope.children && scope.children.length > 0; - }; - - scope.$watch('children', function() { - setExpandState(); - scope.childrenCount = scope.children.length || 0; - }); - - scope.$watch('expand', function(newVal, oldVal) { - scope.stateClass = WorkPackagesHelper.collapseStateIcon(!newVal); - }); - - scope.toggleExpand = function() { - scope.expand = !scope.expand; - }; - - scope.addChild = function() { - // Temporarily go to old create view with parent_id set to currently viewed work package - window.location = PathHelper.staticWorkPackageNewWithParentPath(scope.workPackage.props.projectId, scope.workPackage.props.id); - } - - scope.deleteChild = function() { - //TODO: Requires API endpoint for update work package - } - } - }; -}]); diff --git a/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js b/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js index 5031774417..bf0f2b1c88 100644 --- a/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js +++ b/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js @@ -43,9 +43,7 @@ angular.module('openproject.workPackages.tabs') replace: true, scope: { title: '@', - workPackage: '=', - relations: '=', - relationIdentifier: '=', + handler: '=', btnTitle: '@buttonTitle', btnIcon: '@buttonIcon', isSingletonRelation: '@singletonRelation' @@ -53,16 +51,23 @@ angular.module('openproject.workPackages.tabs') templateUrl: '/templates/work_packages/tabs/_work_package_relations.html', link: function(scope, element, attrs) { scope.I18n = I18n; - scope.canAddRelation = !!scope.workPackage.links.addRelation; var setExpandState = function() { - scope.expand = scope.relations && scope.relations.length > 0; + scope.expand = !scope.handler.isEmpty(); }; - scope.$watch('relations', function() { - setExpandState(); - if(scope.relations) { - scope.relationsCount = scope.relations.length || 0; + scope.$watch('handler', function() { + if (scope.handler) { + scope.workPackage = scope.handler.workPackage; + + setExpandState(); + scope.relationsCount = scope.handler.getCount(); + + if (scope.handler.applyCustomExtensions) { + $timeout(function() { + scope.handler.applyCustomExtensions(); + }); + } } }); @@ -73,36 +78,6 @@ angular.module('openproject.workPackages.tabs') scope.toggleExpand = function() { scope.expand = !scope.expand; }; - - scope.addRelation = function() { - var inputElement = angular.element('#relation_to_id-' + scope.relationIdentifier); - var toId = inputElement.val(); - WorkPackageService.addWorkPackageRelation(scope.workPackage, toId, scope.relationIdentifier).then(function(relation) { - inputElement.val(''); - scope.$emit('workPackageRefreshRequired', ''); - }, function(error) { - ApiHelper.handleError(scope, error); - }); - }; - - // Massive hack alert - Using old prototype autocomplete /////////// - if(scope.canAddRelation) { - $timeout(function(){ - var url = PathHelper.workPackageAutoCompletePath(scope.workPackage.props.projectId, scope.workPackage.props.id); - new Ajax.Autocompleter('relation_to_id-' + scope.relationIdentifier, - 'related_issue_candidates-' + scope.relationIdentifier, - url, - { minChars: 1, - frequency: 0.5, - paramName: 'q', - updateElement: function(value) { - document.getElementById('relation_to_id-' + scope.relationIdentifier).value = value.id; - }, - parameters: 'scope=all' - }); - }); - } - //////////////////////////////////////////////////////////////////// } }; }]); diff --git a/app/assets/javascripts/angular/work_packages/view_models/relations-handler.js b/app/assets/javascripts/angular/work_packages/view_models/relations-handler.js new file mode 100644 index 0000000000..3d8256000d --- /dev/null +++ b/app/assets/javascripts/angular/work_packages/view_models/relations-handler.js @@ -0,0 +1,144 @@ +//-- 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.viewModels') + +.factory('CommonRelationsHandler', [ + '$timeout', + 'WorkPackageService', + 'ApiHelper', + function($timeout, WorkPackageService, ApiHelper) { + function CommonRelationsHandler(workPackage, + relations, + relationsId) { + this.workPackage = workPackage; + this.relations = relations; + this.relationsId = relationsId; + + this.type = "relation"; + this.isSingletonRelation = false; + } + + CommonRelationsHandler.prototype = { + isEmpty: function() { + return !this.relations || this.relations.length === 0; + }, + + getCount: function() { + return (this.relations) ? this.relations.length : 0; + }, + + canAddRelation: function() { + return !!this.workPackage.links.addRelation; + }, + + addRelation: function(scope) { + var inputElement = angular.element('#relation_to_id-' + this.relationsId); + var toId = inputElement.val(); + WorkPackageService.addWorkPackageRelation(this.workPackage, toId, this.relationsId).then(function(relation) { + inputElement.val(''); + scope.$emit('workPackageRefreshRequired', ''); + }, function(error) { + ApiHelper.handleError(scope, error); + }); + }, + + applyCustomExtensions: function() { + // Massive hack alert - Using old prototype autocomplete /////////// + if(this.canAddRelation) { + var workPackage = this.workPackage; + var relationsId = this.relationsId; + + $timeout(function() { + var url = PathHelper.workPackageAutoCompletePath(workPackage.props.projectId, workPackage.props.id); + new Ajax.Autocompleter('relation_to_id-' + relationsId, + 'related_issue_candidates-' + relationsId, + url, + { minChars: 1, + frequency: 0.5, + paramName: 'q', + updateElement: function(value) { + document.getElementById('relation_to_id-' + relationsId).value = value.id; + }, + parameters: 'scope=all' + }); + }); + } + //////////////////////////////////////////////////////////////////// + }, + + getRelatedWorkPackage: function(workPackage, relation) { + var self = workPackage.links.self.href; + + if (relation.links.relatedTo.href == self) { + return relation.links.relatedFrom.fetch(); + } else { + return relation.links.relatedTo.fetch(); + } + } + }; + + return CommonRelationsHandler; +}]) + +.factory('ChildrenRelationsHandler', ['PathHelper', + 'CommonRelationsHandler', + function(PathHelper, + CommonRelationsHandler) { + function ChildrenRelationsHandler(workPackage, children) { + var handler = new CommonRelationsHandler(workPackage, children, undefined); + + handler.type = "child"; + handler.canAddRelation = function() { return true }; + handler.addRelation = function() { + window.location = PathHelper.staticWorkPackageNewWithParentPath(this.workPackage.props.projectId, this.workPackage.props.id); + }; + handler.applyCustomExtensions = undefined; + handler.getRelatedWorkPackage = function(workPackage, relation) { return relation.fetch() }; + + return handler; + } + + return ChildrenRelationsHandler; +}]) + +.factory('ParentRelationsHandler', ['ChildrenRelationsHandler', + function(ChildrenRelationsHandler) { + function ParentRelationsHandler(workPackage, parents) { + var handler = new ChildrenRelationsHandler(workPackage, parents, undefined); + + handler.type = "parent"; + handler.canAddRelation = function() { return false }; + handler.addRelation = undefined; + handler.isSingletonRelation = true; + + return handler; + } + + return ParentRelationsHandler; +}]); diff --git a/app/assets/stylesheets/content/_components_add_comments_default.sass b/app/assets/stylesheets/content/_components_add_comments_default.sass index a2da372a1d..026e760e2b 100644 --- a/app/assets/stylesheets/content/_components_add_comments_default.sass +++ b/app/assets/stylesheets/content/_components_add_comments_default.sass @@ -35,10 +35,11 @@ font-size: $global_font_size width: 100% box-sizing: border-box + &:required + box-shadow: none &:hover border: 1px solid #aaaaaa &:focus - border: 1px solid #aaaaaa - box-shadow: 1px 1px 1px #dddddd inset + box-shadow: 1px 1px 1px #dddddd inset .add-comment-text resize: none diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 0b67dbbfee..15fd78ac7d 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -40,14 +40,12 @@ class AccountController < ApplicationController # Login request and validation def login - if User.current.logged? + user = User.current + + if user.logged? redirect_to home_url elsif Concerns::OmniauthLogin.direct_login? - ps = {}.tap do |p| - p[:origin] = params[:back_url] if params[:back_url] - end - - redirect_to Concerns::OmniauthLogin.direct_login_provider_url(ps) + direct_login(user) elsif request.post? authenticate_user end @@ -56,7 +54,12 @@ class AccountController < ApplicationController # Log out current user and redirect to welcome page def logout logout_user - redirect_to home_url + if Setting.login_required? && Concerns::OmniauthLogin.direct_login? + flash.now[:notice] = I18n.t :notice_logged_out + render :exit, locals: { instructions: :after_logout } + else + redirect_to home_url + end end # Enable user to choose a new password @@ -195,6 +198,23 @@ class AccountController < ApplicationController private + def direct_login(user) + if flash.empty? + ps = {}.tap do |p| + p[:origin] = params[:back_url] if params[:back_url] + end + + redirect_to Concerns::OmniauthLogin.direct_login_provider_url(ps) + else + if Setting.login_required? + error = user.active? || flash[:error] + instructions = error ? :after_error : :after_registration + + render :exit, locals: { instructions: instructions } + end + end + end + def logout_user if User.current.logged? cookies.delete OpenProject::Configuration['autologin_cookie_name'] diff --git a/app/controllers/auth_sources_controller.rb b/app/controllers/auth_sources_controller.rb index 761ca1ed31..80864936e0 100644 --- a/app/controllers/auth_sources_controller.rb +++ b/app/controllers/auth_sources_controller.rb @@ -32,6 +32,7 @@ class AuthSourcesController < ApplicationController layout 'admin' before_filter :require_admin + before_filter :block_if_password_login_disabled def index @auth_sources = AuthSource.page(params[:page]) @@ -99,4 +100,8 @@ class AuthSourcesController < ApplicationController def default_breadcrumb l(:label_auth_source_plural) end + + def block_if_password_login_disabled + render_404 if OpenProject::Configuration.disable_password_login? + end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 719f4e7511..41d5dd2e99 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -40,12 +40,16 @@ class SettingsController < ApplicationController def edit @notifiables = Redmine::Notifiable.all if request.post? && params[:settings] && params[:settings].is_a?(Hash) - settings = (params[:settings] || {}).dup.symbolize_keys + settings = (params[:settings] || {}).dup.symbolize_keys.tap do |set| + set.except! *password_settings if OpenProject::Configuration.disable_password_login? + end + settings.each do |name, value| # remove blank values in array settings value.delete_if {|v| v.blank? } if value.is_a?(Array) Setting[name] = value end + flash[:notice] = l(:notice_successful_update) redirect_to :action => 'edit', :tab => params[:tab] else @@ -74,4 +78,15 @@ class SettingsController < ApplicationController def default_breadcrumb l(:label_settings) end + + private + + ## + # Returns all password-login related setting keys. + def password_settings + [ + :password_min_length, :password_active_rules, :password_min_adhered_rules, + :password_days_valid, :password_count_former_banned, :lost_password + ] + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 85a52cce24..fba2a9721d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -44,6 +44,9 @@ class UsersController < ApplicationController before_filter :authorize_for_user, :only => [:destroy] before_filter :check_if_deletion_allowed, :only => [:deletion_info, :destroy] + + before_filter :block_if_password_login_disabled, :only => [:new, :create] + accept_key_auth :index, :show, :create, :update, :destroy include SortHelper @@ -352,4 +355,8 @@ class UsersController < ApplicationController 'admin' end end + + def block_if_password_login_disabled + render_404 if OpenProject::Configuration.disable_password_login? + end end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 7364ba50bb..aeb2243f90 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -75,9 +75,9 @@ class CustomField < ActiveRecord::Base validate :validate_name - validates :min_length, numericality: { only_integer: true, greater_than_or_equal: 0 } - validates :max_length, numericality: { only_integer: true, greater_than_or_equal: 0 } - validates :min_length, numericality: { less_than_or_equal_to: :max_length, message: :greater_than_or_equal_to_max_length}, unless: Proc.new { |cf| cf.max_length.blank?} + validates :min_length, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_length, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :min_length, numericality: { less_than_or_equal_to: :max_length, message: :smaller_than_or_equal_to_max_length}, unless: Proc.new { |cf| cf.max_length.blank?} diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index ccb3de5f8c..4b6bdecb71 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -94,7 +94,7 @@ class CustomValue < ActiveRecord::Base end def validate_length_of_value - if value.present? + if value.present? && custom_field.min_length.present? && custom_field.max_length.present? errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length end diff --git a/app/views/account/exit.html.erb b/app/views/account/exit.html.erb new file mode 100644 index 0000000000..b025b88825 --- /dev/null +++ b/app/views/account/exit.html.erb @@ -0,0 +1,49 @@ +<%#-- 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. + +++#%> + +<% # LOCALS + fail ArgumentError, 'missing instructions' unless defined? instructions +%> + +<% disable_accessibility_css! %> + +<% breadcrumb_paths(l(:label_login)) %> +<%= call_hook :view_account_login_top %> + +<% + signin_link = link_to I18n.t('label_here'), signin_path + instruction_text = I18n.t "instructions_#{instructions}", signin: signin_link +%> + +
+

<%= I18n.t(:label_login) %>

+
+

<%= instruction_text.html_safe %>

+
+<%= call_hook :view_account_login_bottom %> diff --git a/app/views/api/experimental/work_packages/index.api.rabl b/app/views/api/experimental/work_packages/index.api.rabl index d773bac849..a7712e941f 100644 --- a/app/views/api/experimental/work_packages/index.api.rabl +++ b/app/views/api/experimental/work_packages/index.api.rabl @@ -61,6 +61,14 @@ child @work_packages => :work_packages do wp.parent_id end + node :updated_at do |wp| + wp.updated_at.utc.iso8601 + end + + node :created_at do |wp| + wp.created_at.utc.iso8601 + end + node :_actions do |wp| @can.actions(wp) end diff --git a/app/views/settings/_authentication.html.erb b/app/views/settings/_authentication.html.erb index 8a5771705c..ccbff45e24 100644 --- a/app/views/settings/_authentication.html.erb +++ b/app/views/settings/_authentication.html.erb @@ -42,22 +42,38 @@ See doc/COPYRIGHT.rdoc for more details.
<%= I18n.t(:passwords, :scope => [:settings]) %> -

<%= setting_text_field :password_min_length, :size => 6 %>

-

<%= setting_multiselect :password_active_rules, - OpenProject::Passwords::Evaluator.known_rules.map do |r| - [l("label_password_rule_#{r}"), r] - end %>

-

<%= setting_text_field :password_min_adhered_rules, :size => 6 %>

-

<%= setting_text_field :password_days_valid, :size => 6 %>

-

<%= setting_text_field :password_count_former_banned, :size => 6 %>

-

<%= setting_check_box :lost_password, :label => :label_password_lost %>

+ <% if !OpenProject::Configuration.disable_password_login? %> +

<%= setting_text_field :password_min_length, :size => 6 %>

+

<%= setting_multiselect :password_active_rules, + OpenProject::Passwords::Evaluator.known_rules.map do |r| + [l("label_password_rule_#{r}"), r] + end %>

+

<%= setting_text_field :password_min_adhered_rules, :size => 6 %>

+

<%= setting_text_field :password_days_valid, :size => 6 %>

+

<%= setting_text_field :password_count_former_banned, :size => 6 %>

+

<%= setting_check_box :lost_password, :label => :label_password_lost %>

+ <% else %> +

+ + <%= + url = 'https://github.com/opf/openproject/blob/dev/doc/CONFIGURATION.md#disable-password-login' + + explanation = I18n.t :note_password_login_disabled, + :configuration => "#{I18n.t('label_configuration')}" + + explanation.html_safe + %> +

+ <% end %>
-
- <%= I18n.t(:brute_force_prevention, :scope => [:settings]) %> -

<%= setting_text_field :brute_force_block_after_failed_logins %>

-

<%= setting_text_field :brute_force_block_minutes %>

-
+ <% unless OpenProject::Configuration.disable_password_login? %> +
+ <%= I18n.t(:brute_force_prevention, :scope => [:settings]) %> +

<%= setting_text_field :brute_force_block_after_failed_logins %>

+

<%= setting_text_field :brute_force_block_minutes %>

+
+ <% end %>
<%= I18n.t(:session, :scope => [:settings]) %> @@ -82,9 +98,11 @@ See doc/COPYRIGHT.rdoc for more details.
-
- <%= link_to l(:label_ldap_authentication), {:controller => '/ldap_auth_sources', :action => 'index'}, :class => 'icon icon-server-key' %> -
+ <% unless OpenProject::Configuration.disable_password_login? %> +
+ <%= link_to l(:label_ldap_authentication), {:controller => '/ldap_auth_sources', :action => 'index'}, :class => 'icon icon-server-key' %> +
+ <% end %> <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/settings/_notifications.html.erb b/app/views/settings/_notifications.html.erb index 618c7d1ab2..d75d2c4230 100644 --- a/app/views/settings/_notifications.html.erb +++ b/app/views/settings/_notifications.html.erb @@ -55,10 +55,10 @@ See doc/COPYRIGHT.rdoc for more details. <% Setting.available_languages.each do |lang| %> <% end %> diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 2d1cb768cb..be360284f0 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -61,39 +61,56 @@ See doc/COPYRIGHT.rdoc for more details.

<%= I18n.t('user.authentication_settings_disabled_due_to_external_authentication') %> <% else %> - <% unless @auth_sources.empty? %> + <% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>

<%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %>

<% end %> + <% if !OpenProject::Configuration.disable_password_login? %> + <% + pw_style = + if @user.change_password_allowed? + '' + else + ' style="display: none;"' + end + %> +
> + <% assign_random_password_enabled = params[:user] && + params[:user][:assign_random_password] %> +

+ + <%= check_box_tag("user[assign_random_password]", + "1", + assign_random_password_enabled) %> +

+

+ <%= f.password_field :password, + :required => true, + :size => 25, + :disabled => assign_random_password_enabled %>
+ <%= password_complexity_requirements %> +

+

+ <%= f.password_field :password_confirmation, + :required => true, + :size => 25, + :disabled => assign_random_password_enabled %> +

+

+ <%= f.check_box :force_password_change, + :disabled => assign_random_password_enabled %> +

+
+ <% else %> +
+

+ + <%= I18n.t 'user.no_login' %> +

+
+ <% end %> <% end %> -
- <% assign_random_password_enabled = params[:user] && - params[:user][:assign_random_password] %> -

- - <%= check_box_tag("user[assign_random_password]", - "1", - assign_random_password_enabled) %> -

-

- <%= f.password_field :password, - :required => true, - :size => 25, - :disabled => assign_random_password_enabled %>
- <%= password_complexity_requirements %> -

-

- <%= f.password_field :password_confirmation, - :required => true, - :size => 25, - :disabled => assign_random_password_enabled %> -

-

- <%= f.check_box :force_password_change, - :disabled => assign_random_password_enabled %> -

-
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 679239d486..4fb295798e 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -27,8 +27,10 @@ See doc/COPYRIGHT.rdoc for more details. ++#%> -<% content_for :action_menu_specific do %> - <%= link_to l(:label_user_new), {:action => 'new'}, :class => 'icon icon-add' %> +<% unless OpenProject::Configuration.disable_password_login? %> + <% content_for :action_menu_specific do %> + <%= link_to l(:label_user_new), {:action => 'new'}, :class => 'icon icon-add' %> + <% end %> <% end %>

<%=l(:label_user_plural)%>

diff --git a/bower.json b/bower.json index 206440046d..18d6287a9d 100644 --- a/bower.json +++ b/bower.json @@ -6,8 +6,7 @@ "jquery-ujs": "1.0.0", "jquery-ui": "~1.10.4", "select2": "3.3.2", - "jquery.atwho": "finnlabs/At.js#0025862f7600c8dddec98caab13a76b11cfaba18", - "Caret.js": "ichord/Caret.js#72495fcb194a0beec14de25956a7c615ede630b2", + "jquery.atwho": "0.5.0", "openproject-ui_components": "opf/openproject-ui_components#with-bower", "angular": "~1.2.14", "angular-animate": "~1.2.14", diff --git a/config/locales/de.yml b/config/locales/de.yml index 0abfe812d8..ec31eb9c16 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -196,7 +196,7 @@ de: exclusion: "ist nicht verfügbar" greater_than: "muss größer als %{count} sein" greater_than_or_equal_to: "muss größer oder gleich %{count} sein" - greater_than_or_equal_to_max_length: "muss größer oder gleich der maximalen Länge sein" + smaller_than_or_equal_to_max_length: "muss kleiner als oder gleich der maximalen Länge sein" greater_than_start_date: "muss größer als Anfangsdatum sein" inclusion: "ist kein gültiger Wert" invalid: "ist nicht gültig" @@ -580,6 +580,10 @@ de: gui_validation_error: "1 Fehler" gui_validation_error_plural: "%{count} Fehler" + instructions_after_registration: "Sie können sich %{signin} einloggen sobald Ihr Konto aktiviert wurde." + instructions_after_logout: "Sie können sich %{signin} wieder einloggen." + instructions_after_error: "Sie können noch mal versuchen sich einzuloggen, indem Sie %{signin} klicken. Wenn der Fehler weiterhin auftritt, wenden Sie sich bitte an den Ihren Admin." + label_account: "Konto" label_activity: "Aktivität" label_add_another_file: "Eine weitere Datei hinzufügen" @@ -640,6 +644,7 @@ de: label_close_versions: "Vollständige Versionen schließen" label_closed_work_packages: "geschlossen" label_collapse: "Zuklappen" + label_configuration: Konfiguration label_comment_add: "Kommentar hinzufügen" label_comment_added: "Kommentar hinzugefügt" label_comment_delete: "Kommentar löschen" @@ -713,6 +718,7 @@ de: label_group_new: "Neue Gruppe" label_group_plural: "Gruppen" label_help: "Hilfe" + label_here: hier label_hide: "Verbergen" label_history: "Historie" label_home: "Hauptseite" @@ -1091,6 +1097,7 @@ de: notice_unable_delete_time_entry: "Der Zeiterfassungseintrag konnte nicht gelöscht werden." notice_unable_delete_version: "Die Version konnte nicht gelöscht werden." notice_automatic_set_of_standard_type: "Der Standard-Typ wurde automatisch gesetzt." + notice_logged_out: "Sie wurden ausgeloggt." error_types_in_use_by_work_packages: "Die folgenden Typen werden von Arbeitspaketen referenziert: %{types}" # Default format for numbers @@ -1235,7 +1242,7 @@ de: setting_diff_max_lines_displayed: "Maximale Anzahl anzuzeigender Diff-Zeilen" setting_display_subprojects_work_packages: "Arbeitspakete von Unterprojekten im Hauptprojekt anzeigen" setting_emails_footer: "E-Mail-Fußzeile" - setting_emails_header: "Emailkopf" + setting_emails_header: "E-Mail-Kopfzeile" setting_enabled_scm: "Aktivierte Versionskontrollsysteme" setting_feeds_enabled: "Feeds aktiviert" setting_feeds_limit: "Max. Anzahl Einträge pro Atom-Feed" @@ -1600,6 +1607,8 @@ de: status_user_and_brute_force: "%{user} und %{brute_force}" unlock: "Entsperren" unlock_and_reset_failed_logins: "Entsperren und fehlgeschlagene Logins zurücksetzen" + no_login: "Dieser Nutzer wird per Passwort authentifiziert. Er kann sich jedoch nicht einloggen, da Login per Passwort deaktiviert ist." + password_change_unsupported: Passwortänderung wird nicht unterstützt authorization_rejected: "Sie dürfen sich nicht einloggen." version_status_closed: "abgeschlossen" @@ -1607,6 +1616,10 @@ de: version_status_open: "offen" warning_attachments_not_saved: "%{count} Datei(en) konnten nicht gespeichert werden." + note: Hinweis + note_password_login_disabled: "Der Passwort-Login wurde per %{configuration} deaktiviert." + warning: Warnung + menu_item: "Menüpunkt" menu_item_setting: "Sichtbarkeit" wiki_menu_item_for: "Menüpunkt für die Wikiseite \"%{title}\"" diff --git a/config/locales/en.yml b/config/locales/en.yml index b7e3118e00..f8a81555ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -196,7 +196,7 @@ en: exclusion: "is reserved" greater_than: "must be greater than %{count}" greater_than_or_equal_to: "must be greater than or equal to %{count}" - greater_than_or_equal_to_max_length: "must be greater than or equal to maximum length" + smaller_than_or_equal_to_max_length: "must be smaller than or equal to maximum length" greater_than_start_date: "must be greater than start date" inclusion: "is not included in the list" invalid: "is invalid" @@ -577,6 +577,10 @@ en: gui_validation_error: "1 error" gui_validation_error_plural: "%{count} errors" + instructions_after_registration: "You can sign in as soon as your account has been activated by clicking %{signin}." + instructions_after_logout: "You can sign in again by clicking %{signin}." + instructions_after_error: "You can try to sign in again by clicking %{signin}. If the error persists, ask your admin for help." + label_account: "Account" label_activity: "Activity" label_add_another_file: "Add another file" @@ -637,6 +641,7 @@ en: label_close_versions: "Close completed versions" label_closed_work_packages: "closed" label_collapse: "Collapse" + label_configuration: configuration label_comment_add: "Add a comment" label_comment_added: "Comment added" label_comment_delete: "Delete comments" @@ -710,6 +715,7 @@ en: label_group_new: "New group" label_group_plural: "Groups" label_help: "Help" + label_here: here label_hide: "Hide" label_history: "History" label_home: "Home" @@ -1089,6 +1095,7 @@ en: notice_unable_delete_time_entry: "Unable to delete time log entry." notice_unable_delete_version: "Unable to delete version." notice_automatic_set_of_standard_type: "Set standard type automatically." + notice_logged_out: "You have been logged out." error_types_in_use_by_work_packages: "The following types are still referenced by work packages: %{types}" # Default format for numbers @@ -1590,13 +1597,17 @@ en: status_user_and_brute_force: "%{user} and %{brute_force}" unlock: "Unlock" unlock_and_reset_failed_logins: "Unlock and reset failed logins" + no_login: "This user authenticates through login by password. Since it is disabled, they cannot log in." + password_change_unsupported: Change of password is not supported. authorization_rejected: "You are not allowed to sign in." - version_status_closed: "closed" version_status_locked: "locked" version_status_open: "open" + note: Note + note_password_login_disabled: "Password login has been disabled by %{configuration}." + warning: Warning warning_attachments_not_saved: "%{count} file(s) could not be saved." menu_item: "Menu item" diff --git a/features/timelines/timeline_modal_views.feature b/features/timelines/timeline_modal_views.feature index a556f11d34..1f9b705815 100644 --- a/features/timelines/timeline_modal_views.feature +++ b/features/timelines/timeline_modal_views.feature @@ -31,26 +31,7 @@ Feature: Timeline View Tests I want edit planning elements via a modal window Background: - Given there are the following types: - | Name | Is Milestone | In aggregation | - | Phase | false | true | - | Milestone | true | true | - # Hack to ensure that the project is persisted before opening the - # timeline. Otherwise we regularly have - # Couldn't find Project with identifier=ecookbook - # errors. - # As far as I could figure it out, the: - # I am already logged in as "manager" - # will raise the bug when the project is not already persisted and he - # tries to reopen the page he had visited last in the test before. - When I go to the home page - - Given there are the following project types: - | Name | - | Standard Project | - | Extraordinary Project | - - And there is 1 user with: + Given there is 1 user with: | login | manager | And there is a role "manager" @@ -59,15 +40,11 @@ Feature: Timeline View Tests | edit_timelines | | view_work_packages | - And there is a project named "ecookbook" of type "Standard Project" + And there is a project named "ecookbook" And I am working in project "ecookbook" And there is a timeline "Testline" for project "ecookbook" - And the following types are enabled for projects of type "Standard Project" - | Phase | - | Milestone | - And the project uses the following modules: | timelines | @@ -77,9 +54,6 @@ Feature: Timeline View Tests | Start date | Due date | description | responsible | Subject | | 2012-01-01 | 2012-01-31 | #2 http://google.de | manager | January | | 2012-02-01 | 2012-02-24 | Avocado Rincon | manager | February | - | 2012-03-01 | 2012-03-30 | Hass | manager | March | - | 2012-04-01 | 2012-04-30 | Avocado Choquette | manager | April | - | 2012-04-01 | 2012-04-30 | Relish | manager | Test2 | And I am already logged in as "manager" @@ -112,8 +86,14 @@ Feature: Timeline View Tests And I click on the first anchor matching "Update" in the modal And I fill in "work_package_notes" with "A new comment" in the modal And I click on the div "ui-dialog-closer" - Then I confirm the JS confirm dialog - And I should not see a modal window + And I confirm the JS confirm dialog + Then I should not see a modal window + # Hack to ensure that this scenario does not interfere with the next one. As + # closing the modal will trigger the timeline to be reloaded we have to + # ensure, that this request is finished before starting the next scenario. + # Otherwise the data required to successfully finish the request (esp. the + # project) might already be removed for the next senario. + Given I wait for the AJAX requests to finish @javascript Scenario: closing the modal window after adding a related work package should not display a warning message @@ -131,4 +111,10 @@ Feature: Timeline View Tests And I press "Add" in the modal And I wait for the AJAX requests to finish And I click on the div "ui-dialog-closer" - Then I should not see a modal window + Then I should not see a modal window + # Hack to ensure that this scenario does not interfere with the next one. As + # closing the modal will trigger the timeline to be reloaded we have to + # ensure, that this request is finished before starting the next scenario. + # Otherwise the data required to successfully finish the request (esp. the + # project) might already be removed for the next senario. + Given I wait for the AJAX requests to finish diff --git a/karma/tests/controllers/details-tab-overview-controller-test.js b/karma/tests/controllers/details-tab-overview-controller-test.js index 7dc0846a3c..774b99f928 100644 --- a/karma/tests/controllers/details-tab-overview-controller-test.js +++ b/karma/tests/controllers/details-tab-overview-controller-test.js @@ -241,7 +241,7 @@ describe('DetailsTabOverviewController', function() { }); it('renders the due date and a placeholder for the start date as date property', function() { - expect(fetchPresentPropertiesWithName('date')[0].value).to.equal(placeholder + ' - Jul 10, 2014'); + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal(placeholder + ' - 07/10/2014'); }); }); @@ -262,7 +262,7 @@ describe('DetailsTabOverviewController', function() { }); it('renders the start date and a placeholder for the due date as date property', function() { - expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - ' + placeholder); + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('07/09/2014 - ' + placeholder); }); }); @@ -275,7 +275,7 @@ describe('DetailsTabOverviewController', function() { }); it('combines them and renders them as date property', function() { - expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - Jul 10, 2014'); + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('07/09/2014 - 07/10/2014'); }); }); }); diff --git a/karma/tests/controllers/work-package-details-controller-test.js b/karma/tests/controllers/work-package-details-controller-test.js index ef287365a8..8a89cc6667 100644 --- a/karma/tests/controllers/work-package-details-controller-test.js +++ b/karma/tests/controllers/work-package-details-controller-test.js @@ -87,7 +87,14 @@ describe('WorkPackageDetailsController', function() { return workPackage; } - beforeEach(module('openproject.api', 'openproject.services', 'openproject.workPackages.controllers')); + beforeEach(module('openproject.api', 'openproject.services', 'openproject.workPackages.controllers', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $controller, $timeout) { var workPackageId = 99; @@ -152,7 +159,7 @@ describe('WorkPackageDetailsController', function() { }); it('Relation::Relates', function() { - expect(scope.relatedTo.length).to.eq(1); + expect(scope.relatedTo).to.be.ok; }); }); }); diff --git a/karma/tests/controllers/work-packages-controller-test.js b/karma/tests/controllers/work-packages-controller-test.js index 34faf5362e..45170cdbbd 100644 --- a/karma/tests/controllers/work-packages-controller-test.js +++ b/karma/tests/controllers/work-packages-controller-test.js @@ -31,7 +31,14 @@ describe('WorkPackagesController', function() { var scope, win, testParams, buildController; - beforeEach(module('openproject.workPackages.controllers', 'openproject.api')); + beforeEach(module('openproject.workPackages.controllers', 'openproject.api', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $controller, $timeout) { scope = $rootScope.$new(); })); diff --git a/karma/tests/controllers/work-packages-list-controller-test.js b/karma/tests/controllers/work-packages-list-controller-test.js index 91f5e309db..adf05d7516 100644 --- a/karma/tests/controllers/work-packages-list-controller-test.js +++ b/karma/tests/controllers/work-packages-list-controller-test.js @@ -33,7 +33,14 @@ describe('WorkPackagesListController', function() { testProjectService, testWorkPackageService, testQueryService, testPaginationService; var buildController; - beforeEach(module('openproject.api', 'openproject.workPackages.controllers', 'openproject.workPackages.services', 'ng-context-menu', 'btford.modal')); + beforeEach(module('openproject.api', 'openproject.workPackages.controllers', 'openproject.workPackages.services', 'ng-context-menu', 'btford.modal', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $controller, $timeout) { scope = $rootScope.$new(); win = { diff --git a/karma/tests/directives/components/date-time-directive-test.js b/karma/tests/directives/components/date-time-directive-test.js index b283d4ebaf..e3df01ac22 100644 --- a/karma/tests/directives/components/date-time-directive-test.js +++ b/karma/tests/directives/components/date-time-directive-test.js @@ -27,7 +27,7 @@ //++ describe('date time Directives', function() { - var I18n, compile, element, scope, timezoneService, configurationService; + var I18n, compile, element, scope, configurationService; var formattedDate = function() { var formattedDateElement = element[0]; diff --git a/karma/tests/directives/components/toggled-multiselect-directive-test.js b/karma/tests/directives/components/toggled-multiselect-directive-test.js index fd779b7110..cb6ab894df 100644 --- a/karma/tests/directives/components/toggled-multiselect-directive-test.js +++ b/karma/tests/directives/components/toggled-multiselect-directive-test.js @@ -29,8 +29,16 @@ describe('toggledMultiselect Directive', function() { var compile, element, rootScope, scope; - beforeEach(angular.mock.module('openproject.uiComponents', 'openproject.workPackages.helpers')); - beforeEach(module('templates')); + beforeEach(angular.mock.module('openproject.uiComponents', + 'openproject.workPackages.helpers', + 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $compile) { var html; diff --git a/karma/tests/directives/work_packages/options-dropdown-directive-test.js b/karma/tests/directives/work_packages/options-dropdown-directive-test.js index 94414e210d..e8704b3c3a 100644 --- a/karma/tests/directives/work_packages/options-dropdown-directive-test.js +++ b/karma/tests/directives/work_packages/options-dropdown-directive-test.js @@ -30,7 +30,17 @@ describe('optionsDropdown Directive', function() { var compile, element, rootScope, scope; beforeEach(angular.mock.module('openproject.workPackages.directives')); - beforeEach(module('openproject.models', 'openproject.workPackages.controllers', 'openproject.api')); + beforeEach(module('openproject.models', + 'openproject.workPackages.controllers', + 'openproject.api', + 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(module('templates', function($provide) { var state = { go: function() { return false; } }; diff --git a/karma/tests/directives/work_packages/work-package-column-directive-test.js b/karma/tests/directives/work_packages/work-package-column-directive-test.js index 893ed545d7..506ae6e8c8 100644 --- a/karma/tests/directives/work_packages/work-package-column-directive-test.js +++ b/karma/tests/directives/work_packages/work-package-column-directive-test.js @@ -30,8 +30,14 @@ describe('workPackageColumn Directive', function() { var compile, element, rootScope, scope; beforeEach(angular.mock.module('openproject.workPackages.directives')); - beforeEach(module('templates', 'openproject.api')); + beforeEach(module('templates', 'openproject.api', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $compile) { var html; html = ''; diff --git a/karma/tests/directives/work_packages/work-package-group-sums-directive-test.js b/karma/tests/directives/work_packages/work-package-group-sums-directive-test.js index 69dbf185e3..6d6d49174c 100644 --- a/karma/tests/directives/work_packages/work-package-group-sums-directive-test.js +++ b/karma/tests/directives/work_packages/work-package-group-sums-directive-test.js @@ -29,8 +29,14 @@ describe('workPackageGroupSums Directive', function() { var compile, element, rootScope, scope; - beforeEach(angular.mock.module('openproject.workPackages.directives')); - beforeEach(module('openproject.api', 'templates')); + beforeEach(angular.mock.module('openproject.workPackages.directives', 'openproject.services')); + beforeEach(module('openproject.api', 'templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $compile) { var html; diff --git a/karma/tests/directives/work_packages/work-package-relations-directive-test.js b/karma/tests/directives/work_packages/work-package-relations-directive-test.js index caeea67929..05a74fb8e4 100644 --- a/karma/tests/directives/work_packages/work-package-relations-directive-test.js +++ b/karma/tests/directives/work_packages/work-package-relations-directive-test.js @@ -29,11 +29,25 @@ describe('Work Package Relations Directive', function() { var I18n, PathHelper, compile, element, scope; - beforeEach(angular.mock.module('openproject.workPackages.tabs', 'openproject.api', 'openproject.helpers', 'ngSanitize')); + beforeEach(angular.mock.module('openproject.workPackages.tabs', + 'openproject.api', + 'openproject.helpers', + 'openproject.services', + 'ngSanitize')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); })); - beforeEach(inject(function($rootScope, $compile, _I18n_, _PathHelper_, _WorkPackagesHelper_) { + beforeEach(inject(function($rootScope, + $compile, + _I18n_, + _PathHelper_, + _WorkPackagesHelper_) { scope = $rootScope.$new(); compile = function(html) { @@ -44,6 +58,7 @@ describe('Work Package Relations Directive', function() { I18n = _I18n_; PathHelper = _PathHelper_; WorkPackagesHelper = _WorkPackagesHelper_; + Ajax = { Autocompleter: angular.noop } @@ -59,14 +74,43 @@ describe('Work Package Relations Directive', function() { I18n.t.restore(); }); - var multiElementHtml = "" - var singleElementHtml = "" - + var html = "" var workPackage1; var workPackage2; var workPackage3; + var relationsHandlerEmpty; + var relationsHandlerSingle; + var relationsHandlerMulti; + + var createRelationsHandlerStub = function($timeout, count) { + var relationsHandler = new Object(); + + relationsHandler.workPackage = sinon.stub(); + relationsHandler.relationsId = sinon.stub(); + relationsHandler.isEmpty = sinon.stub(); + relationsHandler.getCount = sinon.stub(); + relationsHandler.canAddRelation = sinon.stub(); + relationsHandler.addRelation = sinon.stub(); + relationsHandler.applyCustomExtensions = sinon.stub(); + + relationsHandler.workPackage.returns(workPackage1); + relationsHandler.relationsId.returns('related'); + relationsHandler.isEmpty.returns(count === 0); + relationsHandler.getCount.returns(count); + + relationsHandler.type = "relation"; + + relationsHandler.getRelatedWorkPackage = function() { + return $timeout(function() { + return workPackage1; + }, 10); + }; + + return relationsHandler; + }; + beforeEach(inject(function($q, $timeout) { workPackage1 = { props: { @@ -145,12 +189,14 @@ describe('Work Package Relations Directive', function() { } }; - WorkPackagesHelper.getRelatedWorkPackage = function() { - return $timeout(function() { - return workPackage1; - }, 10); - }; + relationsHandlerEmpty = createRelationsHandlerStub($timeout, 0); + relationsHandlerEmpty.relations = []; + relationsHandlerSingle = createRelationsHandlerStub($timeout, 1); + relationsHandlerSingle.relations = [relation1]; + + relationsHandlerMulti = createRelationsHandlerStub($timeout, 2); + relationsHandlerMulti.relations = [relation1, relation2]; })); var shouldBehaveLikeRelationsDirective = function() { @@ -218,11 +264,10 @@ describe('Work Package Relations Directive', function() { }; var shouldBehaveLikeSingleRelationDirective = function() { - it('should not have an elements count', function() { + it('should NOT have an elements count', function() { var title = angular.element(element.find('h3')); - expect(title.text()).not.to.include('('); - expect(title.text()).not.to.include(')'); + expect(title.text()).to.not.include('(' + scope.relations.getCount() + ')'); }); }; @@ -230,13 +275,13 @@ describe('Work Package Relations Directive', function() { it('should have an elements count', function() { var title = angular.element(element.find('h3')); - expect(title.text()).to.include('(' + scope.relations.length + ')'); + expect(title.text()).to.include('(' + scope.relations.getCount() + ')'); }); }; var shouldBehaveLikeHasAddRelationDialog = function() { it('should have add relation button and id input', function() { - var addRelationDiv = angular.element(element.find('.workpackages .add-relation')); + var addRelationDiv = angular.element(element.find('.content .add-relation')); expect(addRelationDiv.length).not.to.eq(0); var button = addRelationDiv.find('button'); @@ -254,45 +299,46 @@ describe('Work Package Relations Directive', function() { }; describe('no element markup', function() { - describe('single element behavior', function() { - beforeEach(function() { - scope.workPackage = workPackage1; - compile(singleElementHtml); - }); + beforeEach(function() { + scope.relations = relationsHandlerMulti; - shouldBehaveLikeSingleRelationDirective(); + scope.relations.canAddRelation.returns(true); + scope.relations.isEmpty.returns(true); - shouldBehaveLikeCollapsedRelationsDirective(); + compile(html); }); - describe('multi element behavior', function() { - beforeEach(function() { - scope.workPackage = workPackage1; - scope.relations = []; - - compile(multiElementHtml); - }); + shouldBehaveLikeMultiRelationDirective(); - shouldBehaveLikeMultiRelationDirective(); + shouldBehaveLikeCollapsedRelationsDirective(); - shouldBehaveLikeCollapsedRelationsDirective(); - }); + shouldBehaveLikeHasAddRelationDialog(); }); describe('single element markup', function() { - describe('readonly', function(){ + describe('header', function() { beforeEach(inject(function($timeout) { - scope.workPackage = workPackage2; - scope.relations = [relation1]; + scope.relations = relationsHandlerSingle; + scope.relations.isSingletonRelation = true; - compile(singleElementHtml); + compile(html); $timeout.flush(); })); - shouldBehaveLikeRelationsDirective(); - shouldBehaveLikeSingleRelationDirective(); + }); + + describe('readonly', function() { + beforeEach(inject(function($timeout) { + scope.relations = relationsHandlerSingle; + + compile(html); + + $timeout.flush(); + })); + + shouldBehaveLikeRelationsDirective(); shouldBehaveLikeExpandedRelationsDirective(); @@ -303,41 +349,40 @@ describe('Work Package Relations Directive', function() { shouldBehaveLikeReadOnlyRelationDialog(); }); - describe('can add and remove relations', function(){ + describe('can add and remove relations', function() { beforeEach(inject(function($timeout) { - scope.workPackage = workPackage1; - scope.relations = [relation2]; + scope.relations = relationsHandlerSingle; + scope.relations.relations = [relation2]; + scope.relations.canAddRelation.returns(true); - compile(singleElementHtml); + compile(html); $timeout.flush(); + })); - shouldBehaveLikeRelationsDirective(); - - shouldBehaveLikeSingleRelationDirective(); + shouldBehaveLikeRelationsDirective(); - shouldBehaveLikeExpandedRelationsDirective(); + shouldBehaveLikeExpandedRelationsDirective(); - shouldBehaveLikeHasTableHeader(); + shouldBehaveLikeHasTableHeader(); - shouldBehaveLikeHasTableContent(1, false); + shouldBehaveLikeHasTableContent(1, false); - shouldBehaveLikeHasAddRelationDialog(); - })); + shouldBehaveLikeHasAddRelationDialog(); }); - describe('table row of closed work package', function(){ + describe('table row of closed work package', function() { beforeEach(inject(function($timeout) { - scope.workPackage = workPackage1; - scope.relations = [relation2]; + scope.relations = relationsHandlerSingle; + scope.relations.relations = [relation2]; - WorkPackagesHelper.getRelatedWorkPackage = function() { + scope.relations.getRelatedWorkPackage = function() { return $timeout(function() { return workPackage3; }, 10); }; - compile(singleElementHtml); + compile(html); $timeout.flush(); })); diff --git a/karma/tests/directives/work_packages/work-package-total-sums-directive-test.js b/karma/tests/directives/work_packages/work-package-total-sums-directive-test.js index 497bc3a65b..db6f623364 100644 --- a/karma/tests/directives/work_packages/work-package-total-sums-directive-test.js +++ b/karma/tests/directives/work_packages/work-package-total-sums-directive-test.js @@ -29,8 +29,14 @@ describe('workPackageTotalSums Directive', function() { var compile, element, rootScope, scope; - beforeEach(angular.mock.module('openproject.workPackages.directives')); - beforeEach(module('openproject.api', 'templates')); + beforeEach(angular.mock.module('openproject.workPackages.directives', 'openproject.services')); + beforeEach(module('openproject.api', 'templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function($rootScope, $compile) { var html; diff --git a/karma/tests/helpers/components/work-packages-helper-test.js b/karma/tests/helpers/components/work-packages-helper-test.js index 0a4c3e5685..57d11837a9 100644 --- a/karma/tests/helpers/components/work-packages-helper-test.js +++ b/karma/tests/helpers/components/work-packages-helper-test.js @@ -31,7 +31,16 @@ describe('Work packages helper', function() { var WorkPackagesHelper; - beforeEach(module('openproject.helpers')); + beforeEach(module('openproject.helpers', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub(); + configurationService.dateFormatPresent = sinon.stub(); + configurationService.timeFormatPresent = sinon.stub(); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function(_WorkPackagesHelper_) { WorkPackagesHelper = _WorkPackagesHelper_; })); @@ -128,9 +137,13 @@ describe('Work packages helper', function() { expect(formatValue(null, 'date')).to.equal(""); }); + var TIME = '2014-01-01T00:00:00'; + var EXPECTED_DATE = '01/01/2014'; + var EXPECTED_DATETIME = '01/01/2014 12:00 AM'; + it('should display parsed dates and datetimes', function(){ - expect(formatValue("01/01/2014", 'date')).to.equal("Jan 1, 2014"); - expect(formatValue("01/01/2014 08:19 AM", 'datetime')).to.equal("Jan 1, 2014 12:00:00 AM"); + expect(formatValue(TIME, 'date')).to.equal(EXPECTED_DATE); + expect(formatValue(TIME, 'datetime')).to.equal(EXPECTED_DATETIME); }) }); diff --git a/karma/tests/helpers/work-package-context-menu-helper-test.js b/karma/tests/helpers/work-package-context-menu-helper-test.js index 0de8b24473..1f946c8125 100644 --- a/karma/tests/helpers/work-package-context-menu-helper-test.js +++ b/karma/tests/helpers/work-package-context-menu-helper-test.js @@ -31,7 +31,15 @@ describe('WorkPackageContextMenuHelper', function() { var WorkPackageContextMenuHelper; - beforeEach(module('openproject.workPackages.helpers', 'openproject.models', 'openproject.api')); + beforeEach(module('openproject.workPackages.helpers', 'openproject.models', 'openproject.api', 'openproject.services')); + + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function(_WorkPackageContextMenuHelper_) { WorkPackageContextMenuHelper = _WorkPackageContextMenuHelper_; diff --git a/karma/tests/helpers/work-package-table-helper-test.js b/karma/tests/helpers/work-package-table-helper-test.js index 4e58e7b209..1f8641072e 100644 --- a/karma/tests/helpers/work-package-table-helper-test.js +++ b/karma/tests/helpers/work-package-table-helper-test.js @@ -31,7 +31,14 @@ describe('WorkPackagesTableHelper', function() { var WorkPackagesTableHelper; - beforeEach(module('openproject.workPackages.helpers')); + beforeEach(module('openproject.workPackages.helpers', 'openproject.services')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function(_WorkPackagesTableHelper_) { WorkPackagesTableHelper = _WorkPackagesTableHelper_; })); diff --git a/karma/tests/layout/query-menu-item-factory-test.js b/karma/tests/layout/query-menu-item-factory-test.js index 9e85a8b906..16e13704c8 100644 --- a/karma/tests/layout/query-menu-item-factory-test.js +++ b/karma/tests/layout/query-menu-item-factory-test.js @@ -33,7 +33,17 @@ describe('queryMenuItemFactory', function() { queryMenuItemFactory, stateParams = {}; beforeEach(angular.mock.module('openproject.layout')); - beforeEach(module('templates', 'openproject.services', 'openproject.models', 'openproject.api')); + beforeEach(module('templates', + 'openproject.services', + 'openproject.models', + 'openproject.api', + function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(module('templates', function($provide) { $provide.value('$stateParams', stateParams); diff --git a/karma/tests/services/query-service-test.js b/karma/tests/services/query-service-test.js index 8db69738f4..fbe03c7382 100644 --- a/karma/tests/services/query-service-test.js +++ b/karma/tests/services/query-service-test.js @@ -32,7 +32,18 @@ describe('QueryService', function() { var QueryService, query, queryData; - beforeEach(module('openproject.services', 'openproject.models', 'openproject.api')); + beforeEach(module('openproject.services', + 'openproject.models', + 'openproject.api', + 'openproject.services')); + + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function(_QueryService_){ QueryService = _QueryService_; diff --git a/karma/tests/services/work-package-service-test.js b/karma/tests/services/work-package-service-test.js index 08cd1cc018..4e249e91ec 100644 --- a/karma/tests/services/work-package-service-test.js +++ b/karma/tests/services/work-package-service-test.js @@ -31,7 +31,15 @@ describe('WorkPackageService', function() { var WorkPackageService; - beforeEach(module('openproject.api', 'openproject.services', 'openproject.models')); + beforeEach(module('openproject.api','openproject.services', 'openproject.models')); + + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); beforeEach(inject(function(_WorkPackageService_, _HALAPIResource_){ WorkPackageService = _WorkPackageService_; diff --git a/karma/tests/work_packages/column-context-menu-test.js b/karma/tests/work_packages/column-context-menu-test.js index 39ae91c77e..57f379f614 100644 --- a/karma/tests/work_packages/column-context-menu-test.js +++ b/karma/tests/work_packages/column-context-menu-test.js @@ -36,8 +36,17 @@ describe('columnContextMenu', function() { 'openproject.workPackages.controllers', 'openproject.models', 'openproject.api', + 'openproject.services', 'templates')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); + beforeEach(function() { var html = '
'; container = angular.element(html); diff --git a/karma/tests/work_packages/work-package-context-menu-test.js b/karma/tests/work_packages/work-package-context-menu-test.js index 4e68486eb0..a1b224544f 100644 --- a/karma/tests/work_packages/work-package-context-menu-test.js +++ b/karma/tests/work_packages/work-package-context-menu-test.js @@ -36,8 +36,17 @@ describe('workPackageContextMenu', function() { 'openproject.api', 'openproject.workPackages', 'openproject.models', + 'openproject.services', 'templates')); + beforeEach(module('templates', function($provide) { + configurationService = new Object(); + + configurationService.isTimezoneSet = sinon.stub().returns(false); + + $provide.constant('ConfigurationService', configurationService); + })); + beforeEach(function() { var html = '
'; container = angular.element(html); diff --git a/lib/api/errors/validation.rb b/lib/api/errors/validation.rb index e8a38b6f3b..b175ee1801 100644 --- a/lib/api/errors/validation.rb +++ b/lib/api/errors/validation.rb @@ -41,7 +41,7 @@ module API end def errors - @obj.errors.messages.map{ |m| { key: m[0], messages: m[1] }} + @obj.errors.full_messages end def to_json diff --git a/lib/api/v3/relations/relations_api.rb b/lib/api/v3/relations/relations_api.rb index 3c40a7f015..c708b0057d 100644 --- a/lib/api/v3/relations/relations_api.rb +++ b/lib/api/v3/relations/relations_api.rb @@ -25,7 +25,7 @@ module API relation.save! representer.to_json else - raise ::API::Errors::Validation.new(relation) + fail Errors::Validation.new(relation) end end diff --git a/lib/api/v3/work_packages/work_package_model.rb b/lib/api/v3/work_packages/work_package_model.rb index a0738faa61..39d3e83b64 100644 --- a/lib/api/v3/work_packages/work_package_model.rb +++ b/lib/api/v3/work_packages/work_package_model.rb @@ -150,7 +150,9 @@ module API end def relations - work_package.relations.map{ |relation| RelationModel.new(relation) } + relations = work_package.relations + visible_relations = relations.find_all { |relation| relation.other_work_package(work_package).visible? } + visible_relations.map{ |relation| RelationModel.new(relation) } end def is_closed diff --git a/lib/api/v3/work_packages/work_package_representer.rb b/lib/api/v3/work_packages/work_package_representer.rb index fc4d0e7160..f91d02e1ae 100644 --- a/lib/api/v3/work_packages/work_package_representer.rb +++ b/lib/api/v3/work_packages/work_package_representer.rb @@ -129,13 +129,13 @@ module API { href: "#{root_url}/api/v3/work_packages/#{represented.work_package.parent.id}", title: represented.work_package.parent.subject - } unless represented.work_package.parent.nil? + } unless represented.work_package.parent.nil? || !represented.work_package.parent.visible? end links :children do - represented.work_package.children.map do |child| + visible_children.map do |child| { href: "#{root_url}/api/v3/work_packages/#{child.id}", title: child.subject } - end unless represented.work_package.children.empty? + end unless visible_children.empty? end property :id, getter: -> (*) { work_package.id }, render_nil: true @@ -192,6 +192,10 @@ module API def current_user_allowed_to(permission, work_package) @current_user && @current_user.allowed_to?(permission, work_package.project) end + + def visible_children + @visible_children ||= represented.work_package.children.find_all { |child| child.visible? } + end end end end diff --git a/lib/redmine.rb b/lib/redmine.rb index 44b27ac814..90569be7d9 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -270,8 +270,10 @@ Redmine::MenuManager.map :admin_menu do |menu| :html => {:class => 'custom_fields icon2 icon-status' } menu.push :enumerations, {:controller => '/enumerations'}, :html => {:class => "icon2 icon-status"} menu.push :settings, {:controller => '/settings'}, :html => {:class => "icon2 icon-settings2"} - menu.push :ldap_authentication, {:controller => '/ldap_auth_sources', :action => 'index'}, - :html => {:class => 'server_authentication icon2 icon-status'} + menu.push :ldap_authentication, + {:controller => '/ldap_auth_sources', :action => 'index'}, + :html => {:class => 'server_authentication icon2 icon-status'}, + :if => proc { !OpenProject::Configuration.disable_password_login? } menu.push :plugins, {:controller => '/admin', :action => 'plugins'}, :last => true, :html => {:class => "icon2 icon-status"} menu.push :info, {:controller => '/admin', :action => 'info'}, :caption => :label_information_plural, :last => true, :html => {:class => "icon2 icon-info"} menu.push :colors, diff --git a/public/templates/work_packages.list.details.html b/public/templates/work_packages.list.details.html index d1c45beff3..c0c10702a8 100644 --- a/public/templates/work_packages.list.details.html +++ b/public/templates/work_packages.list.details.html @@ -35,9 +35,9 @@ ng-if="toggleWatchLink" /> #{{ workPackage.props.id }} - . + . - . + .
diff --git a/public/templates/work_packages/tabs/_add_work_package_child.html b/public/templates/work_packages/tabs/_add_work_package_child.html new file mode 100644 index 0000000000..f56026985e --- /dev/null +++ b/public/templates/work_packages/tabs/_add_work_package_child.html @@ -0,0 +1,5 @@ + diff --git a/public/templates/work_packages/tabs/_add_work_package_relation.html b/public/templates/work_packages/tabs/_add_work_package_relation.html new file mode 100644 index 0000000000..dfe5097ab3 --- /dev/null +++ b/public/templates/work_packages/tabs/_add_work_package_relation.html @@ -0,0 +1,12 @@ + + + diff --git a/public/templates/work_packages/tabs/_work_package_children.html b/public/templates/work_packages/tabs/_work_package_children.html deleted file mode 100644 index b7728b4cf8..0000000000 --- a/public/templates/work_packages/tabs/_work_package_children.html +++ /dev/null @@ -1,48 +0,0 @@ -
-

- - {{ title }} - ({{ childrenCount }}) - -

-
-
-
- - - - - - - - - - - - - - - - - -
{{ I18n.t('js.work_packages.properties.subject') }}{{ I18n.t('js.work_packages.properties.status') }}{{ I18n.t('js.work_packages.properties.assignee') }}
- - {{ getFullIdentifier(workPackage) }} - - {{ workPackage.props.status }} - - {{ workPackage.embedded.assignee.props.name }} - -
-
-
- No child work packages -
-
- -
-
diff --git a/public/templates/work_packages/tabs/_work_package_parent.html b/public/templates/work_packages/tabs/_work_package_parent.html deleted file mode 100644 index 265af80bc2..0000000000 --- a/public/templates/work_packages/tabs/_work_package_parent.html +++ /dev/null @@ -1,42 +0,0 @@ -
-

- - {{ title }} - -

-
-
-
- - - - - - - - - - - - - - - - - -
{{ I18n.t('js.work_packages.properties.subject') }}{{ I18n.t('js.work_packages.properties.status') }}{{ I18n.t('js.work_packages.properties.assignee') }}
- - {{ getFullIdentifier(parent) }} - - {{ parent.props.status }} - - {{ parent.embedded.assignee.props.name }} - -
-
-
- No parent work packages -
-
-
-
diff --git a/public/templates/work_packages/tabs/_work_package_relations.html b/public/templates/work_packages/tabs/_work_package_relations.html index e3a000dafe..88ee509e04 100644 --- a/public/templates/work_packages/tabs/_work_package_relations.html +++ b/public/templates/work_packages/tabs/_work_package_relations.html @@ -1,13 +1,13 @@
-

+

{{ title }} - ({{ relationsCount }}) + ({{ handler.getCount() }})

-
+
@@ -19,15 +19,15 @@ + ng-repeat="relation in handler.relations"> - + @@ -41,23 +41,13 @@
- + {{ fullIdentifier }} {{ relatedWorkPackage.props.status }}{{ relatedWorkPackage.props.status }} - + {{ relatedWorkPackage.embedded.assignee.props.name }}
-
+
No relation exists
-
- - - +
+ +
diff --git a/public/templates/work_packages/tabs/relations.html b/public/templates/work_packages/tabs/relations.html index f883c1e590..10c2d90ad5 100644 --- a/public/templates/work_packages/tabs/relations.html +++ b/public/templates/work_packages/tabs/relations.html @@ -1,73 +1,55 @@
- - + button-icon=""> + - - + + diff --git a/public/templates/work_packages/tabs/watchers.html b/public/templates/work_packages/tabs/watchers.html index b664595e0a..fd4ab03114 100644 --- a/public/templates/work_packages/tabs/watchers.html +++ b/public/templates/work_packages/tabs/watchers.html @@ -3,13 +3,11 @@
  • - + - + class="detail-panel-watchers-delete-watcher-icon"> +
diff --git a/spec/controllers/auth_sources_controller_spec.rb b/spec/controllers/auth_sources_controller_spec.rb index ee86a236dc..27d70fd446 100644 --- a/spec/controllers/auth_sources_controller_spec.rb +++ b/spec/controllers/auth_sources_controller_spec.rb @@ -32,6 +32,8 @@ describe AuthSourcesController do let(:current_user) { FactoryGirl.create(:admin) } before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + allow(User).to receive(:current).and_return current_user end @@ -119,4 +121,46 @@ describe AuthSourcesController do end end end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + end + + it 'cannot find index' do + get :index + + expect(response.status).to eq 404 + end + + it 'cannot find new' do + get :new + + expect(response.status).to eq 404 + end + + it 'cannot find create' do + post :create, auth_source: { name: 'Test' } + + expect(response.status).to eq 404 + end + + it 'cannot find edit' do + get :edit, id: 42 + + expect(response.status).to eq 404 + end + + it 'cannot find update' do + post :update, id: 42, auth_source: { name: 'TestUpdate' } + + expect(response.status).to eq 404 + end + + it 'cannot find destroy' do + post :destroy, id: 42 + + expect(response.status).to eq 404 + end + end end diff --git a/spec/controllers/settings_controller_spec.rb b/spec/controllers/settings_controller_spec.rb index fdc3080085..1d9f978c4e 100644 --- a/spec/controllers/settings_controller_spec.rb +++ b/spec/controllers/settings_controller_spec.rb @@ -119,5 +119,120 @@ describe SettingsController do expect(response.body).not_to have_selector "input[@name='settings[default_projects_modules][]'][@value='activity'][@checked='checked']" end end + + describe 'password settings' do + let(:old_settings) do + { + password_min_length: 10, + password_active_rules: [], + password_min_adhered_rules: 0, + password_days_valid: 365, + password_count_former_banned: 2, + lost_password: '1' + } + end + + let(:new_settings) do + { + password_min_length: 42, + password_active_rules: %w(uppercase lowercase), + password_min_adhered_rules: 7, + password_days_valid: 13, + password_count_former_banned: 80, + lost_password: '3' + } + end + + let(:original_settings) { Hash.new } + + before do + old_settings.keys.each do |key| + original_settings[key] = Setting[key] + end + + old_settings.keys.each do |key| + Setting[key] = old_settings[key] + end + end + + after do + # restore settings + old_settings.keys.each do |key| + Setting[key] = original_settings[key] + end + end + + describe 'POST #edit with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + + post 'edit', tab: 'authentication', settings: new_settings + end + + it 'is successful' do + expect(response).to be_redirect # to auth tab + end + + it 'sets the minimum password length to 42' do + expect(Setting[:password_min_length]).to eq '42' + end + + it 'sets the active character classes to lowercase and uppercase' do + expect(Setting[:password_active_rules]).to eq ['uppercase', 'lowercase'] + end + + it 'sets the required number of classes to 7' do + expect(Setting[:password_min_adhered_rules]).to eq '7' + end + + it 'sets passwords to expire after 13 days' do + expect(Setting[:password_days_valid]).to eq '13' + end + + it 'bans the last 80 passwords' do + expect(Setting[:password_count_former_banned]).to eq '80' + end + + it 'sets the lost password option to the nonsensical 3' do + expect(Setting[:lost_password]).to eq '3' + end + end + + describe 'POST #edit with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + + post 'edit', tab: 'authentication', settings: new_settings + end + + it 'is successful' do + expect(response).to be_redirect # to auth tab + end + + it 'does not set the minimum password length to 42' do + expect(Setting[:password_min_length]).to eq '10' + end + + it 'does not set the active character classes to lowercase and uppercase' do + expect(Setting[:password_active_rules]).to eq [] + end + + it 'does not set the required number of classes to 7' do + expect(Setting[:password_min_adhered_rules]).to eq '0' + end + + it 'does not set passwords to expire after 13 days' do + expect(Setting[:password_days_valid]).to eq '365' + end + + it 'does not ban the last 80 passwords' do + expect(Setting[:password_count_former_banned]).to eq '2' + end + + it 'does not set the lost password option to the nonsensical 3' do + expect(Setting[:lost_password]).to eq '1' + end + end + end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 3429fb9a8d..82e68b4d95 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -247,6 +247,40 @@ describe UsersController do end describe "index" do + describe 'new user button' do + render_views + + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + + as_logged_in_user admin do + get :index + end + end + + it 'is shown' do + expect(response.body).to have_selector('a', text: I18n.t('label_user_new')) + end + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + + as_logged_in_user admin do + get :index + end + end + + # you must not be able to create new users if password login is disabled + # as users are managed externally + it 'is hidden' do + expect(response.body).not_to have_selector('a', text: I18n.t('label_user_new')) + end + end + end + describe "with session lifetime" do # TODO move this section to a proper place because we test a # before_filter from the application controller @@ -347,6 +381,68 @@ describe UsersController do end end + describe '#new' do + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + + as_logged_in_user admin do + get :new + end + end + + it 'should return HTTP 200' do + expect(response.status).to eq 200 + end + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + + as_logged_in_user admin do + get :new + end + end + + # you must not be able to create new users if password login is disabled + it 'should return HTTP 404' do + expect(response.status).to eq 404 + end + end + end + + describe '#create' do + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + + as_logged_in_user admin do + post :create + end + end + + it 'should return HTTP 400 due to missing parameters' do + expect(response.status).to eq 400 + end + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + + as_logged_in_user admin do + post :create + end + end + + # you must not be able to create new users if password login is disabled + it 'should return HTTP 404' do + expect(response.status).to eq 404 + end + end + end + describe "update" do context "fields" do let(:user) { FactoryGirl.create(:user, :firstname => 'Firstname', diff --git a/spec/features/accessibility/work_packages/work_package_query_spec.rb b/spec/features/accessibility/work_packages/work_package_query_spec.rb index 3fd0c2d82e..0308a41605 100644 --- a/spec/features/accessibility/work_packages/work_package_query_spec.rb +++ b/spec/features/accessibility/work_packages/work_package_query_spec.rb @@ -66,6 +66,18 @@ describe 'Work package index accessibility' do describe 'Change state', js: true do # TODO end + + after do + # Ensure that all requests have fired and are answered. Otherwise one + # spec can interfere with the next when a request of the former is still + # running in the one process but the other process has already removed + # the data in the db to prepare for the next spec. + # + # Taking an element, that get's activated late in the page setup. + page.should_not have_selector('ul.dropdown-menu a.inactive', + :text => Regexp.new("^#{I18n.t(:button_save)}$"), + :visible => false) + end end describe 'Sort link', js: true do diff --git a/spec/features/omniauth/omniauth_spec.rb b/spec/features/omniauth/omniauth_spec.rb index 23cfd4c212..2a4e4c63ee 100644 --- a/spec/features/omniauth/omniauth_spec.rb +++ b/spec/features/omniauth/omniauth_spec.rb @@ -46,6 +46,12 @@ describe 'Omniauth authentication' do OmniAuth.config.logger = @omniauth_logger end + ## + # Returns a given translation up until the first occurrence of a parameter (exclusive). + def translation_substring(translation) + translation.scan(/(^.*) %\{/).first.first + end + context 'sign in existing user' do let(:user) do FactoryGirl.create(:user, @@ -101,6 +107,20 @@ describe 'Omniauth authentication' do end end + describe 'sign out a user with direct login and login required' do + before do + Setting.stub(:login_required?).and_return(true) + Concerns::OmniauthLogin.stub(:direct_login_provider).and_return('developer') + end + + it 'shows a notice that the user has been logged out' do + visit signout_path + + expect(page).to have_content(I18n.t(:notice_logged_out)) + expect(page).to have_content translation_substring(I18n.t(:instructions_after_logout)) + end + end + shared_examples 'omniauth user registration' do it 'should register new user' do visit '/auth/developer' @@ -166,15 +186,81 @@ describe 'Omniauth authentication' do end end + context 'registration by email' do + before do + allow(Setting).to receive(:self_registration?).and_return(true) + allow(Setting).to receive(:self_registration).and_return('1') + end + + shared_examples 'registration with registration by email' do + it 'shows a note explaining that the account has to be activated' do + visit login_path + + # login form developer strategy + fill_in 'first_name', with: 'Ifor' + fill_in 'last_name', with: 'McAlistar' + fill_in 'email', with: 'i.mcalistar@example.com' + + click_link_or_button 'Sign In' + + expect(page).to have_content(I18n.t(:notice_account_register_done)) + + if defined? instructions + expect(page).to have_content instructions + end + end + end + + it_behaves_like 'registration with registration by email' do + let(:login_path) { '/auth/developer' } + end + + context 'with direct login enabled and login required' do + before do + Setting.stub(:login_required?).and_return(true) + Concerns::OmniauthLogin.stub(:direct_login_provider).and_return('developer') + end + + it_behaves_like 'registration with registration by email' do + # i.e. it still shows a notice + # instead of redirecting straight back to the omniauth login provider + let(:login_path) { signin_path } + let(:instructions) { translation_substring I18n.t(:instructions_after_registration) } + end + end + end + context 'error occurs' do - it 'should fail with generic error message' do - # set omniauth to test mode will redirect all calls to omniauth - # directly to the callback and by setting the mock_auth provider - # to a symbol will force omniauth to fail /auth/failure - OmniAuth.config.test_mode = true - OmniAuth.config.mock_auth[:developer] = :invalid_credentials - visit '/auth/developer' - expect(page).to have_content(I18n.t(:error_external_authentication_failed)) + shared_examples 'omniauth signin error' do + it 'should fail with generic error message' do + # set omniauth to test mode will redirect all calls to omniauth + # directly to the callback and by setting the mock_auth provider + # to a symbol will force omniauth to fail /auth/failure + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:developer] = :invalid_credentials + visit login_path + expect(page).to have_content(I18n.t(:error_external_authentication_failed)) + + if defined? instructions + expect(page).to have_content instructions + end + end + end + + it_behaves_like 'omniauth signin error' do + let(:login_path) { '/auth/developer' } + end + + context 'with direct login and login required' do + before do + Setting.stub(:login_required?).and_return(true) + Concerns::OmniauthLogin.stub(:direct_login_provider).and_return('developer') + end + + it_behaves_like 'omniauth signin error' do + let(:login_path) { signin_path } + let(:instructions) { translation_substring I18n.t(:instructions_after_error) } + end end end end diff --git a/spec/features/users/edit_users_spec.rb b/spec/features/users/edit_users_spec.rb new file mode 100644 index 0000000000..29b88ca856 --- /dev/null +++ b/spec/features/users/edit_users_spec.rb @@ -0,0 +1,86 @@ +#-- 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. +#++ + +require 'spec_helper' +require 'features/projects/projects_page' + +describe 'edit users', js: true do + let(:current_user) { FactoryGirl.create :admin } + let(:user) { FactoryGirl.create :user } + + let!(:auth_source) { FactoryGirl.create :auth_source } + + before do + allow(User).to receive(:current).and_return current_user + end + + def auth_select + find :css, 'select#user_auth_source_id' + end + + def user_password + find :css, 'input#user_password' + end + + context 'with internal authentication' do + before do + visit edit_user_path(user) + end + + it 'shows internal authentication being selected including password settings' do + expect(auth_select.value).to eq '' # selected internal + expect(user_password).to be_visible + end + + it 'hides password settings when switching to an LDAP auth source' do + auth_select.select auth_source.name + + expect(page).not_to have_selector('input#user_password') + end + end + + context 'with external authentication' do + before do + user.auth_source = auth_source + user.save! + + visit edit_user_path(user) + end + + it 'shows external authentication being selected and no password settings' do + expect(auth_select.value).to eq auth_source.id.to_s + expect(page).not_to have_selector('input#user_password') + end + + it 'shows password settings when switching back to internal authentication' do + auth_select.select I18n.t('label_internal') + + expect(user_password).to be_visible + end + end +end diff --git a/spec/lib/api/v3/work_packages/work_package_model_spec.rb b/spec/lib/api/v3/work_packages/work_package_model_spec.rb index 623614d563..4cb10b5082 100644 --- a/spec/lib/api/v3/work_packages/work_package_model_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_model_spec.rb @@ -70,5 +70,35 @@ h2. Plan for this month it { expect(model.is_closed).to be_false } end end + + describe 'visibility to related work packages' do + let(:project) { FactoryGirl.create(:project, is_public: false) } + let(:forbidden_project) { FactoryGirl.create(:project, is_public: false) } + let(:user) { FactoryGirl.create(:user, member_in_project: project) } + + let(:work_package) { FactoryGirl.create(:work_package, project: project) } + let(:work_package_2) { FactoryGirl.create(:work_package, project: project) } + let(:forbidden_work_package) { FactoryGirl.create(:work_package, project: forbidden_project) } + + before do + allow(User).to receive(:current).and_return(user) + allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true) + end + + describe 'relations' do + let!(:relation) { FactoryGirl.create(:relation, + from: work_package, + to: work_package_2) } + let!(:forbidden_relation) { FactoryGirl.create(:relation, + from: work_package, + to: forbidden_work_package) } + + it { expect(model.relations.count).to eq(1) } + + it { expect(model.relations[0].from_id).to eq(work_package.id) } + + it { expect(model.relations[0].to_id).to eq(work_package_2.id) } + end + end end end diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb index f403d3dc67..f8b3135467 100644 --- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -184,6 +184,45 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do expect(subject).to_not have_json_path('_links/addRelation/href') end end + + describe 'linked relations' do + let(:project) { FactoryGirl.create(:project, is_public: false) } + let(:forbidden_project) { FactoryGirl.create(:project, is_public: false) } + let(:user) { FactoryGirl.create(:user, member_in_project: project) } + + before do + allow(User).to receive(:current).and_return(user) + allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true) + end + + context 'parent' do + let(:work_package) { FactoryGirl.create(:work_package, + project: project, + parent_id: forbidden_work_package.id) } + let!(:forbidden_work_package) { FactoryGirl.create(:work_package, project: forbidden_project) } + + it { expect(subject).to_not have_json_path('_links/parent') } + end + + context 'children' do + let(:work_package) { FactoryGirl.create(:work_package, project: project) } + let!(:forbidden_work_package) { FactoryGirl.create(:work_package, + project: forbidden_project, + parent_id: work_package.id) } + + it { expect(subject).to_not have_json_path('_links/children') } + + describe 'visible and invisible children' do + let!(:child) { FactoryGirl.create(:work_package, + project: project, + parent_id: work_package.id) } + + it { expect(subject).to have_json_size(1).at_path('_links/children') } + + it { expect(parse_json(subject)["_links"]["children"][0]["title"]).to eq(child.subject) } + end + end + end end describe '_embedded' do diff --git a/spec/models/custom_field_spec.rb b/spec/models/custom_field_spec.rb index 6beeda0ebc..38a10d9ff3 100644 --- a/spec/models/custom_field_spec.rb +++ b/spec/models/custom_field_spec.rb @@ -382,5 +382,23 @@ describe CustomField do end it { expect(field).not_to be_valid } end + + describe "WITH a text field + WITH negative minimum length" do + before do + field.field_format = 'text' + field.min_length = -2 + end + it { expect(field).not_to be_valid } + end + + describe "WITH a text field + WITH negative maximum length" do + before do + field.field_format = 'text' + field.max_length = -2 + end + it { expect(field).not_to be_valid } + end end end diff --git a/spec/views/api/experimental/work_packages/index_api_json_spec.rb b/spec/views/api/experimental/work_packages/index_api_json_spec.rb index 015771221b..530aa2a62e 100644 --- a/spec/views/api/experimental/work_packages/index_api_json_spec.rb +++ b/spec/views/api/experimental/work_packages/index_api_json_spec.rb @@ -76,11 +76,29 @@ describe 'api/experimental/work_packages/index.api.rabl' do it { should have_json_size(0).at_path('work_packages') } end + describe 'created/updated at' do + let(:wp) { FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: (DateTime.now + 1.day)) } + let(:work_packages) { [ wp ] } + let(:column_names) { [] } + let(:custom_field_column_names) { [] } + + it { expect(parse_json(subject)['work_packages'][0]['updated_at']).to eq(wp.updated_at.utc.iso8601) } + it { expect(parse_json(subject)['work_packages'][0]['created_at']).to eq(wp.created_at.utc.iso8601) } + end + describe 'with 3 work packages but no columns' do let(:work_packages) { [ - FactoryGirl.build(:work_package), - FactoryGirl.build(:work_package), - FactoryGirl.build(:work_package) + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now), + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now), + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now) ] } let(:column_names) { [] } let(:custom_field_column_names) { [] } @@ -93,8 +111,12 @@ describe 'api/experimental/work_packages/index.api.rabl' do describe 'with 2 work packages and columns' do let(:work_packages) { [ - FactoryGirl.build(:work_package), - FactoryGirl.build(:work_package) + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now), + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now) ] } let(:column_names) { %w(subject description due_date) } let(:custom_field_column_names) { [] } @@ -109,7 +131,11 @@ describe 'api/experimental/work_packages/index.api.rabl' do end describe 'with project column' do - let(:work_packages) { [FactoryGirl.build(:work_package)] } + let(:work_packages) { [ + FactoryGirl.build(:work_package, + created_at: DateTime.now, + updated_at: DateTime.now) + ] } let(:column_names) { %w(subject project) } let(:custom_field_column_names) { [] } diff --git a/spec/views/layouts/admin.html.erb_spec.rb b/spec/views/layouts/admin.html.erb_spec.rb new file mode 100644 index 0000000000..7f3e9f0800 --- /dev/null +++ b/spec/views/layouts/admin.html.erb_spec.rb @@ -0,0 +1,71 @@ +#-- 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. +#++ + +require 'spec_helper' + +describe 'layouts/admin' do + include Redmine::MenuManager::MenuHelper + helper Redmine::MenuManager::MenuHelper + + let(:admin) { FactoryGirl.create :admin } + + before do + view.stub(:current_menu_item).and_return('overview') + view.stub(:default_breadcrumb) + controller.stub(:default_search_scope) + + User.stub(:current).and_return admin + view.stub(:current_user).and_return admin + end + + # All password-based authentication is to be hidden and disabled if + # `disable_password_login` is true. This includes LDAP. + describe 'LDAP authentication menu entry' do + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + render + end + + it 'is shown' do + expect(rendered).to have_selector('a', text: I18n.t('label_ldap_authentication')) + end + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + render + end + + it 'is hidden' do + expect(rendered).not_to have_selector('a', text: I18n.t('label_ldap_authentication')) + end + end + end +end diff --git a/spec/views/settings/_authentication.html.erb_spec.rb b/spec/views/settings/_authentication.html.erb_spec.rb new file mode 100644 index 0000000000..c343942ddf --- /dev/null +++ b/spec/views/settings/_authentication.html.erb_spec.rb @@ -0,0 +1,61 @@ +#-- 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. +#++ + +require 'spec_helper' + +describe 'settings/_authentication' do + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + render + end + + it 'shows password settings' do + expect(rendered).to have_text I18n.t('label_password_lost') + end + + it 'shows automated user blocking options' do + expect(rendered).to have_text I18n.t(:brute_force_prevention, :scope => [:settings]) + end + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + render + end + + it 'does not show password settings' do + expect(rendered).not_to have_text I18n.t('label_password_lost') + end + + it 'does not show automated user blocking options' do + expect(rendered).not_to have_text I18n.t(:brute_force_prevention, :scope => [:settings]) + end + end +end diff --git a/spec/views/users/edit.html.erb_spec.rb b/spec/views/users/edit.html.erb_spec.rb index 8f7b30371b..d8b899f963 100644 --- a/spec/views/users/edit.html.erb_spec.rb +++ b/spec/views/users/edit.html.erb_spec.rb @@ -40,12 +40,82 @@ describe 'users/edit' do assign(:auth_sources, []) allow(view).to receive(:current_user).and_return(current_user) - - render end it 'shows the authentication provider' do + render + expect(response.body).to include('Test Provider') end + + it 'does not show a no-login warning when password login is disabled' do + OpenProject::Configuration.stub(:disable_password_login).and_return(true) + render + + expect(response.body).not_to include I18n.t('user.no_login') + end + end + + context 'with password-based login' do + let(:user) { FactoryGirl.build :user, id: 42 } + + before do + assign :user, user + assign :auth_sources, [] + + allow(view).to receive(:current_user).and_return(current_user) + end + + context 'with password login disabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(true) + end + + it 'warns that the user cannot login' do + render + + expect(response.body).to include I18n.t('user.no_login') + end + + context 'with auth sources' do + let(:auth_sources) { [FactoryGirl.create(:auth_source)]} + + before do + assign :auth_sources, auth_sources + end + + it 'does not show the auth source selection' do + render + + expect(rendered).not_to have_selector('#user_auth_source_id') + end + end + end + + context 'with password login enabled' do + before do + OpenProject::Configuration.stub(:disable_password_login?).and_return(false) + end + + it 'shows password options' do + render + + expect(rendered).to have_text I18n.t('user.assign_random_password') + end + + context 'with auth sources' do + let(:auth_sources) { [FactoryGirl.create(:auth_source)] } + + before do + assign :auth_sources, auth_sources + end + + it 'shows the auth source selection' do + render + + expect(rendered).to have_selector('#user_auth_source_id') + end + end + end end end