Merge branch 'dev' into feature/styleguide-details-pane-table

pull/1717/head
Richard 10 years ago
commit f5be86dafd
  1. 9
      app/assets/javascripts/angular/helpers/components/api-helper.js
  2. 22
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  3. 2
      app/assets/javascripts/angular/openproject-app.js
  4. 26
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  5. 39
      app/assets/javascripts/angular/work_packages/tabs/add-work-package-child-directive.js
  6. 37
      app/assets/javascripts/angular/work_packages/tabs/add-work-package-relation-directive.js
  7. 2
      app/assets/javascripts/angular/work_packages/tabs/related-work-package-table-row-directive.js
  8. 84
      app/assets/javascripts/angular/work_packages/tabs/work-package-children-directive.js
  9. 53
      app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js
  10. 144
      app/assets/javascripts/angular/work_packages/view_models/relations-handler.js
  11. 5
      app/assets/stylesheets/content/_components_add_comments_default.sass
  12. 34
      app/controllers/account_controller.rb
  13. 5
      app/controllers/auth_sources_controller.rb
  14. 17
      app/controllers/settings_controller.rb
  15. 7
      app/controllers/users_controller.rb
  16. 6
      app/models/custom_field.rb
  17. 2
      app/models/custom_value.rb
  18. 49
      app/views/account/exit.html.erb
  19. 8
      app/views/api/experimental/work_packages/index.api.rabl
  20. 52
      app/views/settings/_authentication.html.erb
  21. 4
      app/views/settings/_notifications.html.erb
  22. 77
      app/views/users/_form.html.erb
  23. 6
      app/views/users/index.html.erb
  24. 3
      bower.json
  25. 17
      config/locales/de.yml
  26. 15
      config/locales/en.yml
  27. 48
      features/timelines/timeline_modal_views.feature
  28. 6
      karma/tests/controllers/details-tab-overview-controller-test.js
  29. 11
      karma/tests/controllers/work-package-details-controller-test.js
  30. 9
      karma/tests/controllers/work-packages-controller-test.js
  31. 9
      karma/tests/controllers/work-packages-list-controller-test.js
  32. 2
      karma/tests/directives/components/date-time-directive-test.js
  33. 12
      karma/tests/directives/components/toggled-multiselect-directive-test.js
  34. 12
      karma/tests/directives/work_packages/options-dropdown-directive-test.js
  35. 8
      karma/tests/directives/work_packages/work-package-column-directive-test.js
  36. 10
      karma/tests/directives/work_packages/work-package-group-sums-directive-test.js
  37. 155
      karma/tests/directives/work_packages/work-package-relations-directive-test.js
  38. 10
      karma/tests/directives/work_packages/work-package-total-sums-directive-test.js
  39. 19
      karma/tests/helpers/components/work-packages-helper-test.js
  40. 10
      karma/tests/helpers/work-package-context-menu-helper-test.js
  41. 9
      karma/tests/helpers/work-package-table-helper-test.js
  42. 12
      karma/tests/layout/query-menu-item-factory-test.js
  43. 13
      karma/tests/services/query-service-test.js
  44. 10
      karma/tests/services/work-package-service-test.js
  45. 9
      karma/tests/work_packages/column-context-menu-test.js
  46. 9
      karma/tests/work_packages/work-package-context-menu-test.js
  47. 2
      lib/api/errors/validation.rb
  48. 2
      lib/api/v3/relations/relations_api.rb
  49. 4
      lib/api/v3/work_packages/work_package_model.rb
  50. 10
      lib/api/v3/work_packages/work_package_representer.rb
  51. 6
      lib/redmine.rb
  52. 4
      public/templates/work_packages.list.details.html
  53. 5
      public/templates/work_packages/tabs/_add_work_package_child.html
  54. 12
      public/templates/work_packages/tabs/_add_work_package_relation.html
  55. 48
      public/templates/work_packages/tabs/_work_package_children.html
  56. 42
      public/templates/work_packages/tabs/_work_package_parent.html
  57. 32
      public/templates/work_packages/tabs/_work_package_relations.html
  58. 50
      public/templates/work_packages/tabs/relations.html
  59. 8
      public/templates/work_packages/tabs/watchers.html
  60. 44
      spec/controllers/auth_sources_controller_spec.rb
  61. 115
      spec/controllers/settings_controller_spec.rb
  62. 96
      spec/controllers/users_controller_spec.rb
  63. 12
      spec/features/accessibility/work_packages/work_package_query_spec.rb
  64. 102
      spec/features/omniauth/omniauth_spec.rb
  65. 86
      spec/features/users/edit_users_spec.rb
  66. 30
      spec/lib/api/v3/work_packages/work_package_model_spec.rb
  67. 39
      spec/lib/api/v3/work_packages/work_package_representer_spec.rb
  68. 18
      spec/models/custom_field_spec.rb
  69. 38
      spec/views/api/experimental/work_packages/index_api_json_spec.rb
  70. 71
      spec/views/layouts/admin.html.erb_spec.rb
  71. 61
      spec/views/settings/_authentication.html.erb_spec.rb
  72. 74
      spec/views/users/edit.html.erb_spec.rb

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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
%>
<div id="login-form" class="form">
<h1><%= I18n.t(:label_login) %></h1>
<hr class="form_separator">
<p><%= instruction_text.html_safe %></p>
</div>
<%= call_hook :view_account_login_bottom %>

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

