Merge branch 'dev' into feature/integrate-op-plugins-plugin

pull/1649/head
Alex Coles 10 years ago
commit 6b5d8ba71d
  1. 2
      Gemfile
  2. 4
      Gemfile.lock
  3. 2
      app/assets/javascripts/angular/api/hal-api-resource.js
  4. 26
      app/assets/javascripts/angular/config/configuration-service.js
  5. 4
      app/assets/javascripts/angular/controllers/messages-controllers.js
  6. 56
      app/assets/javascripts/angular/directives/components/activity-comment-directive.js
  7. 2
      app/assets/javascripts/angular/directives/components/authoring-directive.js
  8. 47
      app/assets/javascripts/angular/directives/components/date-directive.js
  9. 17
      app/assets/javascripts/angular/filters/latest-items-filter.js
  10. 20
      app/assets/javascripts/angular/helpers/components/path-helper.js
  11. 42
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  12. 2
      app/assets/javascripts/angular/models/query.js
  13. 2
      app/assets/javascripts/angular/models/timelines/custom-field.js
  14. 4
      app/assets/javascripts/angular/routing.js
  15. 74
      app/assets/javascripts/angular/services/activity-service.js
  16. 43
      app/assets/javascripts/angular/services/timezone-service.js
  17. 155
      app/assets/javascripts/angular/work_packages/controllers/details-tab-overview-controller.js
  18. 92
      app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js
  19. 182
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  20. 26
      app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js
  21. 48
      app/assets/javascripts/angular/work_packages/tabs/attachment-file-size-directive.js
  22. 43
      app/assets/javascripts/angular/work_packages/tabs/attachment-title-cell-directive.js
  23. 47
      app/assets/javascripts/angular/work_packages/tabs/attachment-user-cell-directive.js
  24. 42
      app/assets/javascripts/angular/work_packages/tabs/attachments-table-directive.js
  25. 43
      app/assets/javascripts/angular/work_packages/tabs/attachments-title-directive.js
  26. 43
      app/assets/javascripts/angular/work_packages/tabs/editable-comment-directive.js
  27. 50
      app/assets/javascripts/angular/work_packages/tabs/exclusive-edit-directive.js
  28. 51
      app/assets/javascripts/angular/work_packages/tabs/user-activity-directive.js
  29. 81
      app/assets/javascripts/angular/work_packages/tabs/work-package-relation-directive.js
  30. 4
      app/assets/stylesheets/_work_packages.scss
  31. 3
      app/assets/stylesheets/content/_accounts.sass
  32. 7
      app/assets/stylesheets/content/_components_add_comments_default.md
  33. 44
      app/assets/stylesheets/content/_components_add_comments_default.sass
  34. 10
      app/assets/stylesheets/content/_components_add_comments_onclick.md
  35. 28
      app/assets/stylesheets/content/_components_add_comments_onclick.sass
  36. 41
      app/assets/stylesheets/content/_work_packages_details_activities.sass
  37. 87
      app/assets/stylesheets/content/_work_packages_details_attachments.sass
  38. 52
      app/assets/stylesheets/content/_work_packages_relations.sass
  39. 6
      app/assets/stylesheets/default.css.sass
  40. 3
      app/assets/stylesheets/default_simple.css.sass
  41. 30
      app/assets/stylesheets/layout/_split_view.sass
  42. 2
      app/assets/stylesheets/timelines.css.erb
  43. 47
      app/controllers/account_controller.rb
  44. 10
      app/controllers/api/experimental/concerns/query_loading.rb
  45. 3
      app/controllers/boards_controller.rb
  46. 29
      app/controllers/concerns/omniauth_login.rb
  47. 2
      app/controllers/my_controller.rb
  48. 47
      app/controllers/sys_controller.rb
  49. 2
      app/controllers/timelog_controller.rb
  50. 5
      app/controllers/work_packages_controller.rb
  51. 37
      app/helpers/content_for_helper.rb
  52. 7
      app/mailers/user_mailer.rb
  53. 5
      app/models/copy_project_job.rb
  54. 7
      app/models/user.rb
  55. 2
      app/models/work_package.rb
  56. 6
      app/views/account/_auth_providers.html.erb
  57. 32
      app/views/account/_omniauth_login.html.erb
  58. 63
      app/views/account/_password_login_form.html.erb
  59. 39
      app/views/account/login.html.erb
  60. 2
      app/views/boards/show.html.erb
  61. 2
      app/views/common/_validation_error.html.erb
  62. 5
      app/views/messages/show.rabl
  63. 2
      app/views/settings/_repositories.html.erb
  64. 2
      app/views/work_packages/bulk/edit.html.erb
  65. 6
      bower.json
  66. 18
      config/configuration.yml.example
  67. 1
      config/locales/de.yml
  68. 1
      config/locales/en.yml
  69. 21
      config/locales/js-de.yml
  70. 21
      config/locales/js-en.yml
  71. 2
      config/settings.yml
  72. 2
      db/migrate/migration_utils/customizable_utils.rb
  73. 2
      doc/CHANGELOG.md
  74. 33
      doc/CONFIGURATION.md
  75. 175
      extra/svn/OpenProjectAuthentication.pm
  76. 52
      extra/svn/create_views.sql
  77. 103
      extra/svn/reposman.rb
  78. 4
      karma.conf.js
  79. 295
      karma/tests/controllers/details-tab-overview-controller-test.js
  80. 233
      karma/tests/controllers/work-package-details-controller-test.js
  81. 186
      karma/tests/directives/components/date-time-directive-test.js
  82. 234
      karma/tests/directives/components/work-package-relation-directive-test.js
  83. 90
      karma/tests/directives/work_packages/attachment-file-size-directive-test.js
  84. 72
      karma/tests/directives/work_packages/attachment-title-cell-directive-test.js
  85. 79
      karma/tests/directives/work_packages/attachment-user-cell-directive-test.js
  86. 67
      karma/tests/directives/work_packages/attachments-title-directive-test.js
  87. 55
      karma/tests/filters/latest-items-filter-test.js
  88. 82
      karma/tests/services/activity-service-test.js
  89. 41
      karma/tests/services/timezone-service-test.js
  90. 2
      karma/tests/timeline_stubs.js
  91. 57
      lib/api/decorators/collection.rb
  92. 5
      lib/api/root.rb
  93. 25
      lib/api/v3/activities/activities_api.rb
  94. 1
      lib/api/v3/activities/activity_representer.rb
  95. 2
      lib/api/v3/root.rb
  96. 20
      lib/api/v3/users/user_representer.rb
  97. 43
      lib/api/v3/watchers/watchers_representer.rb
  98. 53
      lib/api/v3/work_packages/relation_model.rb
  99. 79
      lib/api/v3/work_packages/relation_representer.rb
  100. 62
      lib/api/v3/work_packages/watchers_api.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -200,7 +200,7 @@ end
gem 'grape', '~> 0.7.0'
gem 'representable', git: 'https://github.com/finnlabs/representable'
gem 'roar', '~> 0.12.6'
gem 'reform', require: false
gem 'reform', '~> 1.0.4', require: false
# Use the commented pure ruby gems, if you have not the needed prerequisites on
# board to compile the native ones. Note, that their use is discouraged, since

@ -339,7 +339,7 @@ GEM
json (~> 1.4)
redcarpet (3.0.0)
ref (1.0.5)
reform (1.0.1)
reform (1.0.4)
activemodel
disposable (~> 0.0.4)
representable (~> 1.8.1)
@ -497,7 +497,7 @@ DEPENDENCIES
rails_autolink
rb-readline (~> 0.5.1)
rdoc (>= 2.4.2)
reform
reform (~> 1.0.4)
representable!
request_store
roar (~> 0.12.6)

@ -15,7 +15,7 @@ angular.module('openproject.api')
url: PathHelper.apiV3 + '/' + uri,
});
}
}
};
return HALAPIResource;
}]);

@ -39,6 +39,14 @@ angular.module('openproject.config')
userPreferencesPresent: function() {
return this.settingsPresent() && gon.settings.hasOwnProperty('user_preferences');
},
displaySettingsPresent: function() {
return this.settingsPresent() && gon.settings.hasOwnProperty('display');
},
displaySettingPresent: function(setting) {
return this.displaySettingsPresent()
&& gon.settings.display.hasOwnProperty(setting)
&& gon.settings.display[setting] != false;
},
accessibilityModeEnabled: function() {
if (!this.userPreferencesPresent()) {
$log.error('User preferences are not available.');
@ -54,6 +62,24 @@ angular.module('openproject.config')
} else {
return gon.settings.user_preferences.others.comments_sorting === 'desc';
}
},
isTimezoneSet: function() {
return this.userPreferencesPresent() && gon.settings.user_preferences.time_zone != '';
},
timezone: function() {
return (this.isTimezoneSet()) ? gon.settings.user_preferences.time_zone : '';
},
dateFormatPresent: function() {
return this.displaySettingPresent('date_format');
},
dateFormat: function() {
return gon.settings.display.date_format;
},
timeFormatPresent: function() {
return this.displaySettingPresent('time_format');
},
timeFormat: function() {
return gon.settings.display.time_format;
}
};
}]);