@ -42,22 +42,38 @@ See doc/COPYRIGHT.rdoc for more details.
<fieldset>
<legend><%= I18n.t(:passwords, :scope => [:settings]) %></legend>
<p><%= setting_text_field :password_min_length, :size => 6 %></p>
<p><%= setting_multiselect :password_active_rules,
OpenProject::Passwords::Evaluator.known_rules.map do |r|
[l("label_password_rule_#{r}"), r]
end %></p>
<p><%= setting_text_field :password_min_adhered_rules, :size => 6 %></p>
<p><%= setting_text_field :password_days_valid, :size => 6 %></p>
<p><%= setting_text_field :password_count_former_banned, :size => 6 %></p>
<p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
<% if !OpenProject::Configuration.disable_password_login? %>
<p><%= setting_text_field :password_min_length, :size => 6 %></p>
<p><%= setting_multiselect :password_active_rules,
OpenProject::Passwords::Evaluator.known_rules.map do |r|
[l("label_password_rule_#{r}"), r]
end %></p>
<p><%= setting_text_field :password_min_adhered_rules, :size => 6 %></p>
<p><%= setting_text_field :password_days_valid, :size => 6 %></p>
<p><%= setting_text_field :password_count_former_banned, :size => 6 %></p>
<p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
<% else %>
<p>
<label><%= I18n.t :note %></label>
<%=
url = 'https://github.com/opf/openproject/blob/dev/doc/CONFIGURATION.md#disable-password-login'
explanation = I18n.t :note_password_login_disabled,
:configuration => "<a href=\"#{url}\">#{I18n.t('label_configuration')}</a>"
explanation.html_safe
%>
</p>
<% end %>
</fieldset>
<fieldset>
<legend><%= I18n.t(:brute_force_prevention, :scope => [:settings]) %></legend>
<p><%= setting_text_field :brute_force_block_after_failed_logins %></p>
<p><%= setting_text_field :brute_force_block_minutes %></p>
</fieldset>
<% unless OpenProject::Configuration.disable_password_login? %>
<fieldset>
<legend><%= I18n.t(:brute_force_prevention, :scope => [:settings]) %></legend>
<p><%= setting_text_field :brute_force_block_after_failed_logins %></p>
<p><%= setting_text_field :brute_force_block_minutes %></p>
</fieldset>
<% end %>
<fieldset>
<legend><%= I18n.t(:session, :scope => [:settings]) %></legend>
@ -82,9 +98,11 @@ See doc/COPYRIGHT.rdoc for more details.
</fieldset>
</div>
<div style="float:right;">
<%= link_to l(:label_ldap_authentication), {:controller => '/ldap_auth_sources', :action => 'index'}, :class => 'icon icon-server-key' %>
</div>
<% unless OpenProject::Configuration.disable_password_login? %>
<div style="float:right;">
<%= link_to l(:label_ldap_authentication), {:controller => '/ldap_auth_sources', :action => 'index'}, :class => 'icon icon-server-key' %>
</div>
<% end %>
<%= submit_tag l(:button_save) %>
<% end %>

@ -55,10 +55,10 @@ See doc/COPYRIGHT.rdoc for more details.
<% Setting.available_languages.each do |lang| %>
<div id="emails_decorators_<%= lang %>" style="display:none" class="emails_decorators">
<%= label_tag l(:setting_emails_header) %>
<%= label_tag "settings[emails_header][#{lang}]", l(:setting_emails_header) %>
<%= text_area_tag("settings[emails_header][#{lang}]", Setting.emails_header[lang], :label => false, :class => 'wiki-edit', :rows => 5) %>
<%= label_tag l(:setting_emails_footer) %>
<%= label_tag "settings[emails_footer][#{lang}]", l(:setting_emails_footer) %>
<%= text_area_tag("settings[emails_footer][#{lang}]", Setting.emails_footer[lang], :label => false, :class => 'wiki-edit', :rows => 5) %>
</div>
<% end %>

@ -61,39 +61,56 @@ See doc/COPYRIGHT.rdoc for more details.
</p>
<%= I18n.t('user.authentication_settings_disabled_due_to_external_authentication') %>
<% else %>
<% unless @auth_sources.empty? %>
<% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>
<p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %></p>
<% end %>
<% if !OpenProject::Configuration.disable_password_login? %>
<%
pw_style =
if @user.change_password_allowed?
''
else
' style="display: none;"'
end
%>
<div id="password_fields"<%= pw_style.html_safe %>>
<% assign_random_password_enabled = params[:user] &&
params[:user][:assign_random_password] %>
<p>
<label for="user_assign_random_password">
<%= I18n.t(:assign_random_password, :scope => :user) %>
</label>
<%= check_box_tag("user[assign_random_password]",
"1",
assign_random_password_enabled) %>
</p>
<p>
<%= f.password_field :password,
:required => true,
:size => 25,
:disabled => assign_random_password_enabled %><br />
<%= password_complexity_requirements %>
</p>
<p>
<%= f.password_field :password_confirmation,
:required => true,
:size => 25,
:disabled => assign_random_password_enabled %>
</p>
<p>
<%= f.check_box :force_password_change,
:disabled => assign_random_password_enabled %>
</p>
</div>
<% else %>
<div id="no_password_info">
<p>
<label><%= I18n.t 'warning' %></label>
<%= I18n.t 'user.no_login' %>
</p>
</div>
<% end %>
<% end %>
<div id="password_fields" style="<%= 'display:none;' unless @user.change_password_allowed? %>">
<% assign_random_password_enabled = params[:user] &&
params[:user][:assign_random_password] %>
<p>
<label for="user_assign_random_password">
<%= I18n.t(:assign_random_password, :scope => :user) %>
</label>
<%= check_box_tag("user[assign_random_password]",
"1",
assign_random_password_enabled) %>
</p>
<p>
<%= f.password_field :password,
:required => true,
:size => 25,
:disabled => assign_random_password_enabled %><br />
<%= password_complexity_requirements %>
</p>
<p>
<%= f.password_field :password_confirmation,
:required => true,
:size => 25,
:disabled => assign_random_password_enabled %>
</p>
<p>
<%= f.check_box :force_password_change,
:disabled => assign_random_password_enabled %>
</p>
</div>
</div>
<div class="box">

@ -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 %>
<h2><%=l(:label_user_plural)%></h2>

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

@ -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}\""

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

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

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

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

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

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

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

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

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

@ -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 = '<span work-package-column work-package="workPackage" column="column" display-type="displayType" display-empty="-"></span>';

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

@ -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 = "<work-package-relations title='MyRelation' work-package='workPackage' relations='relations' button-title='Add Relation' button-icon='%MyIcon%'></work-package-relation>"
var singleElementHtml = "<work-package-relations title='MyRelation' work-package='workPackage' relations='relations' button-title='Add Relation' button-icon='%MyIcon%' singleton-relation='true'></work-package-relation>"
var html = "<work-package-relations title='MyRelation' handler='relations' button-title='Add Relation' button-icon='%MyIcon%'></work-package-relations>"
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();
}));

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

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

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

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

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

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

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

@ -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 = '<div></div>';
container = angular.element(html);

@ -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 = '<div></div>';
container = angular.element(html);

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

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

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

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

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