@ -28,7 +28,7 @@
angular.module('openproject.messages.controllers')
.controller('MessagesController', ['$scope', '$http', 'PathHelper', 'TimezoneService', 'SortService', 'PaginationService', function ($scope, $http, PathHelper, TimezoneService, SortService, PaginationService) {
.controller('MessagesController', ['$scope', '$http', 'PathHelper', 'SortService', 'PaginationService', function ($scope, $http, PathHelper, SortService, PaginationService) {
$scope.PathHelper = PathHelper;
$scope.messages = gon.messages;
$scope.totalMessageCount = gon.total_count;
@ -36,8 +36,6 @@ angular.module('openproject.messages.controllers')
$scope.projectId = gon.project_id;
$scope.activityModuleEnabled = gon.activity_modul_enabled;
TimezoneService.setTimezone(gon.timezone);
SortService.setColumn(gon.sort_column);
SortService.setDirection(gon.sort_direction);

@ -0,0 +1,56 @@
//-- 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.uiComponents')
.directive('activityComment', ['I18n', 'ActivityService', 'ConfigurationService', function(I18n, ActivityService, ConfigurationService) {
return {
restrict: 'E',
replace: true,
scope: {
workPackage: '=',
activities: '='
},
templateUrl: '/templates/components/activity_comment.html',
link: function(scope, element, attrs) {
scope.title = I18n.t('js.label_add_comment_title');
scope.buttonTitle = I18n.t('js.label_add_comment');
scope.createComment = function() {
var comment = angular.element('#add-comment-text').val();
var descending = ConfigurationService.commentsSortedInDescendingOrder();
ActivityService.createComment(scope.workPackage.props.id, scope.activities, descending, comment)
.then(function(response){
angular.element('#add-comment-text').val('');
return response;
});
}
}
};
}]);

@ -46,7 +46,7 @@ angular.module('openproject.uiComponents')
scope.authorLink = '<a href="'+ PathHelper.userPath(scope.author.id) + '">' + scope.author.name + '</a>';
if (scope.activity) {
scope.timestamp = '<a title="' + time + '" href="' + PathHelper.activityPath(scope.project, createdOn.format('YYYY-MM-DD')) + '">' + timeago + '</a>';
scope.timestamp = '<a title="' + time + '" href="' + PathHelper.activityFromPath(scope.project, createdOn.format('YYYY-MM-DD')) + '">' + timeago + '</a>';
} else {
scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>';
}

@ -29,18 +29,47 @@
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('date', ['I18n', function(I18n) {
.directive('date', ['I18n', 'TimezoneService', 'ConfigurationService', function(I18n, TimezoneService, ConfigurationService) {
return {
restrict: 'EA',
replace: false,
scope: { date: '=' },
template: '<span>{{date}}</span>',
replace: true,
scope: { dateValue: '=', hideTitle: '@' },
template: '<span title="{{ dateTitle }}">{{date}}</span>',
link: function(scope, element, attrs) {
moment.lang(I18n.locale);
scope.date = TimezoneService.formattedDate(scope.dateValue);
if (!scope.hideTitle) {
scope.dateTitle = scope.date;
}
}
};
}])
.directive('time', ['I18n', 'TimezoneService', 'ConfigurationService', function(I18n, TimezoneService, ConfigurationService) {
return {
restrict: 'EA',
replace: true,
scope: { timeValue: '=', hideTitle: '@' },
template: '<span title="{{ timeTitle }}">{{time}}</span>',
link: function(scope, element, attrs) {
scope.time = TimezoneService.formattedTime(scope.timeValue);
if (!scope.hideTitle) {
scope.timeTitle = scope.time;
}
}
};
}])
.directive('dateTime', function($compile) {
return {
restrict: 'EA',
replace: true,
scope: { dateTimeValue: '=' },
template: '<span title="{{ date }} {{ time }}"><date date-value="dateTimeValue" hide-title="true"></date> <time time-value="dateTimeValue" hide-title="true"></time></span>',
link: function(scope, element, attrs) {
scope.date = TimezoneService.formattedDate(scope.dateTimeValue);
scope.time = TimezoneService.formattedTime(scope.dateTimeValue);
// TODO: The timezone of scope.time is UTC. Thus, we need to adapt the
// time to the local timezone or user setting.
scope.time = moment(scope.dateTime).utc().format('LL');
$compile(element.contents())(scope);
}
};
}]);
});

@ -26,19 +26,10 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('formattedDate', ['I18n', 'TimezoneService', function(I18n, TimezoneService) {
return {
restrict: 'EA',
replace: false,
scope: { formattedDate: '=' },
template: '<span>{{time}}</span>',
link: function(scope, element, attrs) {
moment.lang(I18n.locale);
scope.time = TimezoneService.parseDate(scope.formattedDate).format('LLL');
.filter('latestItems', function(){
return function(items, isDescending, visible){
return isDescending ? items.slice(0, visible) : items.slice(-visible).reverse();
}
};
}]);
});

@ -36,7 +36,19 @@ angular.module('openproject.helpers')
apiV3: '/api/v3',
staticBase: window.appBasePath ? window.appBasePath : '',
activityPath: function(projectIdentifier, from) {
activityPath: function(activityId) {
return 'activities/' + activityId;
},
activitiesPath: function(workPackageId) {
var path = '';
if(workPackageId) {
path = '/work_packages/' + workPackageId;
}
path = path + '/activities';
return path;
},
activityFromPath: function(projectIdentifier, from) {
var link = '/activity';
if (projectIdentifier) {
@ -52,6 +64,9 @@ angular.module('openproject.helpers')
assetPath: function(assetIdentifier) {
return '/assets/' + assetIdentifier;
},
attachmentPath: function(attachmentId, fileName) {
return '/attachments/' + attachmentId + '/' + fileName;
},
boardsPath: function(projectIdentifier) {
return PathHelper.projectPath(projectIdentifier) + '/boards';
},
@ -220,6 +235,9 @@ angular.module('openproject.helpers')
},
// Static
staticAttachmentPath: function(attachmentId, fileName) {
return PathHelper.staticBase + PathHelper.attachmentPath(attachmentId, fileName);
},
staticUserPath: function(userId) {
return PathHelper.staticBase + PathHelper.userPath(userId);
},

@ -123,8 +123,50 @@ angular.module('openproject.workPackages.helpers')
parseDateTime: function(value) {
return new Date(Date.parse(value.replace(/(A|P)M$/, '')));
},
getParent: function(workPackage) {
var wpParent = workPackage.links.parent;
return (wpParent) ? [wpParent.fetch()] : [];
},
getChildren: function(workPackage) {
var children = workPackage.links.children;
var result = [];
if (children) {
for (var x = 0; x < children.length; x++) {
var child = children[x];
result.push(child.fetch());
}
}
return result;
},
getRelationsOfType: function(workPackage, type) {
var self = workPackage.links.self.href;
var relations = workPackage.embedded.relations;
var result = [];
if (relations) {
for (var x = 0; x < relations.length; x++) {
var relation = relations[x];
if (relation.props._type == type) {
if (relation.links.relatedTo.href == self) {
result.push(relation.links.relatedFrom.fetch());
} else {
result.push(relation.links.relatedTo.fetch());
}
}
}
}
return result;
}
};
return WorkPackagesHelper;

@ -187,7 +187,7 @@ angular.module('openproject.models')
*/
setDefaultFilter: function() {
var statusOpenFilterData = this.getExtendedFilterData({name: 'status_id', operator: 'o'});
this.filters = new Array(new Filter(statusOpenFilterData));
this.filters = [new Filter(statusOpenFilterData)];
},
/**

@ -51,7 +51,7 @@
// environment and other global vars
/*jshint browser:true, devel:true*/
/*global jQuery:false, Raphael:false, Timeline:true*/
/*global angular:false, Timeline:true*/
angular.module('openproject.timelines.models')

@ -71,6 +71,7 @@ angular.module('openproject')
})
.state('work-packages.list.details.overview', {
url: "/overview",
controller: 'DetailsTabOverviewController',
templateUrl: "/templates/work_packages/tabs/overview.html",
})
.state('work-packages.list.details.activity', {
@ -83,7 +84,8 @@ angular.module('openproject')
})
.state('work-packages.list.details.watchers', {
url: "/watchers",
templateUrl: "/templates/work_packages/tabs/watchers.html",
controller: 'DetailsTabWatchersController',
templateUrl: "/templates/work_packages/tabs/watchers.html"
})
.state('work-packages.list.details.attachments', {
url: "/attachments",

@ -0,0 +1,74 @@
//-- 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.services')
.service('ActivityService', ['HALAPIResource',
'$http',
'PathHelper', function(HALAPIResource, $http, PathHelper){
var ActivityService = {
createComment: function(workPackageId, activities, descending, comment) {
var resource = HALAPIResource.setup(PathHelper.activitiesPath(workPackageId));
var options = {
ajax: {
method: "POST",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
// We are unable to add to the work package's embedded activities directly
if(activity) {
if(descending){
activities.unshift(activity);
} else {
activities.push(activity);
}
return activity;
}
});
},
updateComment: function(activityId, comment) {
var resource = HALAPIResource.setup(PathHelper.activityPath(activityId));
var options = {
ajax: {
method: "PUT",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
return activity;
});
}
}
return ActivityService;
}]);

@ -28,23 +28,46 @@
angular.module('openproject.services')
.service('TimezoneService', [function() {
var timezoneOptions = {
name: ''
};
.service('TimezoneService', ['ConfigurationService', 'I18n', function(ConfigurationService, I18n) {
TimezoneService = {
setTimezone: function(name) {
timezoneOptions.name = name;
},
parseDate: function(date) {
var d = moment.utc(date, "MM/DD/YYYY/ HH:mm A");
var d = moment.utc(date);
if (timezoneOptions.name) {
d.tz(timezoneOptions.name);
if (ConfigurationService.isTimezoneSet()) {
d.local();
d.tz(ConfigurationService.timezone());
}
return d;
},
formattedDate: function(date) {
var date;
if (ConfigurationService.dateFormatPresent()) {
date = TimezoneService.parseDate(date).format(ConfigurationService.dateFormat());
} else {
moment.lang(I18n.locale);
date = TimezoneService.parseDate(date).format('L');
}
return date;
},
formattedTime: function(date) {
var time;
if (ConfigurationService.timeFormatPresent()) {
time = TimezoneService.parseDate(date).format(ConfigurationService.timeFormat());
} else {
moment.lang(I18n.locale);
time = TimezoneService.parseDate(date).format('LT');
}
return time;
},
};
return TimezoneService;

@ -0,0 +1,155 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.controllers')
.constant('DEFAULT_WORK_PACKAGE_PROPERTIES', [
'status', 'assignee', 'responsible',
'date', 'percentageDone', 'priority',
'estimatedTime', 'versionName'
])
.constant('USER_TYPE', 'user')
.controller('DetailsTabOverviewController', [
'$scope',
'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES',
'USER_TYPE',
'CustomFieldHelper',
'WorkPackagesHelper',
'UserService',
'$q',
function($scope, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, CustomFieldHelper, WorkPackagesHelper, UserService, $q) {
// work package properties
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
function getPropertyValue(property, format) {
if (format === USER_TYPE) {
return $scope.workPackage.embedded[property];
} else {
return getFormattedPropertyValue(property);
}
}
function getFormattedPropertyValue(property) {
if (property === 'date') {
return getDateProperty();
} else {
return WorkPackagesHelper.formatWorkPackageProperty($scope.workPackage.props[property], property);
}
}
function getDateProperty() {
if ($scope.workPackage.props.startDate || $scope.workPackage.props.dueDate) {
var displayedStartDate = WorkPackagesHelper.formatWorkPackageProperty($scope.workPackage.props.startDate, 'startDate') || I18n.t('js.label_no_start_date'),
displayedEndDate = WorkPackagesHelper.formatWorkPackageProperty($scope.workPackage.props.dueDate, 'dueDate') || I18n.t('js.label_no_due_date');
return displayedStartDate + ' - ' + displayedEndDate;
}
}
function addFormattedValueToPresentProperties(property, label, value, format) {
var propertyData = {
property: property,
label: label,
format: format,
value: null
};
$q.when(value).then(function(value) {
propertyData.value = value;
});
$scope.presentWorkPackageProperties.push(propertyData);
}
function secondRowToBeDisplayed() {
return !!workPackageProperties
.slice(3, 6)
.map(function(property) {
return $scope.workPackage.props[property];
})
.reduce(function(a, b) {
return a || b;
});
}
var userFields = ['assignee', 'author', 'responsible'];
(function setupWorkPackageProperties() {
angular.forEach(workPackageProperties, function(property, index) {
var label = I18n.t('js.work_packages.properties.' + property),
format = userFields.indexOf(property) === -1 ? 'text' : USER_TYPE,
value = getPropertyValue(property, format);
if (!!value ||
index < 3 ||
index < 6 && secondRowToBeDisplayed()) {
addFormattedValueToPresentProperties(property, label, value, format);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
})();
function getCustomPropertyValue(customProperty) {
if (!!customProperty.value && customProperty.format === USER_TYPE) {
return UserService.getUser(customProperty.value);
} else {
return CustomFieldHelper.formatCustomFieldValue(customProperty.value, customProperty.format);
}
}
(function setupCustomProperties() {
angular.forEach($scope.workPackage.props.customProperties, function(customProperty) {
var property = customProperty.name,
label = customProperty.name,
value = getCustomPropertyValue(customProperty),
format = customProperty.format;
if (customProperty.value) {
addFormattedValueToPresentProperties(property, label, value, format);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
})();
// toggles
$scope.toggleStates = {
hideFullDescription: true,
hideAllAttributes: true
};
}]);

@ -0,0 +1,92 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.controllers')
.controller('DetailsTabWatchersController', ['$scope', 'workPackage', function($scope, workPackage) {
// available watchers
$scope.$watch('watchers.length', fetchAvailableWatchers); fetchAvailableWatchers();
/**
* @name getResourceIdentifier
* @function
*
* @description
* Returns the resource identifier of an API resource retrieved via hyperagent
*
* @param {Object} resource The resource object
*
* @returns {String} identifier
*/
function getResourceIdentifier(resource) {
// TODO move to helper
return resource.links.self.href;
}
/**
* @name getFilteredCollection
* @function
*
* @description
* Filters collection of HAL resources by entries listed in resourcesToBeFilteredOut
*
* @param {Array} collection Array of resources retrieved via hyperagend
* @param {Array} resourcesToBeFilteredOut Entries to be filtered out
*
* @returns {Array} filtered collection
*/
function getFilteredCollection(collection, resourcesToBeFilteredOut) {
return collection.filter(function(resource) {
return resourcesToBeFilteredOut.map(getResourceIdentifier).indexOf(getResourceIdentifier(resource)) === -1;
});
}
function fetchAvailableWatchers() {
$scope.workPackage.links.availableWatchers
.fetch()
.then(function(data) {
// Temporarily filter out watchers already assigned to the work package on the client-side
$scope.availableWatchers = getFilteredCollection(data.embedded.availableWatchers, $scope.watchers);
// TODO do filtering on the API side and replace the update of the available watchers with the code provided in the following line
// $scope.availableWatchers = data.embedded.availableWatchers;
});
}
$scope.addWatcher = function(id) {
$scope.workPackage.link('addWatcher', {user_id: id})
.fetch({ajax: {method: 'POST'}})
.then($scope.refreshWorkPackage, $scope.outputError);
};
$scope.deleteWatcher = function(watcher) {
watcher.links.removeWatcher
.fetch({ ajax: watcher.links.removeWatcher.props })
.then($scope.refreshWorkPackage, $scope.outputError);
};
}]);

@ -28,158 +28,118 @@
angular.module('openproject.workPackages.controllers')
.constant('DEFAULT_WORK_PACKAGE_PROPERTIES', [
'status', 'assignee', 'responsible',
'date', 'percentageDone', 'priority',
'estimatedTime', 'versionName'
])
.constant('USER_TYPE', 'user')
.constant('VISIBLE_LATEST')
.constant('RELATION_TYPES', {
relatedTo: "Relation::Relates",
duplicates: "Relation::Duplicates",
duplicated: "Relation::Duplicated",
blocks: "Relation::Blocks",
blocked: "Relation::Blocked",
precedes: "Relation::Precedes",
follows: "Relation::Follows"
})
.controller('WorkPackageDetailsController', [
'$scope',
'latestTab',
'workPackage',
'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES',
'USER_TYPE',
'CustomFieldHelper',
'WorkPackagesHelper',
'PathHelper',
'UserService',
'VISIBLE_LATEST',
'RELATION_TYPES',
'$q',
'WorkPackagesHelper',
'ConfigurationService',
function($scope, latestTab, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, CustomFieldHelper, WorkPackagesHelper, PathHelper, UserService, $q, ConfigurationService) {
function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, $q, WorkPackagesHelper, ConfigurationService) {
$scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name);
});
$scope.$on('workPackageRefreshRequired', function(event, toState){
refreshWorkPackage();
});
// initialization
setWorkPackageScopeProperties(workPackage);
$scope.I18n = I18n;
$scope.workPackage = workPackage;
$scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id;
$scope.maxDescriptionLength = 800;
function refreshWorkPackage() {
workPackage.links.self
.fetch({force: true})
.then(setWorkPackageScopeProperties);
}
$scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers
// resources for tabs
// activities and latest activities
function outputError(error) {
$scope.$emit('flashMessage', {
isError: true,
text: error.message
});
}
$scope.outputError = outputError; // expose to child controllers
$scope.activities = workPackage.embedded.activities;
$scope.activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
function setWorkPackageScopeProperties(workPackage){
$scope.workPackage = workPackage;
$scope.latestActitivies = $scope.activities.reverse().slice(0, 3); // this leaves the activities in reverse order
$scope.isWatched = !!workPackage.links.unwatch;
$scope.toggleWatchLink = workPackage.links.watch === undefined ? workPackage.links.unwatch : workPackage.links.watch;
$scope.watchers = workPackage.embedded.watchers;
// activities and latest activities
$scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
// restore former order of actvities unless comments are to be sorted in descending order
if (!$scope.activitiesSortedInDescendingOrder) {
$scope.activities.reverse();
}
$scope.activities = displayedActivities($scope.workPackage);
// watchers
$scope.watchers = workPackage.embedded.watchers;
$scope.author = workPackage.embedded.author;
// work package properties
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
// Attachments
$scope.attachments = workPackage.embedded.attachments;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
function getPropertyValue(property, format) {
if (format === USER_TYPE) {
return workPackage.embedded[property];
} else {
return getFormattedPropertyValue(property);
}
}
// relations
$q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parent) {
$scope.wpParent = parent;
});
$q.all(WorkPackagesHelper.getChildren(workPackage)).then(function(children) {
$scope.wpChildren = children;
});
function getFormattedPropertyValue(property) {
if (property === 'date') {
return getDateProperty();
} else {
return WorkPackagesHelper.formatWorkPackageProperty(workPackage.props[property], property);
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;
});
})(key);
}
}
function getDateProperty() {
if (workPackage.props.startDate || workPackage.props.dueDate) {
var displayedStartDate = WorkPackagesHelper.formatWorkPackageProperty(workPackage.props.startDate, 'startDate') || I18n.t('js.label_no_start_date'),
displayedEndDate = WorkPackagesHelper.formatWorkPackageProperty(workPackage.props.dueDate, 'dueDate') || I18n.t('js.label_no_due_date');
return displayedStartDate + ' - ' + displayedEndDate;
}
// Author
$scope.author = workPackage.embedded.author;
}
function addFormattedValueToPresentProperties(property, label, value, format) {
var propertyData = {
property: property,
label: label,
format: format,
value: null
$scope.toggleWatch = function() {
$scope.toggleWatchLink
.fetch({ ajax: $scope.toggleWatchLink.props })
.then(refreshWorkPackage, outputError);
};
$q.when(value).then(function(value) {
propertyData.value = value;
});
$scope.presentWorkPackageProperties.push(propertyData);
}
function secondRowToBeDisplayed() {
return !!workPackageProperties
.slice(3, 6)
.map(function(property) {
return workPackage.props[property];
})
.reduce(function(a, b) {
return a || b;
});
}
var userFields = ['assignee', 'author', 'responsible'];
(function setupWorkPackageProperties() {
angular.forEach(workPackageProperties, function(property, index) {
var label = I18n.t('js.work_packages.properties.' + property),
format = userFields.indexOf(property) === -1 ? 'text' : USER_TYPE,
value = getPropertyValue(property, format);
if (!!value ||
index < 3 ||
index < 6 && secondRowToBeDisplayed()) {
addFormattedValueToPresentProperties(property, label, value, format);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
})();
$scope.canViewWorkPackageWatchers = function() {
return !!($scope.workPackage && $scope.workPackage.embedded.watchers !== undefined);
};
function getCustomPropertyValue(customProperty) {
if (!!customProperty.value && customProperty.format === USER_TYPE) {
return UserService.getUser(customProperty.value);
} else {
return CustomFieldHelper.formatCustomFieldValue(customProperty.value, customProperty.format);
function displayedActivities(workPackage) {
var activities = workPackage.embedded.activities;
activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
if ($scope.activitiesSortedInDescendingOrder) {
activities.reverse();
}
return activities;
}
(function setupCustomProperties() {
angular.forEach(workPackage.props.customProperties, function(customProperty) {
var property = customProperty.name,
label = customProperty.name,
value = getCustomPropertyValue(customProperty),
format = customProperty.format;
if (customProperty.value) {
addFormattedValueToPresentProperties(property, label, value, format);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
})();
// toggles
$scope.toggleStates = {

@ -60,19 +60,23 @@ angular.module('openproject.workPackages.controllers')
$scope.disableFilters = false;
$scope.disableNewWorkPackage = true;
var getWorkPackages, params;
var fetchWorkPackages;
if($scope.query_id){
getWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id);
fetchWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id);
} else {
getWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location);
fetchWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location);
}
$scope.settingUpPage = getWorkPackages.then(setupPage);
loadProjectTypesAndQueries();
$scope.settingUpPage = fetchWorkPackages // put promise in scope for cg-busy
.then(setupPage)
.then(function() {
fetchAvailableColumns();
fetchProjectTypesAndQueries();
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
});
}
function loadProjectTypesAndQueries() {
function fetchProjectTypesAndQueries() {
if ($scope.projectIdentifier) {
ProjectService.getProject($scope.projectIdentifier)
.then(function(project) {
@ -82,16 +86,12 @@ angular.module('openproject.workPackages.controllers')
});
}
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
}
function setupPage(json) {
initQuery(json.meta);
setupWorkPackagesTable(json);
initAvailableColumns();
if (json.work_packages.length) {
$scope.preselectedWorkPackageId = json.work_packages[0].id;
}
@ -153,7 +153,7 @@ angular.module('openproject.workPackages.controllers')
AuthorisationService.initModelAuth("query", meta.query._links);
}
function initAvailableColumns() {
function fetchAvailableColumns() {
return QueryService.loadAvailableUnusedColumns($scope.projectIdentifier)
.then(function(data){
$scope.availableUnusedColumns = data;
@ -187,7 +187,7 @@ angular.module('openproject.workPackages.controllers')
$scope.setQueryState = function(query_id) {
$state.go('work-packages.list', { query_id: query_id });
}
};
// More

@ -0,0 +1,48 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentFileSize', [function(){
return {
restrict: 'A',
replace: false,
templateUrl: '/templates/work_packages/tabs/_attachment_file_size.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.displayFileSize = "(" + formattedFileSize(scope.attachment.props.fileSize) + ")";
function formattedFileSize(fileSize) {
var size = parseFloat(fileSize);
return isNaN(size) ? "0kB" : (size / 1000).toFixed(2) + "kB";
};
}
};
}]);

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentTitleCell', ['PathHelper', function(PathHelper){
return {
restrict: 'A',
replace: false,
templateUrl: '/templates/work_packages/tabs/_attachment_title_cell.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.attachmentPath = PathHelper.staticAttachmentPath(scope.attachment.props.id, scope.attachment.props.fileName);
}
};
}]);

@ -0,0 +1,47 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentUserCell', ['PathHelper', function(PathHelper){
return {
restrict: 'A',
templateUrl: '/templates/work_packages/tabs/_attachment_user_cell.html',
scope: {
attachment: '='
},
link: function(scope, element, attributes) {
scope.attachment.links.author.fetch()
.then(function(author){
scope.authorName = author.props.name;
scope.authorId = author.props.id;
scope.userPath = PathHelper.staticUserPath(author.props.id);
});
}
};
}]);

@ -0,0 +1,42 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentsTable', ['PathHelper', 'I18n', function(PathHelper, I18n){
return {
restrict: 'E',
templateUrl: '/templates/work_packages/tabs/_attachments_table.html',
scope: {
attachments: '='
},
link: function(scope) {
scope.I18n = I18n;
}
};
}]);

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('attachmentsTitle', [function(){
return {
restrict: 'E',
replace: true,
templateUrl: '/templates/work_packages/tabs/_attachments_title.html',
scope: {
attachments: '='
},
link: function(scope, element, attributes) {
scope.attachmentsTitle = "Attachments (" + scope.attachments.length + ")";
}
};
}]);

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.directives')
.directive('editableComment', [function(){
return {
restrict: 'A',
scope: {
activity: '=',
commentInEdit: '='
},
templateUrl: '/templates/work_packages/tabs/_editable_comment.html',
link: function(scope){
}
};
}]);

@ -0,0 +1,50 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.tabs')
.directive('exclusiveEdit', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
template: '<div class="exclusive-edit" ng-transclude></div>',
controller: function() {
var editors = [];
this.gotEditable = function(selectedEditor) {
angular.forEach(editors, function(editor) {
if (selectedEditor != editor) {
editor.inEdit = false; }
});
};
this.addEditable = function(editor) {
editors.push(editor);
};
}
};
})

@ -28,25 +28,68 @@
angular.module('openproject.workPackages.tabs')
.directive('userActivity', ['I18n', 'PathHelper', function(I18n, PathHelper) {
.directive('userActivity', ['$uiViewScroll', 'I18n', 'PathHelper', 'ActivityService', function($uiViewScroll, I18n, PathHelper, ActivityService) {
return {
restrict: 'E',
replace: true,
require: '^?exclusiveEdit',
templateUrl: '/templates/work_packages/tabs/_user_activity.html',
scope: {
activity: '=',
currentAnchor: '=',
activityNo: '='
activityNo: '=',
inputElementId: '='
},
link: function(scope) {
link: function(scope, element, attrs, exclusiveEditController) {
exclusiveEditController.addEditable(scope);
scope.I18n = I18n;
scope.userPath = PathHelper.staticUserPath;
scope.inEdit = false;
scope.inFocus = false;
scope.activity.links.user.fetch().then(function(user) {
scope.userId = user.props.id;
scope.userName = user.props.name;
scope.userAvatar = user.props.avatar;
});
scope.editComment = function() {
scope.inEdit = true;
exclusiveEditController.gotEditable(scope);
};
scope.cancelEdit = function() {
scope.inEdit = false;
};
scope.quoteComment = function() {
var elem = angular.element('#' + scope.inputElementId);
elem.val(quotedText(scope.activity.props.rawComment));
$uiViewScroll(elem);
};
scope.updateComment = function(comment) {
var comment = angular.element('#edit-comment-text').val();
ActivityService.updateComment(scope.activity.props.id, comment).then(function(activity){
scope.$emit('workPackageRefreshRequired', '');
scope.inEdit = false;
});
};
scope.showActions = function() {
scope.inFocus = true;
};
scope.hideActions = function() {
scope.inFocus = false;
};
function quotedText(rawComment) {
quoted = rawComment.split("\n")
.map(function(line){ return "\n> " + line; })
.join('');
return scope.userName + " wrote:" + quoted;
}
}
};
}]);

@ -0,0 +1,81 @@
//-- 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.uiComponents')
.directive('workPackageRelation', [
'I18n',
'PathHelper',
'WorkPackagesHelper',
function(I18n, PathHelper, WorkPackagesHelper) {
return {
restrict: 'E',
replace: true,
scope: { title: '@', relatedWorkPackages: '=', btnTitle: '@buttonTitle', btnIcon: '@buttonIcon', isSingletonRelation: '@singletonRelation' },
templateUrl: '/templates/work_packages/tabs/_work_package_relation.html',
link: function(scope, element, attrs) {
scope.I18n = I18n;
scope.WorkPackagesHelper = WorkPackagesHelper;
scope.workPackagePath = PathHelper.staticWorkPackagePath;
scope.userPath = PathHelper.staticUserPath;
var setExpandState = function() {
scope.expand = scope.relatedWorkPackages && scope.relatedWorkPackages.length > 0;
};
scope.$watch('relatedWorkPackages', function() {
setExpandState();
});
scope.collapseStateIcon = function(collapsed) {
var iconClass = 'icon-arrow-right5-';
if (collapsed) {
iconClass += '3';
} else {
iconClass += '2';
}
return iconClass;
}
scope.getFullIdentifier = function(workPackage) {
var id = '#' + workPackage.props.id;
if (workPackage.props.type) {
id += ' ' + workPackage.props.type + ':';
}
id += ' ' + workPackage.props.subject;
return id;
};
}
};
}]);

@ -43,6 +43,10 @@ See doc/COPYRIGHT.rdoc for more details.
cursor: pointer;
}
.action-icon {
cursor: pointer;
}
select.to-validate.ng-dirty.ng-valid, input.to-validate.ng-dirty.ng-valid { border:1px solid Green; }
select.to-validate.ng-dirty.ng-invalid, input.to-validate.ng-dirty.ng-invalid { border:1px solid Red; }
select.to-validate.ng-dirty.ng-valid ~ span.ok, input.to-validate.ng-dirty.ng-valid ~ span.ok { color:green; display:inline; }

@ -137,3 +137,6 @@
.login-auth-provider-list
margin-top: -15px
margin-bottom: 10px
#top-menu #nav-login-content .login-auth-providers.no-pwd
margin-top: 0px

@ -0,0 +1,7 @@
# Components - Add comments - default
```
<div class="activity-comment">
<textarea class="add-comment-text" placeholder="Add your comments here" rows=1></textarea>
</div>
```

@ -0,0 +1,44 @@
/*-- 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. ++*/
.activity-comment
textarea
border: 1px solid #cacaca
background: #ffffff
border-radius: 2px
padding: 8px
font-family: $font_family_normal
font-size: $global_font_size
width: 100%
box-sizing: border-box
&:hover
border: 1px solid #aaaaaa
&:focus
border: 1px solid #aaaaaa
box-shadow: 1px 1px 1px #dddddd inset
.add-comment-text
resize: none

@ -0,0 +1,10 @@
# Components - Add comments - onclick
```
<div class="activity-comment">
<textarea class="add-comment-text-big" placeholder="Add your comments here" rows=4></textarea>
<button class="button"><i class="icon-yes icon-left"></i>Add comment</button><button class="button"><i class="icon-close icon-left"></i>Cancel</button>
</div>
```

@ -0,0 +1,28 @@
/*-- 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. ++*/

@ -0,0 +1,41 @@
/*-- 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. ++
*/
.activity-comment
textarea
width: 100%
height: 6em
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
box-sizing: border-box
-webkit-border-radius: 3px
-moz-border-radius: 3px
border-radius: 3px
.button
float: right
margin-right: 0px

@ -0,0 +1,87 @@
/*-- 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. ++
*/
.attachments-container
float: left
margin: 0 0 30px 0
width: 100%
ul
margin: 0
padding: 0
list-style-type: none
li
margin: 0
padding: 0
line-height: 20px
table
padding: 0
margin: 0px 0 10px 0
float: left
border-collapse: collapse
border: 0px solid #ddd
width: 100%
table-layout: fixed
tbody
tr
td
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
width: 10%
tr
&:hover
background: #ffffae
th
text-align: left
font-family: 'LatoBold'
font-weight: normal
text-transform: uppercase
background: #fff
padding: 6px 10px 6px 0
border-bottom: 2px solid #eee
td
text-align: left
font-weight: normal
border-bottom: 0px solid #ddd
padding: 6px 10px 6px 0
.add-file
float: left
padding: 8px 0 0 10px
i
font-size: 12px
padding: 0 2px 0 0
.upload-file
display: block
width: 100%
float: left
margin: 20px 0 0 0
padding: 20px 0 0 0
border-top: 1px solid #ddd

@ -0,0 +1,52 @@
/*-- copyright
* OpenProject is a project management system.
* Copyright (C) 2012-2013 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. ++
*/
.detail-panel-description
width: 100%
.detail-panel-description-content
.relation
h3
cursor: pointer
.content
.workpackages
table
width: 100%
table-layout: fixed
thead
font-weight: bold
text-transform: uppercase
line-height: 32px
tr
td
text-overflow: ellipsis
white-space: nowrap
overflow: hidden
&:first-of-type
width: 55%
&:last-of-type
width: 30%

@ -55,9 +55,15 @@
@import content/tabular
@import content/work_packages
@import content/work_packages_filters
@import content/work_packages_relations
@import content/work_packages_table
@import content/work_packages_details_activities
@import content/work_packages_details_attachments
@import content/expandable_group_content
@import content/control_colors
@import content/components_add_comments_default
@import content/components_add_comments_onclick
@import content/tabular
@import content/headings
@import content/timelines

@ -61,6 +61,9 @@
@import content/work_packages_table
@import content/expandable_group_content
@import content/control_colors
@import content/components_add_comments_default
@import content/components_add_comments_onclick
@import content/tabular
@import content/headings
@import content/timelines

@ -95,17 +95,27 @@ div
background: #ffffff
padding: 0 10px 0 0px
&.detail-panel-watchers
&#detail-panel-watchers
float: left
margin: 15px 0 0 0
width: 100%
ul
margin: 10px 0 0 0
padding: 0
margin: 15px 0
padding: 0 0 15px 0
list-style-type: none
li
margin: 0 0 30px 0
padding: 0
clear: both
padding-bottom: 10px
.user-field-user-link
float: left
.detail-panel-watchers-delete-watcher-icon
padding: 0 8px
color: $button_font_color
.avatar
padding-bottom: 10px
fieldset
border: 0
#detail-panel-watchers-add-watcher
clear: left
&.detail-panel-attributes
float: left
@ -121,6 +131,7 @@ div
padding: 0
display: inline-table
min-width: 31%
clear: left
label
font-weight: bold
@ -244,8 +255,10 @@ i
color: #ccc
margin: 0 0 0 -2px
cursor: pointer
&:hover
&:hover, &.active
color: #f8d033
&.active:hover
color: #ccc
#tabs
position: relative
@ -285,6 +298,9 @@ i
.work-package-details-activities-activity-contents
padding: 10px 0
textarea
width: 100%
resize: none
@media only screen and (max-width: 1280px)
.split-view .work-packages--details, .work-packages--split-view .work-packages--details

@ -380,6 +380,8 @@ a.tl-discreet-link:hover, input.icon:hover {
.tl-column {
left: 100px;
display: inline-block;
line-height: 18px;
padding-bottom: 8px;
}
#content .tl-word-ellipsis {

@ -27,11 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'concerns/omniauth_login'
class AccountController < ApplicationController
include CustomFieldsHelper
include OmniauthLogin
include Concerns::OmniauthLogin
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required
@ -44,6 +42,8 @@ class AccountController < ApplicationController
def login
if User.current.logged?
redirect_to home_url
elsif Concerns::OmniauthLogin.direct_login?
redirect_to Concerns::OmniauthLogin.direct_login_provider_url
elsif request.post?
authenticate_user
end
@ -57,7 +57,8 @@ class AccountController < ApplicationController
# Enable user to choose a new password
def lost_password
redirect_to(home_url) && return unless Setting.lost_password?
return redirect_to(home_url) unless allow_lost_password_recovery?
if params[:token]
@token = Token.find_by_action_and_value("recovery", params[:token].to_s)
redirect_to(home_url) && return unless @token and !@token.expired?
@ -102,9 +103,7 @@ class AccountController < ApplicationController
# User self-registration
def register
unless Setting.self_registration? || pending_auth_source_registration?
return self_registration_disabled
end
return self_registration_disabled unless allow_registration?
if request.get?
session[:auth_source_registration] = nil
@ -115,7 +114,7 @@ class AccountController < ApplicationController
@user.register
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if session[:auth_source_registration][:omniauth]
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
@ -123,16 +122,30 @@ class AccountController < ApplicationController
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting(@user)
register_user_according_to_setting @user
end
end
end
def allow_registration?
allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login?
get = request.get? && allow
post = request.post? && (session[:auth_source_registration] || allow)
get || post
end
def allow_lost_password_recovery?
Setting.lost_password? && !OpenProject::Configuration.disable_password_login?
end
# Token based account activation
def activate
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
return redirect_to(home_url) unless Setting.self_registration? && params[:token]
token = Token.find_by_action_and_value('register', params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
@ -149,6 +162,8 @@ class AccountController < ApplicationController
# to change the password.
# When making changes here, also check MyController.change_password
def change_password
return render_404 if OpenProject::Configuration.disable_password_login?
@user = User.find_by_login(params[:username])
@username = @user.login
@ -185,8 +200,12 @@ class AccountController < ApplicationController
end
def authenticate_user
if OpenProject::Configuration.disable_password_login?
render_404
else
password_authentication(params[:username], params[:password])
end
end
def password_authentication(username, password)
user = User.try_to_login(username, password)
@ -254,7 +273,11 @@ class AccountController < ApplicationController
end
def pending_auth_source_registration?
session[:auth_source_registration] && !session[:auth_source_registration][:omniauth]
session[:auth_source_registration] && !pending_omniauth_registration?
end
def pending_omniauth_registration?
Hash(session[:auth_source_registration])[:omniauth]
end
def register_and_login_via_authsource(user, session, permitted_params)

@ -30,12 +30,16 @@
# Differences being that it's not looking to the session and also existing
# queries will be augmented with the params data passed with them.
module Api::Experimental::Concerns::QueryLoading
private
def init_query
if !params[:query_id].blank?
@query = Query.find(params[:query_id])
@query.project = @project if @query.project.nil?
else
@query = Query.new({name: "_", :project => @project})
@query = Query.new({ name: "_", :project => @project },
:initialize_with_default_filter => no_query_params_provided?)
end
prepare_query
@query
@ -58,4 +62,8 @@ module Api::Experimental::Concerns::QueryLoading
# For the sake of not breaking from convention we encoding/decoding the sortation.
params[:sort].split(',').collect{|p| [p.split(':')[0], p.split(':')[1] || 'asc']}
end
def no_query_params_provided?
(params.keys & %w(group_by c fields f sort is_public name page per_page display_sums)).empty?
end
end

@ -39,6 +39,7 @@ class BoardsController < ApplicationController
include SortHelper
include WatchersHelper
include PaginationHelper
include OpenProject::ClientPreferenceExtractor
def index
@boards = @project.boards
@ -67,7 +68,7 @@ class BoardsController < ApplicationController
gon.sort_column = 'updated_on'
gon.sort_direction = 'desc'
gon.total_count = @board.topics.count
gon.timezone = User.current.time_zone ? ActiveSupport::TimeZone::MAPPING[User.current.time_zone.name] : ""
gon.settings = client_preferences
@message = Message.new
render :action => 'show', :layout => !request.xhr?

@ -1,6 +1,11 @@
##
# Intended to be used by the AccountController to handle omniauth logins
module OmniauthLogin
module Concerns::OmniauthLogin
def self.included(base)
# disable CSRF protection since that should be covered by the omniauth strategy
base.skip_before_filter :verify_authenticity_token, :only => [:omniauth_login]
end
def omniauth_login
auth_hash = request.env['omniauth.auth']
@ -24,6 +29,28 @@ module OmniauthLogin
redirect_to :action => 'login'
end
def self.direct_login?
direct_login_provider.is_a? String
end
##
# Per default the user may choose the usual password login as well as several omniauth providers
# on the login page and in the login drop down menu.
#
# With his configuration option you can set a specific omniauth provider to be
# used for direct login. Meaning that the login provider selection is skipped and
# the configured provider is used directly instead.
#
# If this option is active /login will lead directly to the configured omniauth provider
# and so will a click on 'Sign in' (as opposed to opening the drop down menu).
def self.direct_login_provider
OpenProject::Configuration['omniauth_direct_login_provider']
end
def self.direct_login_provider_url
"/auth/#{direct_login_provider}" if direct_login?
end
private
# a user may login via omniauth and (if that user does not exist

@ -91,6 +91,8 @@ class MyController < ApplicationController
# When making changes here, also check AccountController.change_password
def change_password
return render_404 if OpenProject::Configuration.disable_password_login?
@user = User.current # required by "my" layout
@username = @user.login
return if redirect_if_password_change_not_allowed_for(@user)

@ -29,6 +29,7 @@
class SysController < ActionController::Base
before_filter :check_enabled
before_filter :require_basic_auth, :only => [ :repo_auth ]
def projects
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
@ -70,6 +71,19 @@ class SysController < ActionController::Base
render :nothing => true, :status => 404
end
def repo_auth
@project = Project.find_by_identifier(params[:repository])
if ( %w(GET PROPFIND REPORT OPTIONS).include?(params[:method]) &&
@authenticated_user.allowed_to?(:browse_repository, @project) ) ||
@authenticated_user.allowed_to?(:commit_access, @project)
render :text => "Access granted"
return
end
render :text => "Not allowed", :status => 403 # default to deny
end
protected
def check_enabled
@ -79,4 +93,37 @@ class SysController < ActionController::Base
return false
end
end
private
def require_basic_auth
authenticate_with_http_basic do |username, password|
@authenticated_user = cached_user_login(username, password)
return true if @authenticated_user
end
response.headers["WWW-Authenticate"] = 'Basic realm="Repository Authentication"'
render :text => "Authorization required", :status => 401
false
end
def user_login(username, password)
User.try_to_login(username, password)
end
def cached_user_login(username, password)
unless Setting.repository_authentication_caching_enabled?
return user_login(username, password)
end
user = nil
user_id = Rails.cache.fetch(OpenProject::RepositoryAuthentication::CACHE_PREFIX + Digest::SHA1.hexdigest("#{username}#{password}"),
:expires_in => OpenProject::RepositoryAuthentication::CACHE_EXPIRES_AFTER) do
user = user_login(username, password)
user ? user.id.to_s : '-1'
end
return nil if user_id.blank? or user_id == '-1'
user || User.find_by_id(user_id.to_i)
end
end

@ -42,6 +42,7 @@ class TimelogController < ApplicationController
include TimelogHelper
include CustomFieldsHelper
include PaginationHelper
include OpenProject::ClientPreferenceExtractor
def index
sort_init 'spent_on', 'desc'
@ -76,6 +77,7 @@ class TimelogController < ApplicationController
gon.sort_column = 'spent_on'
gon.sort_direction = 'desc'
gon.total_count = total_entry_count(cond)
gon.settings = client_preferences
render :layout => !request.xhr?
}

@ -49,6 +49,7 @@ class WorkPackagesController < ApplicationController
include PaginationHelper
include SortHelper
include OpenProject::Concerns::Preview
include OpenProject::ClientPreferenceExtractor
accept_key_auth :index, :show, :create, :update
@ -204,9 +205,7 @@ class WorkPackagesController < ApplicationController
respond_to do |format|
format.html do
gon.settings = {
user_preferences: current_user.pref
}
gon.settings = client_preferences
render :index, :locals => { :query => @query,
:project => @project },

@ -0,0 +1,37 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module ContentForHelper
# Thanks to http://blog.plataformatec.com.br/2012/07/flushing-content-blocks-with-rails-4/
# TODO: This method becomes obsolete with Rails 4 and the 'flush' parameter
def single_content_for(name, content = nil, &block)
@view_flow.set(name, ActiveSupport::SafeBuffer.new)
content_for(name, content, &block)
end
end

@ -32,6 +32,8 @@ class UserMailer < ActionMailer::Base
:work_packages, # for css classes
:custom_fields # for show_value
include OpenProject::LocaleHelper
# wrap in a lambda to allow changing at run-time
default :from => Proc.new { Setting.mail_from }
@ -392,11 +394,6 @@ private
headers['References'] = "<#{self.class.generate_message_id(object, user)}>"
end
def with_locale_for(user, &block)
locale = user.language.presence || Setting.default_language.presence || I18n.default_locale
I18n.with_locale(locale, &block)
end
# Prepends given fields with 'X-OpenProject-' to save some duplication
def open_project_headers(hash)
hash.each { |key, value| headers["X-OpenProject-#{key}"] = value.to_s }

@ -33,13 +33,16 @@ class CopyProjectJob < Struct.new(:user,
:enabled_modules,
:associations_to_copy,
:send_mails)
include OpenProject::LocaleHelper
def perform
target_project, errors = create_project_copy(source_project,
target_project, errors = with_locale_for(user) do
create_project_copy(source_project,
target_project_params,
enabled_modules,
associations_to_copy,
send_mails)
end
if target_project
UserMailer.delay.copy_project_succeeded(user, source_project, target_project, errors)

@ -251,7 +251,7 @@ class User < Principal
# Tries to authenticate a user in the database via external auth source
# or password stored in the database
def self.try_authentication_for_existing_user(user, password)
return nil if !user.active?
return nil if !user.active? || OpenProject::Configuration.disable_password_login?
if user.auth_source
# user has an external authentication method
return nil unless user.auth_source.authenticate(user.login, password)
@ -266,6 +266,8 @@ class User < Principal
# Tries to authenticate with available sources and creates user on success
def self.try_authentication_and_create_user(login, password)
return nil if OpenProject::Configuration.disable_password_login?
user = nil
attrs = AuthSource.authenticate(login, password)
if attrs
@ -362,7 +364,8 @@ class User < Principal
# Does the backend storage allow this user to change their password?
def change_password_allowed?
return false if uses_external_authentication?
return false if uses_external_authentication? ||
OpenProject::Configuration.disable_password_login?
return true if auth_source_id.blank?
return auth_source.allow_password_changes?
end

@ -150,7 +150,7 @@ class WorkPackage < ActiveRecord::Base
# test_destroying_root_projects_should_clear_data #
# for details. #
###################################################
acts_as_attachable :after_remove => :attachments_changed
acts_as_attachable :after_remove => :attachments_changed, :order => "#{Attachment.table_name}.filename"
after_validation :set_attachments_error_details, if: lambda {|work_package| work_package.errors.messages.has_key? :attachments}

@ -8,12 +8,16 @@
# * https://www.openproject.org/work_packages/7192
# * http://stackoverflow.com/questions/13112430/find-loaded-providers-for-omniauth
auth_provider_html = call_hook :view_account_login_auth_provider
no_pwd = OpenProject::Configuration.disable_password_login?
pclass = no_pwd ? 'no-pwd' : ''
%>
<% if auth_provider_html.strip != '' %>
<div class="login-auth-providers">
<div class="login-auth-providers <%= pclass %>">
<% unless no_pwd %>
<h3 class="login-auth-providers-title"><span>
<%= I18n.t('account.login_with_auth_provider')%>
</span></h3>
<% end %>
<div class="login-auth-provider-list">
<%= auth_provider_html %>
</div>

@ -0,0 +1,32 @@
<%#-- 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.
++#%>
<div id="nav-login-content">
<%= render :partial => 'account/auth_providers' %>
</div>

@ -0,0 +1,63 @@
<%#-- 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.
++#%>
<%= form_tag({:action=> "login"}, autocomplete: 'off') do %>
<%= back_url_hidden_field_tag %>
<div class="attribute_wrapper">
<label for="username"><%= User.human_attribute_name :login %></label>
<%= text_field_tag 'username', nil %>
</div>
<div class="attribute_wrapper">
<label for="password"><%= User.human_attribute_name :password %></label>
<%= password_field_tag 'password', nil %>
</div>
<div class="login-options-container">
<div class="login-links">
<% if Setting.lost_password? %>
<%= link_to l(:label_password_lost), :controller => '/account', :action => 'lost_password' %>
<br>
<% end %>
<% if Setting.self_registration? %>
<%= link_to l(:label_register), { :controller => '/account', :action => 'register' } %>
<% end %>
</div>
<% if Setting.autologin? %>
<div class="attribute_wrapper indented">
<label for="autologin">
<%= check_box_tag 'autologin', 1, false %> <%= l(:label_stay_logged_in) %>
</label>
</div>
<% end %>
</div>
<input type="submit" name="login" value="<%=l(:button_login)%>" class="button_highlight" />
<%= javascript_tag "Form.Element.focus('username');" %>
<% end %>

@ -31,42 +31,13 @@ See doc/COPYRIGHT.rdoc for more details.
<% breadcrumb_paths(l(:label_login)) %>
<%= call_hook :view_account_login_top %>
<%= form_tag({:action=> "login"}, autocomplete: 'off', id: 'login-form', class: 'form') do %>
<%= back_url_hidden_field_tag %>
<div id="login-form" class="form">
<h1><%= I18n.t(:label_login) %></h1>
<hr class="form_separator">
<div class="attribute_wrapper">
<label for="username"><%= User.human_attribute_name :login %></label>
<%= text_field_tag 'username', nil %>
</div>
<div class="attribute_wrapper">
<label for="password"><%= User.human_attribute_name :password %></label>
<%= password_field_tag 'password', nil %>
</div>
<div class="login-options-container">
<div class="login-links">
<% if Setting.lost_password? %>
<%= link_to l(:label_password_lost), :controller => '/account', :action => 'lost_password' %>
<br>
<% end %>
<% if Setting.self_registration? %>
<%= link_to l(:label_register), { :controller => '/account', :action => 'register' } %>
<% end %>
</div>
<% if Setting.autologin? %>
<div class="attribute_wrapper indented">
<label for="autologin">
<%= check_box_tag 'autologin', 1, false %> <%= l(:label_stay_logged_in) %>
</label>
</div>
<% unless OpenProject::Configuration.disable_password_login? %>
<%= render partial: 'password_login_form' %>
<% end %>
</div>
<input type="submit" name="login" value="<%=l(:button_login)%>" class="button_highlight" />
<%= render :partial => 'auth_providers' %>
<%= javascript_tag "Form.Element.focus('username');" %>
<% end %>
<%= render partial: 'auth_providers' %>
</div>
<%= call_hook :view_account_login_bottom %>

@ -76,7 +76,7 @@ See doc/COPYRIGHT.rdoc for more details.
<a ng-href="{{PathHelper.messagePath(message.id)}}">{{message.subject}}</a>
</td>
<td class="author"><a ng-href="{{PathHelper.userPath(message.author.id)}}">{{message.author.name}}</a></td>
<td class="created_on" formatted-date="message.created_on"></td>
<td class="created_on" date-time date-time-value="message.created_on"></td>
<td class="replies">{{message.replies_count}}</td>
<td class="last_message">
<span ng-if="message.last_reply !== undefined">

@ -32,7 +32,7 @@ See doc/COPYRIGHT.rdoc for more details.
model: object_name)
%>
<% content_for :error_details do %>
<% single_content_for :error_details do %>
<p><%= l("errors.header_invalid_fields") %></p>
<ul>
<% error_messages.each do |message| %>

@ -29,10 +29,13 @@
object @message
attributes :id,
:subject,
:created_on,
:replies_count,
:sticked_on
node :created_on do |m|
m.created_on.iso8601
end
node :author do |m|
partial('users/show', object: m.author)
end

@ -48,6 +48,8 @@ See doc/COPYRIGHT.rdoc for more details.
<em><%= l(:text_comma_separated) %></em></p>
<p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
<p><%= setting_check_box :repository_authentication_caching_enabled %></p>
</div>
<fieldset class="box tabular settings"><legend><%= l(:text_work_packages_ref_in_commit_messages) %></legend>

@ -29,7 +29,7 @@ See doc/COPYRIGHT.rdoc for more details.
<h2><%= l(:label_bulk_edit_selected_work_packages) %></h2>
<ul><%= @work_packages.collect {|i| content_tag('li', link_to(h("#{i.type} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n").html_safe %></ul>
<ul><%= @work_packages.collect {|i| content_tag('li', link_to(h("#{i.type} ##{i.id}"), work_package_path(i)) + h(": #{i.subject}")) }.join("\n").html_safe %></ul>
<%= form_tag(url_for(controller: '/work_packages/bulk', action: :update, ids: @work_packages),
method: :put) do %>

@ -21,11 +21,11 @@
"angular-truncate": "sparkalow/angular-truncate#fdf60fda265042d12e9414b5354b2cc52f1419de",
"angular-feature-flags": "mjt01/angular-feature-flags#ca40201e2279777dc3820c783f60f845cefff731",
"jquery-migrate": "~1.2.1",
"momentjs": "~2.6.0",
"moment-timezone": "~0.0.6",
"momentjs": "~2.7.0",
"moment-timezone": "~0.2.0",
"angular-context-menu": "0.1.2",
"angular-busy": "~4.0.4",
"hyperagent": "manwithtwowatches/hyperagent#v0.4.1"
"hyperagent": "manwithtwowatches/hyperagent#v0.4.2"
},
"devDependencies": {
"mocha": "~1.14.0",

@ -170,6 +170,24 @@ default:
# * change the cipher key here in your configuration file
# * encrypt data using 'rake db:encrypt RAILS_ENV=production'
# database_cipher_key:
#
# Omniauth direct login:
#
# Per default the user may choose the usual password login as well as several omniauth providers
# on the login page and in the login drop down menu.
#
# With his configuration option you can set a specific omniauth provider to be
# used for direct login. Meaning that the login provider selection is skipped and
# the configured provider is used directly instead.
#
# If this option is active /login will lead directly to the configured omniauth provider
# and so will a click on 'Sign in' (as opposed to opening the drop down menu).
#
# Note that this does not stop a user from manually navigating to any other
# omniauth provider if additional ones are configured.
# omniauth_direct_login_provider: developer
#
# disable_password_login: true
# specific configuration options for production environment
# that overrides the default ones

@ -1267,6 +1267,7 @@ de:
setting_plain_text_mail: "Nur reinen Text (kein HTML) senden"
setting_protocol: "Protokoll"
setting_repositories_encodings: "Kodierungen der Projektarchive"
setting_repository_authentication_caching_enabled: "Aktiviere Cache für Authentifizierungsversuche von Versionskontrollsoftware"
setting_repository_log_display_limit: "Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei"
setting_rest_api_enabled: "REST-Schnittstelle aktivieren"
setting_self_registration: "Anmeldung ermöglicht"

@ -1255,6 +1255,7 @@ en:
setting_plain_text_mail: "Plain text mail (no HTML)"
setting_protocol: "Protocol"
setting_repositories_encodings: "Repositories encodings"
setting_repository_authentication_caching_enabled: "Enable caching for authentication request of version control software"
setting_repository_log_display_limit: "Maximum number of revisions displayed on file log"
setting_rest_api_enabled: "Enable REST web service"
setting_self_registration: "Self-registration"

@ -31,9 +31,11 @@ de:
ajax:
hide: "Verbergen"
loading: "Lädt ..."
button_add_watcher: "Beobachter hinzufügen"
button_check_all: "Alles auswählen"
button_copy: "Kopieren"
button_delete: "Löschen"
button_delete_watcher: "Lösche Beobachter"
button_duplicate: "Duplizieren"
button_edit: "Bearbeiten"
button_log_time: "Aufwand buchen"
@ -52,6 +54,8 @@ de:
general_text_No: "Nein"
general_text_Yes: "Ja"
label_add_columns: "Ausgewählte Spalten hinzufügen"
label_add_comment: "Kommentar hinzufügen"
label_add_comment_title: "Fügen Sie Ihre Kommentare hier zu"
label_added_by: "hinzugefügt von"
label_added_time_by: "Von %{author} %{age} hinzugefügt"
label_ago: "vor"
@ -66,12 +70,15 @@ de:
label_collapse_all: "Alle zuklappen"
label_commented_on: "kommentiert am"
label_contains: "enthält"
label_date: "Datum"
label_descending: "Absteigend"
label_description: "Beschreibung"
label_equals: "ist"
label_expand: "Aufklappen"
label_expanded: "aufgeklappt"
label_expand_all: "Alle aufklappen"
label_filename: "Datei"
label_filesize: "Größe"
label_format_atom: "Atom"
label_format_csv: "CSV"
label_format_pdf: "PDF"
@ -119,6 +126,9 @@ de:
label_today: "heute"
label_work_package: "Arbeitspaket"
label_total_progress: "Gesamtfortschritt"
label_watch_work_package: "Arbeitspaket beobachten"
label_unwatch_work_package: "Arbeitspaket nicht beobachten"
label_uploaded_by: "Hochgeladen von"
text_are_you_sure: "Sind Sie sicher?"
@ -150,6 +160,17 @@ de:
version: "Version"
watcher: "Beobachter"
relation_labels:
parent: "Übergeordnete Aufgabe"
children: "Untergeordnete Aufgaben"
relatedTo: "Beziehung mit"
duplicates: "Dupliziert"
duplicated: "Dupliziert durch"
blocks: "Blockiert"
blocked: "Blockiert durch"
precedes: "Vorgänger von"
follows: "Folgt"
select2:
input_too_short:
one: "Bitte geben Sie ein weiteres Zeichen ein"

@ -31,9 +31,11 @@ en:
ajax:
hide: "Hide"
loading: "Loading ..."
button_add_watcher: "Add watcher"
button_check_all: "Check all"
button_copy: "Copy"
button_delete: "Delete"
button_delete_watcher: "Delete watcher"
button_duplicate: "Duplicate"
button_edit: "Edit"
button_log_time: "Log time"
@ -52,6 +54,8 @@ en:
general_text_No: "No"
general_text_Yes: "Yes"
label_add_columns: "Add selected columns"
label_add_comment: "Add comment"
label_add_comment_title: "Add your comments here"
label_added_by: "added by"
label_added_time_by: "Added by %{author} %{age}"
label_ago: "days ago"
@ -60,6 +64,7 @@ en:
label_ascending: "Ascending"
label_board_locked: "Locked"
label_board_sticky: "Sticky"
label_date: "Date"
label_descending: "Descending"
label_description: "Description"
label_closed_work_packages: "closed"
@ -72,6 +77,8 @@ en:
label_expand: "Expand"
label_expanded: "expanded"
label_expand_all: "Expand all"
label_filename: "File"
label_filesize: "Size"
label_format_atom: "Atom"
label_format_csv: "CSV"
label_format_pdf: "PDF"
@ -118,6 +125,9 @@ en:
label_this_week: "this week"
label_work_package: "Work package"
label_total_progress: "Total progress"
label_watch_work_package: "Watch work package"
label_unwatch_work_package: "Unwatch work package"
label_uploaded_by: "Uploaded by"
text_are_you_sure: "Are you sure?"
@ -149,6 +159,17 @@ en:
version: "Version"
watcher: "Watcher"
relation_labels:
parent: "Parent"
children: "Children"
relatedTo: "Related To"
duplicates: "Duplicates"
duplicated: "Duplicated by"
blocks: "Blocks"
blocked: "Blocked by"
precedes: "Precedes"
follows: "Follows"
select2:
input_too_short:
one: "Please enter one more character"

@ -133,6 +133,8 @@ sys_api_enabled:
default: 0
sys_api_key:
default: ''
repository_authentication_caching_enabled:
default: 1
commit_ref_keywords:
default: 'refs,references,IssueID'
commit_fix_keywords:

@ -68,7 +68,7 @@ module Migration::Utils
journal_ids.each do |journal_id|
insert <<-SQL
INSERT INTO customizable_journals (journal_id, custom_field_id, value)
VALUES (#{journal_id}, #{m.custom_field_id}, '#{m.value}')
VALUES (#{journal_id}, #{m.custom_field_id}, #{quote_value(m.value)})
SQL
end
end

@ -103,6 +103,8 @@ See doc/COPYRIGHT.rdoc for more details.
* Fix: Asset require for plug-ins
* Fix: at.who styling
* `#1030` Fix: New target version cannot be created from work package view
## 3.0.8
* new version scheme

@ -62,12 +62,39 @@ In case you want to use environment variables, but you have no easy way to set t
* `scm_git_command` (default: 'git')
* `scm_subversion_command` (default: 'git')
* `session_store`: `active_record_store`, `cache_store`, or `cookie_store` (default: cache_store)
* [`omniauth_direct_login_provider`](#omniauth-direct-login-provider) (default: nil)
* [`disable_password_login`](#disable-password-login) (default: false)
Email configuration
### disable password login
*default: false*
If you enable this option you have to configure at least one omniauth authentication
provider to take care of authentication instead of the password login.
All username/password forms will be removed and only a list of omniauth providers
presented to the users.
### omniauth direct login provider
*default: nil*
Example:
omniauth_direct_login_provider: google
Per default the user may choose the usual password login as well as several omniauth providers on the login page and in the login drop down menu. With his configuration option you can set a specific omniauth provider to be used for direct login. Meaning that the login provider selection is skipped and the configured provider is used directly instead.
If this option is active /login will lead directly to the configured omniauth provider and so will a click on 'Sign in' (as opposed to opening the drop down menu).
Note that this does not stop a user from manually navigating to any other
omniauth provider if additional ones are configured.
## Email configuration
* `email_delivery_method`: The way emails should be delivered. Possible values: `smtp` or `sendmail`
SMTP Options:
### SMTP Options:
* `smtp_address`: SMTP server hostname, e.g. `smtp.example.net`
* `smtp_port`: SMTP server port. Common options are `25` and `587`.
@ -78,7 +105,7 @@ SMTP Options:
* `smtp_enable_starttls_auto`: You can disable STARTTLS here in case it doesn't work. Make sure you don't login to a SMTP server over a public network when using this. This setting can't currently be used via environment variables, since setting options to `false` is only possible via a YAML file. (default: true, optional)
* `smtp_openssl_verify_mode`: Define how the SMTP server certificate is validated. Make sure you don't just disable verification here unless both, OpenProject and SMTP servers are on a private network. Possible values: `none`, `peer`, `client_once` or `fail_if_no_peer_cert`
Cache Options:
## Cache Options:
* `rails_cache_store`: `memcache` for [memcached](http://www.memcached.org/) or `memory_store` (default: `file_store`)
* `cache_memcache_server`: The memcache server host and IP (default: `127.0.0.1:11211`)

@ -0,0 +1,175 @@
package Apache::Authn::OpenProject;
=head1 Apache::Authn::OpenProject
OpenProject - a mod_perl module to authenticate webdav subversion users
against an OpenProject web service
=head1 SYNOPSIS
This module allow anonymous users to browse public project and
registred users to browse and commit their project. Authentication is
done against an OpenProject web service.
=head1 INSTALLATION
For this to automagically work, you need to have a recent reposman.rb.
Sorry ruby users but you need some perl modules, at least mod_perl2 and apache2-svn.
On debian/ubuntu you must do :
aptitude install libapache2-mod-perl2 libapache2-svn
=head1 CONFIGURATION
## This module has to be in your perl path
## eg: /usr/lib/perl5/Apache/Authn/OpenProjectAuthentication.pm
PerlLoadModule Apache::Authn::OpenProjectAuthentication
<Location /svn>
DAV svn
SVNParentPath "/var/svn"
AuthType Basic
AuthName OpenProject
Require valid-user
PerlAccessHandler Apache::Authn::OpenProject::access_handler
PerlAuthenHandler Apache::Authn::OpenProject::authen_handler
OpenProjectUrl "http://example.com/openproject/"
OpenProjectApiKey "<API key>"
</Location>
To be able to browse repository inside openproject, you must add something
like that :
<Location /svn-private>
DAV svn
SVNParentPath "/var/svn"
Order deny,allow
Deny from all
# only allow reading orders
<Limit GET PROPFIND OPTIONS REPORT>
Allow from openproject.server.ip
</Limit>
</Location>
and you will have to use this reposman.rb command line to create repository :
reposman.rb --openproject my.openproject.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
=cut
use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';
use Digest::SHA;
use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::Const qw(:common :override :cmd_how);
use APR::Pool ();
use APR::Table ();
use HTTP::Request::Common qw(POST);
use LWP::UserAgent;
# use Apache2::Directive qw();
my @directives = (
{
name => 'OpenProjectUrl',
req_override => OR_AUTHCFG,
args_how => TAKE1,
errmsg => 'URL of your (local) OpenProject. (e.g. http://localhost/ or http://www.example.com/openproject/)',
},
{
name => 'OpenProjectApiKey',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
);
sub OpenProjectUrl { set_val('OpenProjectUrl', @_); }
sub OpenProjectApiKey { set_val('OpenProjectApiKey', @_); }
sub trim {
my $string = shift;
$string =~ s/\s{2,}/ /g;
return $string;
}
sub set_val {
my ($key, $self, $parms, $arg) = @_;
$self->{$key} = $arg;
}
Apache2::Module::add(__PACKAGE__, \@directives);
sub access_handler {
my $r = shift;
unless ($r->some_auth_required) {
$r->log_reason("No authentication has been configured");
return FORBIDDEN;
}
return OK
}
sub authen_handler {
my $r = shift;
my ($status, $password) = $r->get_basic_auth_pw();
my $login = $r->user;
return $status unless $status == OK;
my $identifier = get_project_identifier($r);
my $method = $r->method;
if( is_access_allowed( $login, $password, $identifier, $method, $r ) ) {
return OK;
} else {
$r->note_auth_failure();
return AUTH_REQUIRED;
}
}
# we send a request to the openproject sys api
# and use the user's given login and password for basic auth
# for accessing the openproject sys api an api key is needed
sub is_access_allowed {
my $login = shift;
my $password = shift;
my $identifier = shift;
my $method = shift;
my $r = shift;
my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );
my $key = $cfg->{OpenProjectApiKey};
my $openproject_url = $cfg->{OpenProjectUrl} . '/sys/repo_auth';
my $openproject_req = POST $openproject_url , [ repository => $identifier, key => $key, method => $method ];
$openproject_req->authorization_basic( $login, $password );
my $ua = LWP::UserAgent->new;
my $response = $ua->request($openproject_req);
return $response->is_success();
}
sub get_project_identifier {
my $r = shift;
my $location = $r->location;
my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
$identifier;
}
1;

@ -1,52 +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.
-- ++
/* ssh views */
CREATE OR REPLACE VIEW ssh_users as
select login as username, hashed_password as password
from users
where status = 1;
/* nss views */
CREATE OR REPLACE VIEW nss_groups AS
select identifier AS name, (id + 5000) AS gid, 'x' AS password
from projects;
CREATE OR REPLACE VIEW nss_users AS
select login AS username, CONCAT_WS(' ', firstname, lastname) as realname, (id + 5000) AS uid, 'x' AS password
from users
where status = 1;
CREATE OR REPLACE VIEW nss_grouplist AS
select (members.project_id + 5000) AS gid, users.login AS username
from users, members
where users.id = members.user_id
and users.status = 1;

@ -31,13 +31,16 @@
require 'optparse'
require 'find'
require 'etc'
require 'json'
require 'net/http'
require 'uri'
Version = "1.3"
Version = "1.4"
SUPPORTED_SCM = %w( Subversion Git Filesystem )
$verbose = 0
$quiet = false
$redmine_host = ''
$openproject_host = ''
$repos_base = ''
$svn_owner = 'root'
$svn_group = 'root'
@ -86,36 +89,37 @@ OptionParser.new do |opts|
opts.separator("")
opts.separator("Required arguments:")
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v}
opts.on("-r", "--redmine-host HOST", "assume Redmine is hosted on HOST. Examples:",
" -r redmine.example.net",
" -r http://redmine.example.net",
" -r https://redmine.example.net") {|v| $redmine_host = v}
opts.on("-k", "--key KEY", "use KEY as the Redmine API key") {|v| $api_key = v}
opts.on("-r", "--openproject-host HOST", "assume OpenProject is hosted on HOST. Examples:",
" -r openproject.example.net",
" -r http://openproject.example.net",
" -r https://openproject.example.net") {|v| $openproject_host = v}
opts.on('', "--redmine-host HOST", "DEPRECATED: please use --openproject-host instead") {|v| $openproject_host = v}
opts.on("-k", "--key KEY", "use KEY as the OpenProject API key") {|v| $api_key = v}
opts.separator("")
opts.separator("Options:")
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
"allows users to browse the repository within",
"Redmine even for private projects. If you want to",
"share repositories through Redmine.pm, you need",
"OpenProject even for private projects. If you want to",
"share repositories through OpenProject.pm, you need",
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false}
opts.on("-g", "--group GROUP", "group of the repository (default: root)") {|v| $svn_group = v; $use_groupid = false}
opts.on( "--public-mode MODE", "file mode for new public repositories (default: 0775)") {|v| $public_mode = v}
opts.on( "--private-mode MODE", "file mode for new private repositories (default: 0770)") {|v| $private_mode = v}
opts.on( "--scm SCM", "the kind of SCM repository you want to create",
"(and register) in Redmine (default: Subversion).",
"(and register) in OpenProject (default: Subversion).",
"reposman is able to create Git and Subversion",
"repositories.",
"For all other kind, you must specify a --command",
"option") {|v| v.capitalize; log("Invalid SCM: #{v}", :exit => true) unless SUPPORTED_SCM.include?(v)}
opts.on("-u", "--url URL", "the base url Redmine will use to access your",
opts.on("-u", "--url URL", "the base url OpenProject will use to access your",
"repositories. This option is used to automatically",
"register the repositories in Redmine. The project ",
"register the repositories in OpenProject. The project ",
"identifier will be appended to this url.",
"Examples:",
" -u https://example.net/svn",
" -u file:///var/svn/",
"if this option isn't set, reposman won't register",
"the repositories in Redmine") {|v| $svn_url = v}
"the repositories in OpenProject") {|v| $svn_url = v}
opts.on("-c", "--command COMMAND", "use this command instead of 'svnadmin create' to",
"create a repository. This option can be used to",
"create repositories other than subversion and git",
@ -123,7 +127,7 @@ OptionParser.new do |opts|
"This command override the default creation for git",
"and subversion.") {|v| $command = v}
opts.on("-f", "--force", "force repository creation even if the project",
"repository is already declared in Redmine") {$force = true}
"repository is already declared in OpenProject") {$force = true}
opts.on("-t", "--test", "only show what should be done") {$test = true}
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
opts.on("-v", "--verbose", "verbose") {$verbose += 1}
@ -131,10 +135,10 @@ OptionParser.new do |opts|
opts.on("-q", "--quiet", "no log") {$quiet = true}
opts.separator("")
opts.separator("Examples:")
opts.separator(" reposman.rb --svn-dir=/var/svn --redmine-host=redmine.example.net --scm Subversion")
opts.separator(" reposman.rb -s /var/git -r redmine.example.net -u http://svn.example.net --scm Git")
opts.separator(" reposman.rb --svn-dir=/var/svn --openproject-host=openproject.example.net --scm Subversion")
opts.separator(" reposman.rb -s /var/git -r openproject.example.net -u http://svn.example.net --scm Git")
opts.separator("")
opts.separator("You can find more information on the redmine's wiki:\nhttp://www.redmine.org/projects/redmine/wiki/HowTos")
opts.separator("You might find more information on the openproject's wiki:\nhttps://www.openproject.org/projects/openproject/wiki/Support")
end.parse!
if $test
@ -152,7 +156,7 @@ end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
if ($redmine_host.empty? or $repos_base.empty?)
if ($openproject_host.empty? or $repos_base.empty?)
puts "Required argument missing. Type 'reposman.rb --help' for usage."
exit 1
end
@ -161,28 +165,22 @@ unless File.directory?($repos_base)
log("directory '#{$repos_base}' doesn't exists", :exit => true)
end
begin
require 'active_resource'
rescue LoadError
log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
end
class Project < ActiveResource::Base
self.headers["User-agent"] = "Redmine repository manager/#{Version}"
end
log("querying Redmine for projects...", :level => 1);
log("querying OpenProject for projects...", :level => 1);
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
$redmine_host.gsub!(/\/$/, '')
$openproject_host.gsub!(/^/, "http://") unless $openproject_host.match("^https?://")
$openproject_host.gsub!(/\/$/, '')
Project.site = "#{$redmine_host}/sys";
api_uri = URI.parse("#{$openproject_host}/sys")
http = Net::HTTP.new(api_uri.host, api_uri.port)
http.use_ssl = (api_uri.scheme == 'https')
http_headers = {'User-Agent' => "OpenProject-Repository-Manager/#{Version}"}
begin
# Get all active projects that have the Repository module enabled
projects = Project.find(:all, :params => {:key => $api_key})
response = http.get("#{api_uri.path}/projects.json?key=#{$api_key}", http_headers)
projects = JSON.parse(response.body)
rescue => e
log("Unable to connect to #{Project.site}: #{e}", :exit => true)
log("Unable to connect to #{$openproject_host}: #{e}", :exit => true)
end
if projects.nil?
@ -195,8 +193,8 @@ def set_owner_and_rights(project, repos_path, &block)
if mswin?
yield if block_given?
else
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid)
right = project.is_public ? $public_mode : $private_mode
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project['identifier']).gid : Etc.getgrnam($svn_group).gid)
right = project['is_public'] ? $public_mode : $private_mode
right = right.to_i(8) & 007777
yield if block_given?
Find.find(repos_path) do |f|
@ -221,17 +219,17 @@ def mswin?
end
projects.each do |project|
log("treating project #{project.name}", :level => 1)
log("treating project #{project['name']}", :level => 1)
if project.identifier.empty?
log("\tno identifier for project #{project.name}")
if project['identifier'].empty?
log("\tno identifier for project #{project['name']}")
next
elsif not project.identifier.match(/^[a-z0-9\-_]+$/)
log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
elsif not project['identifier'].match(/^[a-z0-9\-_]+$/)
log("\tinvalid identifier for project #{project['name']} : #{project['identifier']}");
next;
end
repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
repos_path = File.join($repos_base, project['identifier']).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
if File.directory?(repos_path)
@ -239,7 +237,7 @@ projects.each do |project|
# rights before leaving
other_read = other_read_right?(repos_path)
owner = owner_name(repos_path)
next if project.is_public == other_read and owner == $svn_owner
next if project['is_public'] == other_read and owner == $svn_owner
if $test
log("\tchange mode on #{repos_path}")
@ -256,18 +254,18 @@ projects.each do |project|
log("\tmode change on #{repos_path}");
else
# if repository is already declared in redmine, we don't create
# if repository is already declared in openproject, we don't create
# unless user use -f with reposman
if $force == false and project.respond_to?(:repository)
log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1)
if $force == false and project.has_key?('repository')
log("\trepository for project #{project['identifier']} already exists in OpenProject", :level => 1)
next
end
project.is_public ? File.umask(0002) : File.umask(0007)
project['is_public'] ? File.umask(0002) : File.umask(0007)
if $test
log("\tcreate repository #{repos_path}")
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url;
log("\trepository #{repos_path} registered in OpenProject with url #{$svn_url}#{project['identifier']}") if $svn_url;
next
end
@ -286,10 +284,13 @@ projects.each do |project|
if $svn_url
begin
project.post(:repository, :vendor => $scm, :repository => {:url => "#{$svn_url}#{project.identifier}"}, :key => $api_key)
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
http.post("#{api_uri.path}/projects/#{project['identifier']}/repository.json?" +
"vendor=#{$scm}&repository[url]=#{$svn_url}#{project['identifier']}&key=#{$api_key}",
"", # empty data
http_headers)
log("\trepository #{repos_path} registered in OpenProject with url #{$svn_url}#{project['identifier']}");
rescue => e
log("\trepository #{repos_path} not registered in Redmine: #{e.message}");
log("\trepository #{repos_path} not registered in OpenProject: #{e.message}");
end
end

@ -29,14 +29,18 @@ module.exports = function(config) {
"vendor/assets/components/angular-truncate/src/truncate.js",
"vendor/assets/components/angular-sanitize/angular-sanitize.js",
"vendor/assets/components/momentjs/moment.js",
"vendor/assets/components/moment-timezone/moment-timezone.js",
"vendor/assets/components/angular-context-menu/dist/angular-context-menu.js",
'vendor/assets/components/select2/select2.js',
'vendor/assets/components/hyperagent/dist/hyperagent.js',
"vendor/assets/components/openproject-ui_components/app/assets/javascripts/angular/ui-components-app.js",
"vendor/assets/javascripts/moment-timezone/moment-timezone-data.js",
"app/assets/javascripts/angular/openproject-app.js",
"app/assets/javascripts/angular/config/work-packages-config.js",
"app/assets/javascripts/angular/config/configuration-service.js",
"app/assets/javascripts/angular/controllers/**/*.js",
"app/assets/javascripts/angular/dialogs/**/*.js",

@ -0,0 +1,295 @@
//-- 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.
//++
/*jshint expr: true*/
describe('DetailsTabOverviewController', function() {
var scope;
var buildController;
var I18n = { t: angular.identity },
WorkPackagesHelper = {
formatWorkPackageProperty: angular.identity
},
UserService = {
getUser: angular.identity
},
CustomFieldHelper = {
formatCustomFieldValue: angular.identity
},
workPackage = {
props: {
status: 'open',
versionName: null,
customProperties: [
{ format: 'text', name: 'color', value: 'red' },
]
},
embedded: {
activities: [],
watchers: [],
attachments: []
},
};
function buildWorkPackageWithId(id) {
angular.extend(workPackage.props, {id: id});
return workPackage;
}
beforeEach(module('openproject.api', 'openproject.services', 'openproject.workPackages.controllers'));
beforeEach(inject(function($rootScope, $controller, $timeout) {
var workPackageId = 99;
buildController = function() {
scope = $rootScope.$new();
scope.workPackage = workPackage;
ctrl = $controller("DetailsTabOverviewController", {
$scope: scope,
I18n: I18n,
UserService: UserService,
CustomFieldHelper: CustomFieldHelper,
});
$timeout.flush();
};
}));
describe('initialisation', function() {
it('should initialise', function() {
buildController();
});
});
describe('work package properties', function() {
function fetchPresentPropertiesWithName(propertyName) {
return scope.presentWorkPackageProperties.filter(function(propertyData) {
return propertyData.property === propertyName;
});
}
describe('when the property has a value', function() {
var propertyName = 'status';
beforeEach(function() {
buildController();
});
it('adds properties to present properties', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
describe('when the property is among the first 3 properties', function() {
var propertyName = 'responsible';
beforeEach(function() {
buildController();
});
it('is added to present properties even if it is empty', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
describe('when the property is among the second group of 3 properties', function() {
var propertyName = 'priority',
label = 'Priority';
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.work_packages.properties.' + propertyName)
.returns(label);
buildController();
});
afterEach(function() {
I18n.t.restore();
});
describe('and none of these 3 properties is present', function() {
beforeEach(function() {
buildController();
});
it('is added to the empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1);
});
});
describe('and at least one of these 3 properties is present', function() {
beforeEach(function() {
workPackage.props.percentageDone = '20';
buildController();
});
it('is added to the present properties', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
});
describe('when the property is not among the first 6 properties', function() {
var propertyName = 'versionName',
label = 'Version';
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.work_packages.properties.' + propertyName)
.returns(label);
buildController();
});
afterEach(function() {
I18n.t.restore();
});
it('adds properties that without values to empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1);
});
});
describe('date property', function() {
var startDate = '2014-07-09',
dueDate = '2014-07-10',
placeholder = 'placeholder';
describe('when only the due date is present', function() {
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.label_no_start_date')
.returns(placeholder);
workPackage.props.startDate = null;
workPackage.props.dueDate = dueDate;
buildController();
});
afterEach(function() {
I18n.t.restore();
});
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');
});
});
describe('when only the start date is present', function() {
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.label_no_due_date')
.returns(placeholder);
workPackage.props.startDate = startDate;
workPackage.props.dueDate = null;
buildController();
});
afterEach(function() {
I18n.t.restore();
});
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);
});
});
describe('when both - start and due date are present', function() {
beforeEach(function() {
workPackage.props.startDate = startDate;
workPackage.props.dueDate = dueDate;
buildController();
});
it('combines them and renders them as date property', function() {
expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - Jul 10, 2014');
});
});
});
describe('custom field properties', function() {
var customPropertyName = 'color';
describe('when the property has a value', function() {
beforeEach(function() {
formatCustomFieldValueSpy = sinon.spy(CustomFieldHelper, 'formatCustomFieldValue');
buildController();
});
afterEach(function() {
CustomFieldHelper.formatCustomFieldValue.restore();
});
it('adds properties to present properties', function() {
expect(fetchPresentPropertiesWithName(customPropertyName)).to.have.length(1);
});
it('formats values using the custom field helper', function() {
expect(CustomFieldHelper.formatCustomFieldValue.calledWith('red', 'text')).to.be.true;
});
});
describe('when the property does not have a value', function() {
beforeEach(function() {
workPackage.props.customProperties[0].value = null;
buildController();
});
it('adds the custom property to empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(customPropertyName)).to.be.greaterThan(-1);
});
});
describe('user custom property', function() {
var userId = '1';
beforeEach(function() {
workPackage.props.customProperties[0].value = userId;
workPackage.props.customProperties[0].format = 'user';
getUserSpy = sinon.spy(UserService, 'getUser');
buildController();
});
it('fetches the user using the user service', function() {
expect(UserService.getUser.calledWith(userId)).to.be.true;
});
});
});
});
});

@ -50,7 +50,35 @@ describe('WorkPackageDetailsController', function() {
]
},
embedded: {
activities: []
activities: [],
watchers: [],
attachments: [],
relations: [
{
props: {
_type: "Relation::Relates"
},
links: {
relatedFrom: {
fetch: sinon.spy()
},
relatedTo: {
fetch: sinon.spy()
}
}
}
]
},
links: {
self: "it's a me, it's... you know...",
availableWatchers: {
fetch: function() { return {then: angular.noop}; }
}
},
link: {
addWatcher: {
fetch: function() { return {then: angular.noop}; }
}
},
};
@ -76,8 +104,9 @@ describe('WorkPackageDetailsController', function() {
return false;
}
},
UserService: UserService,
CustomFieldHelper: CustomFieldHelper,
WorkPackagesDetailsHelper: {
attachmentsTitle: function() { return ''; }
},
workPackage: buildWorkPackageWithId(workPackageId),
});
@ -92,206 +121,38 @@ describe('WorkPackageDetailsController', function() {
});
});
describe('work package properties', function() {
function fetchPresentPropertiesWithName(propertyName) {
return scope.presentWorkPackageProperties.filter(function(propertyData) {
return propertyData.property === propertyName;
});
}
describe('when the property has a value', function() {
var propertyName = 'status';
beforeEach(function() {
buildController();
});
it('adds properties to present properties', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
describe('when the property is among the first 3 properties', function() {
var propertyName = 'responsible';
beforeEach(function() {
buildController();
});
it('is added to present properties even if it is empty', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
describe('when the property is among the second group of 3 properties', function() {
var propertyName = 'priority',
label = 'Priority';
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.work_packages.properties.' + propertyName)
.returns(label);
buildController();
});
afterEach(function() {
I18n.t.restore();
});
describe('and none of these 3 properties is present', function() {
beforeEach(function() {
buildController();
});
it('is added to the empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1);
});
});
describe('and at least one of these 3 properties is present', function() {
beforeEach(function() {
workPackage.props.percentageDone = '20';
buildController();
});
it('is added to the present properties', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
});
describe('when the property is not among the first 6 properties', function() {
var propertyName = 'versionName',
label = 'Version';
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.work_packages.properties.' + propertyName)
.returns(label);
buildController();
});
afterEach(function() {
I18n.t.restore();
});
it('adds properties that without values to empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1);
});
});
describe('date property', function() {
var startDate = '2014-07-09',
dueDate = '2014-07-10',
placeholder = 'placeholder';
describe('when only the due date is present', function() {
describe('#scope.canViewWorkPackageWatchers', function() {
describe('when the work package does not contain the embedded watchers property', function() {
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.label_no_start_date')
.returns(placeholder);
workPackage.props.startDate = null;
workPackage.props.dueDate = dueDate;
workPackage.embedded.watchers = undefined;
buildController();
});
afterEach(function() {
I18n.t.restore();
});
})
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');
it('returns false', function() {
expect(scope.canViewWorkPackageWatchers()).to.be.false;
});
});
describe('when only the start date is present', function() {
describe('when the work package contains the embedded watchers property', function() {
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.label_no_due_date')
.returns(placeholder);
workPackage.props.startDate = startDate;
workPackage.props.dueDate = null;
workPackage.embedded.watchers = [];
buildController();
});
})
afterEach(function() {
I18n.t.restore();
it('returns true', function() {
expect(scope.canViewWorkPackageWatchers()).to.be.true;
});
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);
});
});
describe('when both - start and due date are present', function() {
beforeEach(function() {
workPackage.props.startDate = startDate;
workPackage.props.dueDate = dueDate;
buildController();
});
it('combines them and renders them as date property', function() {
expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - Jul 10, 2014');
});
});
});
describe('custom field properties', function() {
var customPropertyName = 'color';
describe('when the property has a value', function() {
beforeEach(function() {
formatCustomFieldValueSpy = sinon.spy(CustomFieldHelper, 'formatCustomFieldValue');
buildController();
});
afterEach(function() {
CustomFieldHelper.formatCustomFieldValue.restore();
});
it('adds properties to present properties', function() {
expect(fetchPresentPropertiesWithName(customPropertyName)).to.have.length(1);
});
it('formats values using the custom field helper', function() {
expect(CustomFieldHelper.formatCustomFieldValue.calledWith('red', 'text')).to.be.true;
});
});
describe('when the property does not have a value', function() {
beforeEach(function() {
workPackage.props.customProperties[0].value = null;
buildController();
});
it('adds the custom property to empty properties', function() {
expect(scope.emptyWorkPackageProperties.indexOf(customPropertyName)).to.be.greaterThan(-1);
});
});
describe('user custom property', function() {
var userId = '1';
describe('work package properties', function() {
describe('relations', function() {
beforeEach(function() {
workPackage.props.customProperties[0].value = userId;
workPackage.props.customProperties[0].format = 'user';
getUserSpy = sinon.spy(UserService, 'getUser');
buildController();
});
it('fetches the user using the user service', function() {
expect(UserService.getUser.calledWith(userId)).to.be.true;
});
it('Relation::Relates', function() {
expect(scope.relatedTo.length).to.eq(1);
});
});
});

@ -0,0 +1,186 @@
//-- 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.
//++
describe('date time Directives', function() {
var I18n, compile, element, scope, timezoneService, configurationService;
var formattedDate = function() {
var formattedDateElement = element[0];
return formattedDateElement.innerText || formattedDateElement.textContent;
};
beforeEach(angular.mock.module('openproject.uiComponents', '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, _I18n_, _TimezoneService_) {
scope = $rootScope.$new();
scope.testDateTime = "2013-02-08T09:30:26";
compile = function(html) {
element = $compile(html)(scope);
scope.$digest();
};
TimezoneService = _TimezoneService_;
I18n = _I18n_;
I18n.locale = 'en';
}));
var shouldBehaveLikeHashTitle = function(title) {
it('has title', function() {
expect(angular.element(element)[0].title).to.eq(title);
});
};
describe('date directive', function() {
var html = '<date date-value="testDateTime"></date>';
describe('without configuration', function() {
beforeEach(function() {
configurationService.dateFormatPresent = sinon.stub().returns(false);
compile(html);
});
it('should use default formatting', function() {
expect(formattedDate()).to.contain('02/08/2013');
});
shouldBehaveLikeHashTitle('02/08/2013');
});
describe('with configuration', function() {
beforeEach(function() {
configurationService.dateFormatPresent = sinon.stub().returns(true);
configurationService.dateFormat = sinon.stub().returns("DD-MM-YYYY");
compile(html);
});
it('should use user specified formatting', function() {
expect(formattedDate()).to.contain('08-02-2013');
});
shouldBehaveLikeHashTitle('08-02-2013');
});
});
describe('time directive', function() {
var html = '<time time-value="testDateTime"></time>';
describe('without configuration', function() {
beforeEach(function() {
configurationService.timeFormatPresent = sinon.stub().returns(false);
compile(html);
});
it('should use default formatting', function() {
expect(formattedDate()).to.contain('9:30 AM');
});
shouldBehaveLikeHashTitle('9:30 AM');
});
describe('with configuration', function() {
beforeEach(function() {
configurationService.timeFormatPresent = sinon.stub().returns(true);
configurationService.timeFormat = sinon.stub().returns("HH:mm a");
compile(html);
});
it('should use user specified formatting', function() {
expect(formattedDate()).to.contain('09:30 am');
});
shouldBehaveLikeHashTitle('09:30 am');
});
});
describe('date time directive', function() {
var html = '<date-time date-time-value="testDateTime"></date-time>';
var formattedDateTime = function() {
var formattedDateElements = [element.children()[0], element.children()[1]];
var formattedDateTime = "";
for (var x = 0; x < formattedDateElements.length; x++) {
formattedDateTime += (formattedDateElements[x].innerText || formattedDateElements[x].textContent) + " ";
}
return formattedDateTime;
};
describe('without configuration', function() {
beforeEach(function() {
configurationService.dateFormatPresent = sinon.stub().returns(false);
configurationService.timeFormatPresent = sinon.stub().returns(false);
scope.dateTimeValue = "2013-02-08T09:30:26";
compile(html);
});
it('should use default formatting', function() {
expect(formattedDateTime()).to.contain('02/08/2013');
expect(formattedDateTime()).to.contain('9:30 AM');
});
shouldBehaveLikeHashTitle('02/08/2013 9:30 AM');
});
describe('with configuration', function() {
beforeEach(function() {
configurationService.dateFormatPresent = sinon.stub().returns(true);
configurationService.timeFormatPresent = sinon.stub().returns(true);
configurationService.dateFormat = sinon.stub().returns("DD-MM-YYYY");
configurationService.timeFormat = sinon.stub().returns("HH:mm a");
compile(html);
});
it('should use user specified formatting', function() {
expect(formattedDateTime()).to.contain('08-02-2013');
expect(formattedDateTime()).to.contain('09:30 am');
});
shouldBehaveLikeHashTitle('08-02-2013 09:30 am');
});
});
});

@ -0,0 +1,234 @@
//-- 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.
//++
describe('Work Package Relation Directive', function() {
var I18n, PathHelper, compile, element, scope;
beforeEach(angular.mock.module('openproject.uiComponents', 'openproject.helpers', 'ngSanitize'));
beforeEach(module('templates', function($provide) {
}));
beforeEach(inject(function($rootScope, $compile, _I18n_, _PathHelper_) {
scope = $rootScope.$new();
compile = function(html) {
element = $compile(html)(scope);
scope.$digest();
};
I18n = _I18n_;
PathHelper = _PathHelper_;
var stub = sinon.stub(I18n, 't');
stub.withArgs('js.work_packages.properties.subject').returns('Column0');
stub.withArgs('js.work_packages.properties.status').returns('Column1');
stub.withArgs('js.work_packages.properties.assignee').returns('Column2');
}));
afterEach(function() {
I18n.t.restore();
});
var multiElementHtml = "<work-package-relation title='MyRelation' related-work-packages='relations' button-title='Add Relation' button-icon='%MyIcon%'></work-package-relation>"
var singleElementHtml = "<work-package-relation title='MyRelation' related-work-packages='relations' button-title='Add Relation' button-icon='%MyIcon%' singleton-relation='true'></work-package-relation>"
var workPackage1;
var workPackage2;
beforeEach(function() {
workPackage1 = {
props: {
id: "1",
subject: "Subject 1",
status: "Status 1"
},
embedded: {
assignee: {
props: {
name: "Assignee 1",
}
}
}
};
workPackage2 = {
props: {
id: "2",
subject: "Subject 2",
status: "Status 2"
},
embedded: {
assignee: {
props: {
name: "Assignee 2",
}
}
}
};
});
var shouldBehaveLikeRelationDirective = function() {
it('should have a title', function() {
var title = angular.element(element.find('h3'));
expect(title.text()).to.include('MyRelation');
});
//it('should have a button', function() {
// var button = angular.element(element.find('button.button'));
// expect(button.attr('title')).to.include('Add Relation');
// expect(button.text()).to.include('Add Relation');
// expect(button.text()).to.include('%MyIcon%');
//});
};
var shouldBehaveLikeHasTableHeader = function() {
it('should have a table head', function() {
var column0 = angular.element(element.find('.workpackages table thead td:nth-child(1)'));
var column1 = angular.element(element.find('.workpackages table thead td:nth-child(2)'));
var column2 = angular.element(element.find('.workpackages table thead td:nth-child(3)'));
expect(angular.element(column0).text()).to.eq(I18n.t('js.work_packages.properties.subject'));
expect(angular.element(column1).text()).to.eq(I18n.t('js.work_packages.properties.status'));
expect(angular.element(column2).text()).to.eq(I18n.t('js.work_packages.properties.assignee'));
});
};
var shouldBehaveLikeHasTableContent = function(count) {
it('should have table content', function() {
for (var x = 1; x <= count; x++) {
var column0 = angular.element(element.find('.workpackages table tbody:nth-of-type(' + x + ') tr td:nth-child(1)'));
var column1 = angular.element(element.find('.workpackages table tbody:nth-of-type(' + x + ') tr td:nth-child(2)'));
var column2 = angular.element(element.find('.workpackages table tbody:nth-of-type(' + x + ') tr td:nth-child(3)'));
expect(angular.element(column0).text()).to.include('Subject ' + x);
expect(angular.element(column1).text()).to.include('Status ' + x);
expect(angular.element(column2).text()).to.include('Assignee ' + x);
}
});
};
var shouldBehaveLikeCollapsedRelationsDirective = function() {
shouldBehaveLikeRelationDirective();
it('should be initially collapsed', function() {
var content = angular.element(element.find('div.content'));
expect(content.hasClass('ng-hide')).to.eq(true);
});
};
var shouldBehaveLikeExpandedRelationsDirective = function() {
shouldBehaveLikeRelationDirective();
it('should be initially expanded', function() {
var content = angular.element(element.find('div.content'));
expect(content.hasClass('ng-hide')).to.eq(false);
});
};
var shouldBehaveLikeSingleRelationDirective = 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(')');
});
};
var shouldBehaveLikeMultiRelationDirective = function() {
it('should have an elements count', function() {
var title = angular.element(element.find('h3'));
expect(title.text()).to.include('(' + scope.relations.length + ')');
});
};
describe('no element markup', function() {
describe('single element behavior', function() {
beforeEach(function() {
compile(singleElementHtml);
});
shouldBehaveLikeSingleRelationDirective();
shouldBehaveLikeCollapsedRelationsDirective();
});
describe('multi element behavior', function() {
beforeEach(function() {
scope.relations = [];
compile(multiElementHtml);
});
shouldBehaveLikeMultiRelationDirective();
shouldBehaveLikeCollapsedRelationsDirective();
});
});
describe('single element markup', function() {
beforeEach(function() {
scope.relations = [workPackage1];
compile(singleElementHtml);
});
shouldBehaveLikeRelationDirective();
shouldBehaveLikeSingleRelationDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
shouldBehaveLikeHasTableContent(1);
});
describe('multi element markup', function() {
beforeEach(function() {
scope.relations = [workPackage1, workPackage2];
compile(multiElementHtml);
});
shouldBehaveLikeRelationDirective();
shouldBehaveLikeMultiRelationDirective();
shouldBehaveLikeExpandedRelationsDirective();
shouldBehaveLikeHasTableHeader();
shouldBehaveLikeHasTableContent(2);
});
});

@ -0,0 +1,90 @@
//-- 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.
//++
describe('attachmentFileSize Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<td attachment-file-size attachment="attachment"></td>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
describe('with file size present on attachment', function(){
beforeEach(function() {
scope.attachment = {
props: {
id: 1,
fileSize: '12340'
}
};
compile();
});
it('should render element', function() {
expect(element.prop('tagName')).to.equal('TD');
});
it('should render file size in kB', function() {
var el = element.find('span');
expect(el.text()).to.equal('(12.34kB)');
});
});
describe('with missing file size', function(){
beforeEach(function() {
scope.attachment = {
props: {
id: 1
}
};
compile();
});
it('should render 0kB', function() {
var el = element.find('span');
expect(el.text()).to.equal('(0kB)');
});
});
});
});

@ -0,0 +1,72 @@
//-- 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.
//++
describe('attachmentTitleCell Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<td attachment-title-cell attachment="attachment"></td>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
beforeEach(function() {
scope.attachment = {
props: {
id: 1,
fileName: 'hearmi.now',
fileSize: '12340'
}
};
compile();
});
it('should render element', function() {
expect(element.prop('tagName')).to.equal('TD');
});
it('should render link to attachment', function() {
var link = element.find('a');
expect(link.text()).to.equal('hearmi.now');
expect(link.attr('href')).to.equal('/attachments/1/hearmi.now');
});
});
});