@ -35,9 +35,9 @@
ng-if="toggleWatchLink" />
<a href="#">#{{ workPackage.props.id }}</a>
<span ng-bind="I18n.t('js.label_added_by')"/>
<a ng-href="{{ userPath(author.props.id) }}" ng-bind="author.props.name"/> <span ng-bind="I18n.t('js.label_on')"/> <date dateValue="workPackage.props.createdAt"></date>.
<a ng-href="{{ userPath(author.props.id) }}" ng-bind="author.props.name"/> <span ng-bind="I18n.t('js.label_on')"/> <date date-value="workPackage.props.createdAt"></date>.
<span ng-bind="I18n.t('js.label_last_updated_on')"/>
<date dateValue="workPackage.props.updatedAt"></date>.
<date date-value="workPackage.props.updatedAt"></date>.
</span>
<div class="work-package-details-tab" ui-view></div>

@ -0,0 +1,5 @@
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="handler.addRelation()">
</button>

@ -0,0 +1,12 @@
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="handler.addRelation(this)">
</button>
<input id="relation_to_id-{{ handler.relationsId }}"
name="relation[to_id][{{ handler.relationsId }}]"
size="10"
type="text"
autocomplete="off">
<div id="related_issue_candidates-{{ handler.relationsId }}"
class="autocomplete related-issue-candidates"></div>

@ -1,48 +0,0 @@
<div class="relation">
<h3>
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
<span ng-if="!isSingletonRelation">({{ childrenCount }})</span>
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="children">
<table>
<thead>
<tr>
<td>{{ I18n.t('js.work_packages.properties.subject') }}</td>
<td>{{ I18n.t('js.work_packages.properties.status') }}</td>
<td>{{ I18n.t('js.work_packages.properties.assignee') }}</td>
<td></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="workPackage in children">
<td>
<a class="work_package" ng-class="getState(workPackage)" href="{{ workPackagePath(workPackage.props.id) }}">
{{ getFullIdentifier(workPackage) }}
</a>
</td>
<td>{{ workPackage.props.status }}</td>
<td>
<a href="{{ userPath(workPackage.embedded.assignee.props.id) }}">
{{ workPackage.embedded.assignee.props.name }}
</a>
</td>
<td><!--i title="delete relation" class="delete-item icon-delete" ng-click="removeChild()"></i--></td>
</tr>
</tbody>
</table>
</div>
<div ng-if="!children || children.length === 0">
No child work packages
</div>
</div>
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="addChild()">
</button>
</div>
</div>

@ -1,42 +0,0 @@
<div class="relation">
<h3 ng-click="expand=!expand">
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="parent">
<table>
<thead>
<tr>
<td>{{ I18n.t('js.work_packages.properties.subject') }}</td>
<td>{{ I18n.t('js.work_packages.properties.status') }}</td>
<td>{{ I18n.t('js.work_packages.properties.assignee') }}</td>
<td></td>
</tr>
</thead>
<tbody>
<tr>
<td>
<a class="work_package" ng-class="getState(parent)" href="{{ workPackagePath(parent.props.id) }}">
{{ getFullIdentifier(parent) }}
</a>
</td>
<td>{{ parent.props.status }}</td>
<td>
<a href="{{ userPath(parent.embedded.assignee.props.id) }}">
{{ parent.embedded.assignee.props.name }}
</a>
</td>
<td><!--i title="delete relation" class="delete-item icon-delete" ng-click="removeChild()"></i--></td>
</tr>
</tbody>
</table>
</div>
<div ng-if="!parent">
No parent work packages
</div>
</div>
</div>
</div>

@ -1,13 +1,13 @@
<div class="relation">
<h3 ng-click="expand=!expand">
<h3>
<accessible-by-keyboard execute="toggleExpand()">
<i class="icon-pull-content" ng-class="stateClass"></i> {{ title }}
<span ng-if="!isSingletonRelation">({{ relationsCount }})</span>
<span ng-if="!handler.isSingletonRelation">({{ handler.getCount() }})</span>
</accessible-by-keyboard>
</h3>
<div class="content" ng-show="expand">
<div class="workpackages">
<div ng-if="relations">
<div ng-if="handler.relations">
<table>
<thead>
<tr>
@ -19,15 +19,15 @@
</thead>
<tbody>
<tr related-work-package-table-row
ng-repeat="relation in relations">
ng-repeat="relation in handler.relations">
<td>
<a class="work_package" ng-class="state" href="{{ workPackagePath(relatedWorkPackage.props.id) }}">
<a title="{{ fullIdentifier }}" class="work_package" ng-class="state" href="{{ workPackagePath(relatedWorkPackage.props.id) }}">
{{ fullIdentifier }}
</a>
</td>
<td>{{ relatedWorkPackage.props.status }}</td>
<td title="{{ relatedWorkPackage.props.status }}">{{ relatedWorkPackage.props.status }}</td>
<td>
<a href="{{ userPath(relatedWorkPackage.embedded.assignee.props.id) }}">
<a title="{{ relatedWorkPackage.embedded.assignee.props.name }}" href="{{ userPath(relatedWorkPackage.embedded.assignee.props.id) }}">
{{ relatedWorkPackage.embedded.assignee.props.name }}
</a>
</td>
@ -41,23 +41,13 @@
</tbody>
</table>
</div>
<div ng-if="!relations || relations.length === 0">
<div ng-if="handler.isEmpty()">
No relation exists
</div>
</div>
<div class="add-relation" ng-if="canAddRelation">
<button class="button"
title="{{ btnTitle }}"
ng-bind-html="btnIcon + ' ' + btnTitle"
ng-click="addRelation()">
</button>
<input id="relation_to_id-{{ relationIdentifier }}"
name="relation[to_id][{{ relationIdentifier }}]"
size="10"
type="text"
autocomplete="off">
<div id="related_issue_candidates-{{ relationIdentifier }}"
class="autocomplete related-issue-candidates"></div>
<div class="add-relation" ng-if="handler.canAddRelation()" ng-switch="handler.type">
<add-work-package-child ng-switch_when="child"></add-work-package-child>
<add-work-package-relation ng-switch_when="relation"></add-work-package-relation>
</div>
</div>
</div>

@ -1,73 +1,55 @@
<div class="detail-panel-description">
<div class="detail-panel-description-content">
<work-package-parent title="{{ I18n.t('js.relation_labels.parent') }}"
work-package="workPackage"
parent="wpParent"
relation-identifier="'parent'"
<work-package-relations title="{{ I18n.t('js.relation_labels.parent') }}"
handler="wpParent"
button-title="Change Parent"
button-icon="<i class='icon-hierarchy icon-edit'></i>"
singleton-relation="true">
</work-package-parent>
button-icon="<i class='icon-hierarchy icon-edit'></i>">
</work-package-relations>
<work-package-children title="{{ I18n.t('js.relation_labels.children') }}"
work-package="workPackage"
children="wpChildren"
button-title="Add Child"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-children>
<work-package-relations title="{{ I18n.t('js.relation_labels.children') }}"
handler="wpChildren"
button-title="Add Child"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.relatedTo') }}"
work-package="workPackage"
relations="relatedTo"
relation-identifier="'relates'"
handler="relatedTo"
button-title="Add Related to"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.duplicates') }}"
work-package="workPackage"
relations="duplicates"
relation-identifier="'duplicates'"
handler="duplicates"
button-title="Add Duplicates"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.duplicated') }}"
work-package="workPackage"
relations="duplicated"
relation-identifier="'duplicated'"
handler="duplicated"
button-title="Add Duplicated by"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.blocks') }}"
work-package="workPackage"
relations="blocks"
relation-identifier="'blocks'"
handler="blocks"
button-title="Add Blocks"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.blocked') }}"
work-package="workPackage"
relations="blocked"
relation-identifier="'blocked'"
handler="blocked"
button-title="Add Blocked by"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.precedes') }}"
work-package="workPackage"
relations="precedes"
relation-identifier="'precedes'"
handler="precedes"
button-title="Add Precedes"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>
<work-package-relations title="{{ I18n.t('js.relation_labels.follows') }}"
work-package="workPackage"
relations="follows"
relation-identifier="'follows'"
handler="follows"
button-title="Add Follows"
button-icon="<i class='icon-hierarchy icon-add'></i>">
</work-package-relations>

@ -3,13 +3,11 @@
<ul ng-if="watchers.length">
<li ng-repeat="watcher in watchers">
<user-field user="watcher"></user-field>
<a href>
<accessible-by-keyboard execute="deleteWatcher(watcher)" ng-if="watcher.links.removeWatcher">
<icon-wrapper icon-name="close"
icon-title="{{I18n.t('js.button_delete_watcher')}}"
class="detail-panel-watchers-delete-watcher-icon"
ng-if="watcher.links.removeWatcher"
ng-click="deleteWatcher(watcher)"></icon-wrapper>
</a>
class="detail-panel-watchers-delete-watcher-icon"></icon-wrapper>
</accessible-by-keyboard>
</li>
</ul>

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save