@ -0,0 +1,79 @@
//-- 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.
//++
describe('attachmentUserCell Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<td attachment-user-cell attachment="attachment"></td>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
var userName = 'Big Phil Scolari';
var userId = 5;
beforeEach(inject(function($q) {
scope.attachment = {
links: {
author: {
fetch: function() {
deferred = $q.defer();
deferred.resolve({ props: { id: userId, name: userName} } );
return deferred.promise;
}
}
}
};
compile();
}));
it('should render element', function() {
expect(element.prop('tagName')).to.equal('TD');
});
it('should render link to user', function() {
var link = element.find('a');
expect(link.text()).to.equal(userName);
expect(link.attr('href')).to.equal('/users/' + userId);
});
});
});

@ -0,0 +1,67 @@
//-- 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.
//++
describe('attachmentsTitle Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.workPackages.directives'));
beforeEach(module('templates'));
beforeEach(inject(function($rootScope, $compile) {
var html;
html = '<attachments-title attachments="attachments"></attachments-title>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
describe('element', function() {
beforeEach(function() {
scope.attachments = [
{ filename: 'bomba' },
{ filename: 'clat' }
];
compile();
});
it('should render element', function() {
expect(element.prop('tagName')).to.equal('H3');
});
it('should render title', function() {
expect(element.text()).to.equal('Attachments (2)');
});
});
});

@ -0,0 +1,55 @@
//-- 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.
//++
/*jshint expr: true*/
describe('Latest items filter', function() {
beforeEach(module('openproject.workPackages.filters'));
describe('latestItems', function() {
var items;
beforeEach(function(){
items = [1,2,3,4,5,6,7,8,9];
});
it('should be defined', inject(function($filter) {
expect($filter('latestItems')).not.to.equal(null);
}));
it('should return the first 3 items', inject(function($filter) {
expect($filter('latestItems')(items, false, 3)).to.eql([9,8,7]);
}));
it('should return the last 3 items reversed', inject(function($filter) {
expect($filter('latestItems')(items, true, 3)).to.eql([1,2,3]);
}));
});
});

@ -0,0 +1,82 @@
//-- 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.
//++
/*jshint expr: true*/
describe('ActivityService', function() {
var $httpBackend, ActivityService;
beforeEach(module('openproject.api', 'openproject.services', 'openproject.models'));
beforeEach(inject(function(_$httpBackend_, _ActivityService_) {
$httpBackend = _$httpBackend_;
ActivityService = _ActivityService_;
}));
describe('createComment', function() {
var setupFunction;
var workPackageId = 5;
var actvityId = 10;
var activities = [];
var descending = false;
var comment = "Jack Bauer 24 hour power shower";
var apiResource;
var apiFetchResource;
beforeEach(inject(function($q) {
apiResource = {
fetch: function() {
deferred = $q.defer();
deferred.resolve({ id: actvityId, comment: comment } );
return deferred.promise;
}
}
}));
beforeEach(inject(function(HALAPIResource) {
setupFunction = sinon.stub(HALAPIResource, 'setup').returns(apiResource);
}));
beforeEach(inject(function() {
apiFetchResource = ActivityService.createComment(workPackageId, activities, descending, comment);
}));
it('makes an api setup call', function() {
expect(setupFunction).to.have.been.calledWith("/work_packages/" + workPackageId + "/activities");
});
it('returns an activity', function() {
apiFetchResource.then(function(activity){
expect(activity.id).to.equal(activityId);
expect(activity.comment).to.equal(comment);
expect(activities.length).to.equal(1);
});
});
});
});

@ -30,43 +30,42 @@
describe('TimezoneService', function() {
var TIME = '05/19/2014 11:49 AM';
var TIME = '2013-02-08T09:30:26';
var TimezoneService;
var ConfigurationService;
var isTimezoneSetStub;
var timezoneStub;
beforeEach(module('openproject.services'));
beforeEach(module('openproject.services', 'openproject.config'));
beforeEach(inject(function(_TimezoneService_){
beforeEach(inject(function(_TimezoneService_, _ConfigurationService_){
TimezoneService = _TimezoneService_;
ConfigurationService = _ConfigurationService_;
isTimezoneSetStub = sinon.stub(ConfigurationService, "isTimezoneSet");
timezoneStub = sinon.stub(ConfigurationService, "timezone");
}));
describe('#parseDate', function() {
it('is UTC', function() {
expect(TimezoneService.parseDate(TIME).zone()).to.equal(0);
var time = TimezoneService.parseDate(TIME);
expect(time.zone()).to.equal(0);
expect(time.format("HH:mm")).to.eq("09:30");
});
describe('Non-UTC timezone', function() {
var timezone = 'Europe/Berlin';
var momentStub;
var dateStub;
var timezone = 'America/Vancouver';
var date;
beforeEach(function() {
TimezoneService.setTimezone(timezone);
momentStub = sinon.stub(moment, "utc");
dateStub = sinon.stub();
momentStub.returns(dateStub);
dateStub.tz = sinon.spy();
TimezoneService.parseDate(TIME);
});
isTimezoneSetStub.returns(true);
timezoneStub.returns(timezone);
afterEach(function() {
momentStub.restore();
date = TimezoneService.parseDate(TIME);
});
it('is Europe/Berlin', function() {
expect(dateStub.tz.calledWithExactly(timezone)).to.be.true;
it('is ' + timezone, function() {
expect(date.format("HH:mm")).to.eq("01:30");
});
});
});

@ -35,8 +35,6 @@ var modalHelperInstance = {
jQuery.fn.slider = {};
var Raphael = {};
var possibleData = {
projects: [{
"id":1,

@ -0,0 +1,57 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'roar/decorator'
require 'roar/representer/json/hal'
module API
module Decorators
class Collection < Roar::Decorator
include Roar::Representer::JSON::HAL
include Roar::Representer::Feature::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
attr_reader :current_user, :as
def initialize(models, current_user: nil, as: nil)
@current_user = current_user
@as = as.to_s.camelize(:lower)
super(models)
end
as_strategy = API::Utilities::CamelCasingStrategy.new
property :total, as: :_total, exec_context: :decorator
def total
represented.first.model.class.count
end
end
end
end

@ -53,6 +53,11 @@ module API
raise API::Errors::Unauthorized.new(current_user) unless is_authorized && allow
is_authorized
end
def build_representer(obj, model_klass, representer_klass, options = {})
model = (obj.kind_of?(Array)) ? obj.map{ |o| model_klass.new(o) } : model_klass.new(obj)
representer_klass.new(model, options).to_json
end
end
rescue_from :all do |e|

@ -21,6 +21,31 @@ module API
@representer.to_json
end
helpers do
def save_activity(activity)
if activity.save
model = ::API::V3::Activities::ActivityModel.new(activity)
representer = ::API::V3::Activities::ActivityRepresenter.new(model)
representer.to_json
else
errors = activity.errors.full_messages.join(", ")
fail Errors::Validation.new(activity, description: errors)
end
end
end
params do
requires :comment, type: String
end
put do
authorize({ controller: :journals, action: :edit }, context: @activity.journable.project)
@activity.notes = params[:comment]
save_activity(@activity)
end
end
end

@ -83,7 +83,6 @@ module API
def render_details(journal, no_html: false)
journal.details.map{ |d| journal.render_detail(d, no_html: no_html) }
end
end
end
end

@ -35,12 +35,12 @@ module API
module V3
class Root < Grape::API
version 'v3', using: :path
mount ::API::V3::Activities::ActivitiesAPI
mount ::API::V3::Attachments::AttachmentsAPI
mount ::API::V3::Queries::QueriesAPI
mount ::API::V3::Users::UsersAPI
mount ::API::V3::WorkPackages::WorkPackagesAPI
end
end
end

@ -40,13 +40,27 @@ module API
self.as_strategy = API::Utilities::CamelCasingStrategy.new
def initialize(model, options = {}, *expand)
@current_user = options[:current_user]
@work_package = options[:work_package]
@expand = expand
super(model)
end
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_url}api/v3/users/#{represented.model.id}", title: "#{represented.model.name} - #{represented.model.login}" }
end
# will need array of links for work packages the user is watching
link :removeWatcher do
{
href: "#{root_url}/api/v3/work_packages/#{@work_package.id}/watchers/#{represented.model.id}",
method: :delete,
title: 'Remove watcher'
} if @work_package && current_user_allowed_to(:delete_work_package_watchers, @work_package)
end
property :id, getter: -> (*) { model.id }, render_nil: true
property :login, render_nil: true
@ -61,6 +75,10 @@ module API
def _type
'User'
end
def current_user_allowed_to(permission, work_package)
@current_user && @current_user.allowed_to?(permission, work_package.project)
end
end
end
end

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module Watchers
class WatchersRepresenter < ::API::Decorators::Collection
collection :watchers, as: -> (*) { as || :watchers }, exec_context: :decorator, embedded: true
def watchers
represented.map { |model| ::API::V3::Users::UserRepresenter.new(model) }
end
end
end
end
end

@ -0,0 +1,53 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'reform'
require 'reform/form/coercion'
module API
module V3
module WorkPackages
class RelationModel < Reform::Form
include Coercion
# NOTE: to avoid a naming collision with DelayedJob, we define an
# explicit method here rather than relying on the #property macro.
#
# @see Relation#delay
def delay
model.delay
end
def delay=(value)
model.delay = value
end
end
end
end
end

@ -0,0 +1,79 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'roar/decorator'
require 'roar/representer/json/hal'
module API
module V3
module WorkPackages
class RelationRepresenter < Roar::Decorator
include Roar::Representer::JSON::HAL
include Roar::Representer::Feature::Hypermedia
include OpenProject::StaticRouting::UrlHelpers
self.as_strategy = API::Utilities::CamelCasingStrategy.new
def initialize(model, options = {}, *expand)
@current_user = options[:current_user]
@work_package = options[:work_package]
@expand = expand
super(model)
end
property :_type, exec_context: :decorator
link :self do
{ href: "#{root_url}api/v3/relationships/#{represented.model.id}" }
end
link :relatedFrom do
{ href: "#{root_url}api/v3/work_packages/#{represented.model.from_id}" }
end
link :relatedTo do
{ href: "#{root_url}api/v3/work_packages/#{represented.model.to_id}" }
end
property :delay, getter: -> (*) { model.delay }, render_nil: true, if: -> (*) { model.relation_type == 'precedes' }
def _type
"Relation::#{relation_type}"
end
private
def relation_type
represented.model.relation_type_for(@work_package).camelize
end
end
end
end
end

@ -0,0 +1,62 @@
module API
module V3
module WorkPackages
class WatchersAPI < Grape::API
get '/available_watchers' do
available_watchers = @work_package.possible_watcher_users
build_representer(
available_watchers,
::API::V3::Users::UserModel,
::API::V3::Watchers::WatchersRepresenter,
as: :available_watchers
)
end
resources :watchers do
params do
requires :user_id, desc: 'The watcher\'s user id', type: Integer
end
post do
if current_user.id == params[:user_id]
authorize(:view_work_packages, context: @work_package.project)
else
authorize(:add_work_package_watchers, context: @work_package.project)
end
user = User.find params[:user_id]
Services::CreateWatcher.new(@work_package, user).run(
-> (result) { status(200) unless result[:created]},
-> (watcher) { raise ::API::Errors::Validation.new(watcher) }
)
build_representer(user, ::API::V3::Users::UserModel, ::API::V3::Users::UserRepresenter)
end
namespace ':user_id' do
params do
requires :user_id, desc: 'The watcher\'s user id', type: Integer
end
delete do
if current_user.id == params[:user_id]
authorize(:view_work_packages, context: @work_package.project)
else
authorize(:delete_work_package_watchers, context: @work_package.project)
end
user = User.find_by_id params[:user_id]
Services::RemoveWatcher.new(@work_package, user).run
status 204
end
end
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save