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 'grape', '~> 0.7.0'
gem 'representable', git: 'https://github.com/finnlabs/representable' gem 'representable', git: 'https://github.com/finnlabs/representable'
gem 'roar', '~> 0.12.6' 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 # 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 # board to compile the native ones. Note, that their use is discouraged, since

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

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

@ -39,6 +39,14 @@ angular.module('openproject.config')
userPreferencesPresent: function() { userPreferencesPresent: function() {
return this.settingsPresent() && gon.settings.hasOwnProperty('user_preferences'); 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() { accessibilityModeEnabled: function() {
if (!this.userPreferencesPresent()) { if (!this.userPreferencesPresent()) {
$log.error('User preferences are not available.'); $log.error('User preferences are not available.');
@ -54,6 +62,24 @@ angular.module('openproject.config')
} else { } else {
return gon.settings.user_preferences.others.comments_sorting === 'desc'; 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') 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.PathHelper = PathHelper;
$scope.messages = gon.messages; $scope.messages = gon.messages;
$scope.totalMessageCount = gon.total_count; $scope.totalMessageCount = gon.total_count;
@ -36,8 +36,6 @@ angular.module('openproject.messages.controllers')
$scope.projectId = gon.project_id; $scope.projectId = gon.project_id;
$scope.activityModuleEnabled = gon.activity_modul_enabled; $scope.activityModuleEnabled = gon.activity_modul_enabled;
TimezoneService.setTimezone(gon.timezone);
SortService.setColumn(gon.sort_column); SortService.setColumn(gon.sort_column);
SortService.setDirection(gon.sort_direction); 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>'; scope.authorLink = '<a href="'+ PathHelper.userPath(scope.author.id) + '">' + scope.author.name + '</a>';
if (scope.activity) { 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 { } else {
scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>'; scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>';
} }

@ -29,18 +29,47 @@
// TODO move to UI components // TODO move to UI components
angular.module('openproject.uiComponents') angular.module('openproject.uiComponents')
.directive('date', ['I18n', function(I18n) { .directive('date', ['I18n', 'TimezoneService', 'ConfigurationService', function(I18n, TimezoneService, ConfigurationService) {
return { return {
restrict: 'EA', restrict: 'EA',
replace: false, replace: true,
scope: { date: '=' }, scope: { dateValue: '=', hideTitle: '@' },
template: '<span>{{date}}</span>', template: '<span title="{{ dateTitle }}">{{date}}</span>',
link: function(scope, element, attrs) { 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 $compile(element.contents())(scope);
// time to the local timezone or user setting.
scope.time = moment(scope.dateTime).utc().format('LL');
} }
}; };
}]); });

@ -26,19 +26,10 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
// TODO move to UI components
angular.module('openproject.uiComponents') angular.module('openproject.uiComponents')
.directive('formattedDate', ['I18n', 'TimezoneService', function(I18n, TimezoneService) { .filter('latestItems', function(){
return { return function(items, isDescending, visible){
restrict: 'EA', return isDescending ? items.slice(0, visible) : items.slice(-visible).reverse();
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');
} }
}; });
}]);

@ -36,7 +36,19 @@ angular.module('openproject.helpers')
apiV3: '/api/v3', apiV3: '/api/v3',
staticBase: window.appBasePath ? window.appBasePath : '', 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'; var link = '/activity';
if (projectIdentifier) { if (projectIdentifier) {
@ -52,6 +64,9 @@ angular.module('openproject.helpers')
assetPath: function(assetIdentifier) { assetPath: function(assetIdentifier) {
return '/assets/' + assetIdentifier; return '/assets/' + assetIdentifier;
}, },
attachmentPath: function(attachmentId, fileName) {
return '/attachments/' + attachmentId + '/' + fileName;
},
boardsPath: function(projectIdentifier) { boardsPath: function(projectIdentifier) {
return PathHelper.projectPath(projectIdentifier) + '/boards'; return PathHelper.projectPath(projectIdentifier) + '/boards';
}, },
@ -220,6 +235,9 @@ angular.module('openproject.helpers')
}, },
// Static // Static
staticAttachmentPath: function(attachmentId, fileName) {
return PathHelper.staticBase + PathHelper.attachmentPath(attachmentId, fileName);
},
staticUserPath: function(userId) { staticUserPath: function(userId) {
return PathHelper.staticBase + PathHelper.userPath(userId); return PathHelper.staticBase + PathHelper.userPath(userId);
}, },

@ -123,8 +123,50 @@ angular.module('openproject.workPackages.helpers')
parseDateTime: function(value) { parseDateTime: function(value) {
return new Date(Date.parse(value.replace(/(A|P)M$/, ''))); 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; return WorkPackagesHelper;

@ -187,7 +187,7 @@ angular.module('openproject.models')
*/ */
setDefaultFilter: function() { setDefaultFilter: function() {
var statusOpenFilterData = this.getExtendedFilterData({name: 'status_id', operator: 'o'}); 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 // environment and other global vars
/*jshint browser:true, devel:true*/ /*jshint browser:true, devel:true*/
/*global jQuery:false, Raphael:false, Timeline:true*/ /*global angular:false, Timeline:true*/
angular.module('openproject.timelines.models') angular.module('openproject.timelines.models')

@ -71,6 +71,7 @@ angular.module('openproject')
}) })
.state('work-packages.list.details.overview', { .state('work-packages.list.details.overview', {
url: "/overview", url: "/overview",
controller: 'DetailsTabOverviewController',
templateUrl: "/templates/work_packages/tabs/overview.html", templateUrl: "/templates/work_packages/tabs/overview.html",
}) })
.state('work-packages.list.details.activity', { .state('work-packages.list.details.activity', {
@ -83,7 +84,8 @@ angular.module('openproject')
}) })
.state('work-packages.list.details.watchers', { .state('work-packages.list.details.watchers', {
url: "/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', { .state('work-packages.list.details.attachments', {
url: "/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') angular.module('openproject.services')
.service('TimezoneService', [function() { .service('TimezoneService', ['ConfigurationService', 'I18n', function(ConfigurationService, I18n) {
var timezoneOptions = {
name: ''
};
TimezoneService = { TimezoneService = {
setTimezone: function(name) {
timezoneOptions.name = name;
},
parseDate: function(date) { parseDate: function(date) {
var d = moment.utc(date, "MM/DD/YYYY/ HH:mm A"); var d = moment.utc(date);
if (timezoneOptions.name) { if (ConfigurationService.isTimezoneSet()) {
d.tz(timezoneOptions.name); d.local();
d.tz(ConfigurationService.timezone());
} }
return d; 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; 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') angular.module('openproject.workPackages.controllers')
.constant('DEFAULT_WORK_PACKAGE_PROPERTIES', [ .constant('VISIBLE_LATEST')
'status', 'assignee', 'responsible', .constant('RELATION_TYPES', {
'date', 'percentageDone', 'priority', relatedTo: "Relation::Relates",
'estimatedTime', 'versionName' duplicates: "Relation::Duplicates",
]) duplicated: "Relation::Duplicated",
.constant('USER_TYPE', 'user') blocks: "Relation::Blocks",
blocked: "Relation::Blocked",
precedes: "Relation::Precedes",
follows: "Relation::Follows"
})
.controller('WorkPackageDetailsController', [ .controller('WorkPackageDetailsController', [
'$scope', '$scope',
'latestTab', 'latestTab',
'workPackage', 'workPackage',
'I18n', 'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES', 'VISIBLE_LATEST',
'USER_TYPE', 'RELATION_TYPES',
'CustomFieldHelper',
'WorkPackagesHelper',
'PathHelper',
'UserService',
'$q', '$q',
'WorkPackagesHelper',
'ConfigurationService', '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){ $scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name); latestTab.registerState(toState.name);
}); });
$scope.$on('workPackageRefreshRequired', function(event, toState){
refreshWorkPackage();
});
// initialization // initialization
setWorkPackageScopeProperties(workPackage);
$scope.I18n = I18n; $scope.I18n = I18n;
$scope.workPackage = workPackage;
$scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id; $scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id;
$scope.maxDescriptionLength = 800; $scope.maxDescriptionLength = 800;
function refreshWorkPackage() {
workPackage.links.self
.fetch({force: true})
.then(setWorkPackageScopeProperties);
}
$scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers
// resources for tabs function outputError(error) {
$scope.$emit('flashMessage', {
// activities and latest activities isError: true,
text: error.message
});
}
$scope.outputError = outputError; // expose to child controllers
$scope.activities = workPackage.embedded.activities; function setWorkPackageScopeProperties(workPackage){
$scope.activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically) $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(); $scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
$scope.activities = displayedActivities($scope.workPackage);
// restore former order of actvities unless comments are to be sorted in descending order
if (!$scope.activitiesSortedInDescendingOrder) {
$scope.activities.reverse();
}
// watchers // watchers
$scope.watchers = workPackage.embedded.watchers; $scope.watchers = workPackage.embedded.watchers;
$scope.author = workPackage.embedded.author; $scope.author = workPackage.embedded.author;
// work package properties // Attachments
$scope.attachments = workPackage.embedded.attachments;
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES; // relations
$q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parent) {
function getPropertyValue(property, format) { $scope.wpParent = parent;
if (format === USER_TYPE) { });
return workPackage.embedded[property]; $q.all(WorkPackagesHelper.getChildren(workPackage)).then(function(children) {
} else { $scope.wpChildren = children;
return getFormattedPropertyValue(property); });
}
}
function getFormattedPropertyValue(property) { for (var key in RELATION_TYPES) {
if (property === 'date') { if (RELATION_TYPES.hasOwnProperty(key)) {
return getDateProperty(); (function(key) {
} else { $q.all(WorkPackagesHelper.getRelationsOfType(workPackage, RELATION_TYPES[key])).then(function(relations) {
return WorkPackagesHelper.formatWorkPackageProperty(workPackage.props[property], property); $scope[key] = relations;
});
})(key);
} }
} }
function getDateProperty() { // Author
if (workPackage.props.startDate || workPackage.props.dueDate) { $scope.author = workPackage.embedded.author;
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;
}
} }
function addFormattedValueToPresentProperties(property, label, value, format) { $scope.toggleWatch = function() {
var propertyData = { $scope.toggleWatchLink
property: property, .fetch({ ajax: $scope.toggleWatchLink.props })
label: label, .then(refreshWorkPackage, outputError);
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 workPackage.props[property];
})
.reduce(function(a, b) {
return a || b;
});
}
var userFields = ['assignee', 'author', 'responsible'];
(function setupWorkPackageProperties() { $scope.canViewWorkPackageWatchers = function() {
angular.forEach(workPackageProperties, function(property, index) { return !!($scope.workPackage && $scope.workPackage.embedded.watchers !== undefined);
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) { function displayedActivities(workPackage) {
if (!!customProperty.value && customProperty.format === USER_TYPE) { var activities = workPackage.embedded.activities;
return UserService.getUser(customProperty.value); activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
} else { if ($scope.activitiesSortedInDescendingOrder) {
return CustomFieldHelper.formatCustomFieldValue(customProperty.value, customProperty.format); 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 // toggles
$scope.toggleStates = { $scope.toggleStates = {

@ -60,19 +60,23 @@ angular.module('openproject.workPackages.controllers')
$scope.disableFilters = false; $scope.disableFilters = false;
$scope.disableNewWorkPackage = true; $scope.disableNewWorkPackage = true;
var getWorkPackages, params; var fetchWorkPackages;
if($scope.query_id){ if($scope.query_id){
getWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id); fetchWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id);
} else { } else {
getWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location); fetchWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location);
} }
$scope.settingUpPage = getWorkPackages.then(setupPage); $scope.settingUpPage = fetchWorkPackages // put promise in scope for cg-busy
.then(setupPage)
loadProjectTypesAndQueries(); .then(function() {
fetchAvailableColumns();
fetchProjectTypesAndQueries();
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
});
} }
function loadProjectTypesAndQueries() { function fetchProjectTypesAndQueries() {
if ($scope.projectIdentifier) { if ($scope.projectIdentifier) {
ProjectService.getProject($scope.projectIdentifier) ProjectService.getProject($scope.projectIdentifier)
.then(function(project) { .then(function(project) {
@ -82,16 +86,12 @@ angular.module('openproject.workPackages.controllers')
}); });
} }
QueryService.loadAvailableGroupedQueries($scope.projectIdentifier);
} }
function setupPage(json) { function setupPage(json) {
initQuery(json.meta); initQuery(json.meta);
setupWorkPackagesTable(json); setupWorkPackagesTable(json);
initAvailableColumns();
if (json.work_packages.length) { if (json.work_packages.length) {
$scope.preselectedWorkPackageId = json.work_packages[0].id; $scope.preselectedWorkPackageId = json.work_packages[0].id;
} }
@ -153,7 +153,7 @@ angular.module('openproject.workPackages.controllers')
AuthorisationService.initModelAuth("query", meta.query._links); AuthorisationService.initModelAuth("query", meta.query._links);
} }
function initAvailableColumns() { function fetchAvailableColumns() {
return QueryService.loadAvailableUnusedColumns($scope.projectIdentifier) return QueryService.loadAvailableUnusedColumns($scope.projectIdentifier)
.then(function(data){ .then(function(data){
$scope.availableUnusedColumns = data; $scope.availableUnusedColumns = data;
@ -187,7 +187,7 @@ angular.module('openproject.workPackages.controllers')
$scope.setQueryState = function(query_id) { $scope.setQueryState = function(query_id) {
$state.go('work-packages.list', { query_id: query_id }); $state.go('work-packages.list', { query_id: query_id });
} };
// More // 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') angular.module('openproject.workPackages.tabs')
.directive('userActivity', ['I18n', 'PathHelper', function(I18n, PathHelper) { .directive('userActivity', ['$uiViewScroll', 'I18n', 'PathHelper', 'ActivityService', function($uiViewScroll, I18n, PathHelper, ActivityService) {
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
require: '^?exclusiveEdit',
templateUrl: '/templates/work_packages/tabs/_user_activity.html', templateUrl: '/templates/work_packages/tabs/_user_activity.html',
scope: { scope: {
activity: '=', activity: '=',
currentAnchor: '=', activityNo: '=',
activityNo: '=' inputElementId: '='
}, },
link: function(scope) { link: function(scope, element, attrs, exclusiveEditController) {
exclusiveEditController.addEditable(scope);
scope.I18n = I18n; scope.I18n = I18n;
scope.userPath = PathHelper.staticUserPath; scope.userPath = PathHelper.staticUserPath;
scope.inEdit = false;
scope.inFocus = false;
scope.activity.links.user.fetch().then(function(user) { scope.activity.links.user.fetch().then(function(user) {
scope.userId = user.props.id; scope.userId = user.props.id;
scope.userName = user.props.name; scope.userName = user.props.name;
scope.userAvatar = user.props.avatar; 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; 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-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-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; } 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 .login-auth-provider-list
margin-top: -15px margin-top: -15px
margin-bottom: 10px 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/tabular
@import content/work_packages @import content/work_packages
@import content/work_packages_filters @import content/work_packages_filters
@import content/work_packages_relations
@import content/work_packages_table @import content/work_packages_table
@import content/work_packages_details_activities
@import content/work_packages_details_attachments
@import content/expandable_group_content @import content/expandable_group_content
@import content/control_colors @import content/control_colors
@import content/components_add_comments_default
@import content/components_add_comments_onclick
@import content/tabular @import content/tabular
@import content/headings @import content/headings
@import content/timelines @import content/timelines

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

@ -95,17 +95,27 @@ div
background: #ffffff background: #ffffff
padding: 0 10px 0 0px padding: 0 10px 0 0px
&.detail-panel-watchers &#detail-panel-watchers
float: left float: left
margin: 15px 0 0 0
width: 100% width: 100%
ul ul
margin: 10px 0 0 0 margin: 15px 0
padding: 0 padding: 0 0 15px 0
list-style-type: none list-style-type: none
li li
margin: 0 0 30px 0 clear: both
padding: 0 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 &.detail-panel-attributes
float: left float: left
@ -121,6 +131,7 @@ div
padding: 0 padding: 0
display: inline-table display: inline-table
min-width: 31% min-width: 31%
clear: left
label label
font-weight: bold font-weight: bold
@ -244,8 +255,10 @@ i
color: #ccc color: #ccc
margin: 0 0 0 -2px margin: 0 0 0 -2px
cursor: pointer cursor: pointer
&:hover &:hover, &.active
color: #f8d033 color: #f8d033
&.active:hover
color: #ccc
#tabs #tabs
position: relative position: relative
@ -285,6 +298,9 @@ i
.work-package-details-activities-activity-contents .work-package-details-activities-activity-contents
padding: 10px 0 padding: 10px 0
textarea
width: 100%
resize: none
@media only screen and (max-width: 1280px) @media only screen and (max-width: 1280px)
.split-view .work-packages--details, .work-packages--split-view .work-packages--details .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 { .tl-column {
left: 100px; left: 100px;
display: inline-block; display: inline-block;
line-height: 18px;
padding-bottom: 8px;
} }
#content .tl-word-ellipsis { #content .tl-word-ellipsis {

@ -27,11 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'concerns/omniauth_login'
class AccountController < ApplicationController class AccountController < ApplicationController
include CustomFieldsHelper include CustomFieldsHelper
include OmniauthLogin include Concerns::OmniauthLogin
# prevents login action to be filtered by check_if_login_required application scope filter # prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required skip_before_filter :check_if_login_required
@ -44,6 +42,8 @@ class AccountController < ApplicationController
def login def login
if User.current.logged? if User.current.logged?
redirect_to home_url redirect_to home_url
elsif Concerns::OmniauthLogin.direct_login?
redirect_to Concerns::OmniauthLogin.direct_login_provider_url
elsif request.post? elsif request.post?
authenticate_user authenticate_user
end end
@ -57,7 +57,8 @@ class AccountController < ApplicationController
# Enable user to choose a new password # Enable user to choose a new password
def lost_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] if params[:token]
@token = Token.find_by_action_and_value("recovery", params[:token].to_s) @token = Token.find_by_action_and_value("recovery", params[:token].to_s)
redirect_to(home_url) && return unless @token and !@token.expired? redirect_to(home_url) && return unless @token and !@token.expired?
@ -102,9 +103,7 @@ class AccountController < ApplicationController
# User self-registration # User self-registration
def register def register
unless Setting.self_registration? || pending_auth_source_registration? return self_registration_disabled unless allow_registration?
return self_registration_disabled
end
if request.get? if request.get?
session[:auth_source_registration] = nil session[:auth_source_registration] = nil
@ -115,7 +114,7 @@ class AccountController < ApplicationController
@user.register @user.register
if session[:auth_source_registration] if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source # 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) register_via_omniauth(@user, session, permitted_params)
else else
register_and_login_via_authsource(@user, session, permitted_params) register_and_login_via_authsource(@user, session, permitted_params)
@ -123,16 +122,30 @@ class AccountController < ApplicationController
else else
@user.attributes = permitted_params.user @user.attributes = permitted_params.user
@user.login = params[:user][:login] @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
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 end
# Token based account activation # Token based account activation
def activate 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) token = Token.find_by_action_and_value('register', params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired? redirect_to(home_url) && return unless token and !token.expired?
user = token.user user = token.user
@ -149,6 +162,8 @@ class AccountController < ApplicationController
# to change the password. # to change the password.
# When making changes here, also check MyController.change_password # When making changes here, also check MyController.change_password
def change_password def change_password
return render_404 if OpenProject::Configuration.disable_password_login?
@user = User.find_by_login(params[:username]) @user = User.find_by_login(params[:username])
@username = @user.login @username = @user.login
@ -185,8 +200,12 @@ class AccountController < ApplicationController
end end
def authenticate_user def authenticate_user
if OpenProject::Configuration.disable_password_login?
render_404
else
password_authentication(params[:username], params[:password]) password_authentication(params[:username], params[:password])
end end
end
def password_authentication(username, password) def password_authentication(username, password)
user = User.try_to_login(username, password) user = User.try_to_login(username, password)
@ -254,7 +273,11 @@ class AccountController < ApplicationController
end end
def pending_auth_source_registration? 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 end
def register_and_login_via_authsource(user, session, permitted_params) 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 # Differences being that it's not looking to the session and also existing
# queries will be augmented with the params data passed with them. # queries will be augmented with the params data passed with them.
module Api::Experimental::Concerns::QueryLoading module Api::Experimental::Concerns::QueryLoading
private
def init_query def init_query
if !params[:query_id].blank? if !params[:query_id].blank?
@query = Query.find(params[:query_id]) @query = Query.find(params[:query_id])
@query.project = @project if @query.project.nil? @query.project = @project if @query.project.nil?
else else
@query = Query.new({name: "_", :project => @project}) @query = Query.new({ name: "_", :project => @project },
:initialize_with_default_filter => no_query_params_provided?)
end end
prepare_query prepare_query
@query @query
@ -58,4 +62,8 @@ module Api::Experimental::Concerns::QueryLoading
# For the sake of not breaking from convention we encoding/decoding the sortation. # 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']} params[:sort].split(',').collect{|p| [p.split(':')[0], p.split(':')[1] || 'asc']}
end 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 end

@ -39,6 +39,7 @@ class BoardsController < ApplicationController
include SortHelper include SortHelper
include WatchersHelper include WatchersHelper
include PaginationHelper include PaginationHelper
include OpenProject::ClientPreferenceExtractor
def index def index
@boards = @project.boards @boards = @project.boards
@ -67,7 +68,7 @@ class BoardsController < ApplicationController
gon.sort_column = 'updated_on' gon.sort_column = 'updated_on'
gon.sort_direction = 'desc' gon.sort_direction = 'desc'
gon.total_count = @board.topics.count 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 @message = Message.new
render :action => 'show', :layout => !request.xhr? render :action => 'show', :layout => !request.xhr?

@ -1,6 +1,11 @@
## ##
# Intended to be used by the AccountController to handle omniauth logins # 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 def omniauth_login
auth_hash = request.env['omniauth.auth'] auth_hash = request.env['omniauth.auth']
@ -24,6 +29,28 @@ module OmniauthLogin
redirect_to :action => 'login' redirect_to :action => 'login'
end 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 private
# a user may login via omniauth and (if that user does not exist # 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 # When making changes here, also check AccountController.change_password
def change_password def change_password
return render_404 if OpenProject::Configuration.disable_password_login?
@user = User.current # required by "my" layout @user = User.current # required by "my" layout
@username = @user.login @username = @user.login
return if redirect_if_password_change_not_allowed_for(@user) return if redirect_if_password_change_not_allowed_for(@user)

@ -29,6 +29,7 @@
class SysController < ActionController::Base class SysController < ActionController::Base
before_filter :check_enabled before_filter :check_enabled
before_filter :require_basic_auth, :only => [ :repo_auth ]
def projects def projects
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') 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 render :nothing => true, :status => 404
end 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 protected
def check_enabled def check_enabled
@ -79,4 +93,37 @@ class SysController < ActionController::Base
return false return false
end end
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 end

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

@ -49,6 +49,7 @@ class WorkPackagesController < ApplicationController
include PaginationHelper include PaginationHelper
include SortHelper include SortHelper
include OpenProject::Concerns::Preview include OpenProject::Concerns::Preview
include OpenProject::ClientPreferenceExtractor
accept_key_auth :index, :show, :create, :update accept_key_auth :index, :show, :create, :update
@ -204,9 +205,7 @@ class WorkPackagesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
gon.settings = { gon.settings = client_preferences
user_preferences: current_user.pref
}
render :index, :locals => { :query => @query, render :index, :locals => { :query => @query,
:project => @project }, :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 :work_packages, # for css classes
:custom_fields # for show_value :custom_fields # for show_value
include OpenProject::LocaleHelper
# wrap in a lambda to allow changing at run-time # wrap in a lambda to allow changing at run-time
default :from => Proc.new { Setting.mail_from } default :from => Proc.new { Setting.mail_from }
@ -392,11 +394,6 @@ private
headers['References'] = "<#{self.class.generate_message_id(object, user)}>" headers['References'] = "<#{self.class.generate_message_id(object, user)}>"
end 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 # Prepends given fields with 'X-OpenProject-' to save some duplication
def open_project_headers(hash) def open_project_headers(hash)
hash.each { |key, value| headers["X-OpenProject-#{key}"] = value.to_s } hash.each { |key, value| headers["X-OpenProject-#{key}"] = value.to_s }

@ -33,13 +33,16 @@ class CopyProjectJob < Struct.new(:user,
:enabled_modules, :enabled_modules,
:associations_to_copy, :associations_to_copy,
:send_mails) :send_mails)
include OpenProject::LocaleHelper
def perform 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, target_project_params,
enabled_modules, enabled_modules,
associations_to_copy, associations_to_copy,
send_mails) send_mails)
end
if target_project if target_project
UserMailer.delay.copy_project_succeeded(user, source_project, target_project, errors) 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 # Tries to authenticate a user in the database via external auth source
# or password stored in the database # or password stored in the database
def self.try_authentication_for_existing_user(user, password) 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 if user.auth_source
# user has an external authentication method # user has an external authentication method
return nil unless user.auth_source.authenticate(user.login, password) 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 # Tries to authenticate with available sources and creates user on success
def self.try_authentication_and_create_user(login, password) def self.try_authentication_and_create_user(login, password)
return nil if OpenProject::Configuration.disable_password_login?
user = nil user = nil
attrs = AuthSource.authenticate(login, password) attrs = AuthSource.authenticate(login, password)
if attrs if attrs
@ -362,7 +364,8 @@ class User < Principal
# Does the backend storage allow this user to change their password? # Does the backend storage allow this user to change their password?
def change_password_allowed? 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 true if auth_source_id.blank?
return auth_source.allow_password_changes? return auth_source.allow_password_changes?
end end

@ -150,7 +150,7 @@ class WorkPackage < ActiveRecord::Base
# test_destroying_root_projects_should_clear_data # # test_destroying_root_projects_should_clear_data #
# for details. # # 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} 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 # * https://www.openproject.org/work_packages/7192
# * http://stackoverflow.com/questions/13112430/find-loaded-providers-for-omniauth # * http://stackoverflow.com/questions/13112430/find-loaded-providers-for-omniauth
auth_provider_html = call_hook :view_account_login_auth_provider 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 != '' %> <% 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> <h3 class="login-auth-providers-title"><span>
<%= I18n.t('account.login_with_auth_provider')%> <%= I18n.t('account.login_with_auth_provider')%>
</span></h3> </span></h3>
<% end %>
<div class="login-auth-provider-list"> <div class="login-auth-provider-list">
<%= auth_provider_html %> <%= auth_provider_html %>
</div> </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)) %> <% breadcrumb_paths(l(:label_login)) %>
<%= call_hook :view_account_login_top %> <%= call_hook :view_account_login_top %>
<%= form_tag({:action=> "login"}, autocomplete: 'off', id: 'login-form', class: 'form') do %> <div id="login-form" class="form">
<%= back_url_hidden_field_tag %>
<h1><%= I18n.t(:label_login) %></h1> <h1><%= I18n.t(:label_login) %></h1>
<hr class="form_separator"> <hr class="form_separator">
<% unless OpenProject::Configuration.disable_password_login? %>
<div class="attribute_wrapper"> <%= render partial: 'password_login_form' %>
<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 %> <% end %>
</div> <%= render partial: 'auth_providers' %>
<input type="submit" name="login" value="<%=l(:button_login)%>" class="button_highlight" /> </div>
<%= render :partial => 'auth_providers' %>
<%= javascript_tag "Form.Element.focus('username');" %>
<% end %>
<%= call_hook :view_account_login_bottom %> <%= 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> <a ng-href="{{PathHelper.messagePath(message.id)}}">{{message.subject}}</a>
</td> </td>
<td class="author"><a ng-href="{{PathHelper.userPath(message.author.id)}}">{{message.author.name}}</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="replies">{{message.replies_count}}</td>
<td class="last_message"> <td class="last_message">
<span ng-if="message.last_reply !== undefined"> <span ng-if="message.last_reply !== undefined">

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

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

@ -48,6 +48,8 @@ See doc/COPYRIGHT.rdoc for more details.
<em><%= l(:text_comma_separated) %></em></p> <em><%= l(:text_comma_separated) %></em></p>
<p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p> <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
<p><%= setting_check_box :repository_authentication_caching_enabled %></p>
</div> </div>
<fieldset class="box tabular settings"><legend><%= l(:text_work_packages_ref_in_commit_messages) %></legend> <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> <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), <%= form_tag(url_for(controller: '/work_packages/bulk', action: :update, ids: @work_packages),
method: :put) do %> method: :put) do %>

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

@ -170,6 +170,24 @@ default:
# * change the cipher key here in your configuration file # * change the cipher key here in your configuration file
# * encrypt data using 'rake db:encrypt RAILS_ENV=production' # * encrypt data using 'rake db:encrypt RAILS_ENV=production'
# database_cipher_key: # 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 # specific configuration options for production environment
# that overrides the default ones # that overrides the default ones

@ -1267,6 +1267,7 @@ de:
setting_plain_text_mail: "Nur reinen Text (kein HTML) senden" setting_plain_text_mail: "Nur reinen Text (kein HTML) senden"
setting_protocol: "Protokoll" setting_protocol: "Protokoll"
setting_repositories_encodings: "Kodierungen der Projektarchive" 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_repository_log_display_limit: "Maximale Anzahl anzuzeigender Revisionen in der Historie einer Datei"
setting_rest_api_enabled: "REST-Schnittstelle aktivieren" setting_rest_api_enabled: "REST-Schnittstelle aktivieren"
setting_self_registration: "Anmeldung ermöglicht" setting_self_registration: "Anmeldung ermöglicht"

@ -1255,6 +1255,7 @@ en:
setting_plain_text_mail: "Plain text mail (no HTML)" setting_plain_text_mail: "Plain text mail (no HTML)"
setting_protocol: "Protocol" setting_protocol: "Protocol"
setting_repositories_encodings: "Repositories encodings" 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_repository_log_display_limit: "Maximum number of revisions displayed on file log"
setting_rest_api_enabled: "Enable REST web service" setting_rest_api_enabled: "Enable REST web service"
setting_self_registration: "Self-registration" setting_self_registration: "Self-registration"

@ -31,9 +31,11 @@ de:
ajax: ajax:
hide: "Verbergen" hide: "Verbergen"
loading: "Lädt ..." loading: "Lädt ..."
button_add_watcher: "Beobachter hinzufügen"
button_check_all: "Alles auswählen" button_check_all: "Alles auswählen"
button_copy: "Kopieren" button_copy: "Kopieren"
button_delete: "Löschen" button_delete: "Löschen"
button_delete_watcher: "Lösche Beobachter"
button_duplicate: "Duplizieren" button_duplicate: "Duplizieren"
button_edit: "Bearbeiten" button_edit: "Bearbeiten"
button_log_time: "Aufwand buchen" button_log_time: "Aufwand buchen"
@ -52,6 +54,8 @@ de:
general_text_No: "Nein" general_text_No: "Nein"
general_text_Yes: "Ja" general_text_Yes: "Ja"
label_add_columns: "Ausgewählte Spalten hinzufügen" 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_by: "hinzugefügt von"
label_added_time_by: "Von %{author} %{age} hinzugefügt" label_added_time_by: "Von %{author} %{age} hinzugefügt"
label_ago: "vor" label_ago: "vor"
@ -66,12 +70,15 @@ de:
label_collapse_all: "Alle zuklappen" label_collapse_all: "Alle zuklappen"
label_commented_on: "kommentiert am" label_commented_on: "kommentiert am"
label_contains: "enthält" label_contains: "enthält"
label_date: "Datum"
label_descending: "Absteigend" label_descending: "Absteigend"
label_description: "Beschreibung" label_description: "Beschreibung"
label_equals: "ist" label_equals: "ist"
label_expand: "Aufklappen" label_expand: "Aufklappen"
label_expanded: "aufgeklappt" label_expanded: "aufgeklappt"
label_expand_all: "Alle aufklappen" label_expand_all: "Alle aufklappen"
label_filename: "Datei"
label_filesize: "Größe"
label_format_atom: "Atom" label_format_atom: "Atom"
label_format_csv: "CSV" label_format_csv: "CSV"
label_format_pdf: "PDF" label_format_pdf: "PDF"
@ -119,6 +126,9 @@ de:
label_today: "heute" label_today: "heute"
label_work_package: "Arbeitspaket" label_work_package: "Arbeitspaket"
label_total_progress: "Gesamtfortschritt" 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?" text_are_you_sure: "Sind Sie sicher?"
@ -150,6 +160,17 @@ de:
version: "Version" version: "Version"
watcher: "Beobachter" 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: select2:
input_too_short: input_too_short:
one: "Bitte geben Sie ein weiteres Zeichen ein" one: "Bitte geben Sie ein weiteres Zeichen ein"

@ -31,9 +31,11 @@ en:
ajax: ajax:
hide: "Hide" hide: "Hide"
loading: "Loading ..." loading: "Loading ..."
button_add_watcher: "Add watcher"
button_check_all: "Check all" button_check_all: "Check all"
button_copy: "Copy" button_copy: "Copy"
button_delete: "Delete" button_delete: "Delete"
button_delete_watcher: "Delete watcher"
button_duplicate: "Duplicate" button_duplicate: "Duplicate"
button_edit: "Edit" button_edit: "Edit"
button_log_time: "Log time" button_log_time: "Log time"
@ -52,6 +54,8 @@ en:
general_text_No: "No" general_text_No: "No"
general_text_Yes: "Yes" general_text_Yes: "Yes"
label_add_columns: "Add selected columns" 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_by: "added by"
label_added_time_by: "Added by %{author} %{age}" label_added_time_by: "Added by %{author} %{age}"
label_ago: "days ago" label_ago: "days ago"
@ -60,6 +64,7 @@ en:
label_ascending: "Ascending" label_ascending: "Ascending"
label_board_locked: "Locked" label_board_locked: "Locked"
label_board_sticky: "Sticky" label_board_sticky: "Sticky"
label_date: "Date"
label_descending: "Descending" label_descending: "Descending"
label_description: "Description" label_description: "Description"
label_closed_work_packages: "closed" label_closed_work_packages: "closed"
@ -72,6 +77,8 @@ en:
label_expand: "Expand" label_expand: "Expand"
label_expanded: "expanded" label_expanded: "expanded"
label_expand_all: "Expand all" label_expand_all: "Expand all"
label_filename: "File"
label_filesize: "Size"
label_format_atom: "Atom" label_format_atom: "Atom"
label_format_csv: "CSV" label_format_csv: "CSV"
label_format_pdf: "PDF" label_format_pdf: "PDF"
@ -118,6 +125,9 @@ en:
label_this_week: "this week" label_this_week: "this week"
label_work_package: "Work package" label_work_package: "Work package"
label_total_progress: "Total progress" 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?" text_are_you_sure: "Are you sure?"
@ -149,6 +159,17 @@ en:
version: "Version" version: "Version"
watcher: "Watcher" 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: select2:
input_too_short: input_too_short:
one: "Please enter one more character" one: "Please enter one more character"

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

@ -68,7 +68,7 @@ module Migration::Utils
journal_ids.each do |journal_id| journal_ids.each do |journal_id|
insert <<-SQL insert <<-SQL
INSERT INTO customizable_journals (journal_id, custom_field_id, value) 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 SQL
end end
end end

@ -103,6 +103,8 @@ See doc/COPYRIGHT.rdoc for more details.
* Fix: Asset require for plug-ins * Fix: Asset require for plug-ins
* Fix: at.who styling * Fix: at.who styling
* `#1030` Fix: New target version cannot be created from work package view
## 3.0.8 ## 3.0.8
* new version scheme * 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_git_command` (default: 'git')
* `scm_subversion_command` (default: 'git') * `scm_subversion_command` (default: 'git')
* `session_store`: `active_record_store`, `cache_store`, or `cookie_store` (default: cache_store) * `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` * `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_address`: SMTP server hostname, e.g. `smtp.example.net`
* `smtp_port`: SMTP server port. Common options are `25` and `587`. * `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_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` * `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`) * `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`) * `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 'optparse'
require 'find' require 'find'
require 'etc' require 'etc'
require 'json'
require 'net/http'
require 'uri'
Version = "1.3" Version = "1.4"
SUPPORTED_SCM = %w( Subversion Git Filesystem ) SUPPORTED_SCM = %w( Subversion Git Filesystem )
$verbose = 0 $verbose = 0
$quiet = false $quiet = false
$redmine_host = '' $openproject_host = ''
$repos_base = '' $repos_base = ''
$svn_owner = 'root' $svn_owner = 'root'
$svn_group = 'root' $svn_group = 'root'
@ -86,36 +89,37 @@ OptionParser.new do |opts|
opts.separator("") opts.separator("")
opts.separator("Required arguments:") opts.separator("Required arguments:")
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v} 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:", opts.on("-r", "--openproject-host HOST", "assume OpenProject is hosted on HOST. Examples:",
" -r redmine.example.net", " -r openproject.example.net",
" -r http://redmine.example.net", " -r http://openproject.example.net",
" -r https://redmine.example.net") {|v| $redmine_host = v} " -r https://openproject.example.net") {|v| $openproject_host = v}
opts.on("-k", "--key KEY", "use KEY as the Redmine API key") {|v| $api_key = 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("")
opts.separator("Options:") opts.separator("Options:")
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login", opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
"allows users to browse the repository within", "allows users to browse the repository within",
"Redmine even for private projects. If you want to", "OpenProject even for private projects. If you want to",
"share repositories through Redmine.pm, you need", "share repositories through OpenProject.pm, you need",
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false} "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("-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( "--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( "--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", 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", "reposman is able to create Git and Subversion",
"repositories.", "repositories.",
"For all other kind, you must specify a --command", "For all other kind, you must specify a --command",
"option") {|v| v.capitalize; log("Invalid SCM: #{v}", :exit => true) unless SUPPORTED_SCM.include?(v)} "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", "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.", "identifier will be appended to this url.",
"Examples:", "Examples:",
" -u https://example.net/svn", " -u https://example.net/svn",
" -u file:///var/svn/", " -u file:///var/svn/",
"if this option isn't set, reposman won't register", "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", opts.on("-c", "--command COMMAND", "use this command instead of 'svnadmin create' to",
"create a repository. This option can be used to", "create a repository. This option can be used to",
"create repositories other than subversion and git", "create repositories other than subversion and git",
@ -123,7 +127,7 @@ OptionParser.new do |opts|
"This command override the default creation for git", "This command override the default creation for git",
"and subversion.") {|v| $command = v} "and subversion.") {|v| $command = v}
opts.on("-f", "--force", "force repository creation even if the project", 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("-t", "--test", "only show what should be done") {$test = true}
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1} opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
opts.on("-v", "--verbose", "verbose") {$verbose += 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.on("-q", "--quiet", "no log") {$quiet = true}
opts.separator("") opts.separator("")
opts.separator("Examples:") opts.separator("Examples:")
opts.separator(" reposman.rb --svn-dir=/var/svn --redmine-host=redmine.example.net --scm Subversion") opts.separator(" reposman.rb --svn-dir=/var/svn --openproject-host=openproject.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 -s /var/git -r openproject.example.net -u http://svn.example.net --scm Git")
opts.separator("") 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! end.parse!
if $test if $test
@ -152,7 +156,7 @@ end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/) $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." puts "Required argument missing. Type 'reposman.rb --help' for usage."
exit 1 exit 1
end end
@ -161,28 +165,22 @@ unless File.directory?($repos_base)
log("directory '#{$repos_base}' doesn't exists", :exit => true) log("directory '#{$repos_base}' doesn't exists", :exit => true)
end end
begin log("querying OpenProject for projects...", :level => 1);
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);
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") $openproject_host.gsub!(/^/, "http://") unless $openproject_host.match("^https?://")
$redmine_host.gsub!(/\/$/, '') $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 begin
# Get all active projects that have the Repository module enabled # 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 rescue => e
log("Unable to connect to #{Project.site}: #{e}", :exit => true) log("Unable to connect to #{$openproject_host}: #{e}", :exit => true)
end end
if projects.nil? if projects.nil?
@ -195,8 +193,8 @@ def set_owner_and_rights(project, repos_path, &block)
if mswin? if mswin?
yield if block_given? yield if block_given?
else else
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid) 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 = project['is_public'] ? $public_mode : $private_mode
right = right.to_i(8) & 007777 right = right.to_i(8) & 007777
yield if block_given? yield if block_given?
Find.find(repos_path) do |f| Find.find(repos_path) do |f|
@ -221,17 +219,17 @@ def mswin?
end end
projects.each do |project| projects.each do |project|
log("treating project #{project.name}", :level => 1) log("treating project #{project['name']}", :level => 1)
if project.identifier.empty? if project['identifier'].empty?
log("\tno identifier for project #{project.name}") log("\tno identifier for project #{project['name']}")
next next
elsif not project.identifier.match(/^[a-z0-9\-_]+$/) elsif not project['identifier'].match(/^[a-z0-9\-_]+$/)
log("\tinvalid identifier for project #{project.name} : #{project.identifier}"); log("\tinvalid identifier for project #{project['name']} : #{project['identifier']}");
next; next;
end 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) if File.directory?(repos_path)
@ -239,7 +237,7 @@ projects.each do |project|
# rights before leaving # rights before leaving
other_read = other_read_right?(repos_path) other_read = other_read_right?(repos_path)
owner = owner_name(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 if $test
log("\tchange mode on #{repos_path}") log("\tchange mode on #{repos_path}")
@ -256,18 +254,18 @@ projects.each do |project|
log("\tmode change on #{repos_path}"); log("\tmode change on #{repos_path}");
else 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 # unless user use -f with reposman
if $force == false and project.respond_to?(:repository) if $force == false and project.has_key?('repository')
log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1) log("\trepository for project #{project['identifier']} already exists in OpenProject", :level => 1)
next next
end end
project.is_public ? File.umask(0002) : File.umask(0007) project['is_public'] ? File.umask(0002) : File.umask(0007)
if $test if $test
log("\tcreate repository #{repos_path}") 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 next
end end
@ -286,10 +284,13 @@ projects.each do |project|
if $svn_url if $svn_url
begin begin
project.post(:repository, :vendor => $scm, :repository => {:url => "#{$svn_url}#{project.identifier}"}, :key => $api_key) http.post("#{api_uri.path}/projects/#{project['identifier']}/repository.json?" +
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}"); "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 rescue => e
log("\trepository #{repos_path} not registered in Redmine: #{e.message}"); log("\trepository #{repos_path} not registered in OpenProject: #{e.message}");
end end
end end

@ -29,14 +29,18 @@ module.exports = function(config) {
"vendor/assets/components/angular-truncate/src/truncate.js", "vendor/assets/components/angular-truncate/src/truncate.js",
"vendor/assets/components/angular-sanitize/angular-sanitize.js", "vendor/assets/components/angular-sanitize/angular-sanitize.js",
"vendor/assets/components/momentjs/moment.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/angular-context-menu/dist/angular-context-menu.js",
'vendor/assets/components/select2/select2.js', 'vendor/assets/components/select2/select2.js',
'vendor/assets/components/hyperagent/dist/hyperagent.js', 'vendor/assets/components/hyperagent/dist/hyperagent.js',
"vendor/assets/components/openproject-ui_components/app/assets/javascripts/angular/ui-components-app.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/openproject-app.js",
"app/assets/javascripts/angular/config/work-packages-config.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/controllers/**/*.js",
"app/assets/javascripts/angular/dialogs/**/*.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: { 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; return false;
} }
}, },
UserService: UserService, WorkPackagesDetailsHelper: {
CustomFieldHelper: CustomFieldHelper, attachmentsTitle: function() { return ''; }
},
workPackage: buildWorkPackageWithId(workPackageId), workPackage: buildWorkPackageWithId(workPackageId),
}); });
@ -92,206 +121,38 @@ describe('WorkPackageDetailsController', function() {
}); });
}); });
describe('work package properties', function() { describe('#scope.canViewWorkPackageWatchers', function() {
function fetchPresentPropertiesWithName(propertyName) { describe('when the work package does not contain the embedded watchers property', function() {
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() { beforeEach(function() {
sinon.stub(I18n, 't') workPackage.embedded.watchers = undefined;
.withArgs('js.label_no_start_date')
.returns(placeholder);
workPackage.props.startDate = null;
workPackage.props.dueDate = dueDate;
buildController(); buildController();
}); })
afterEach(function() {
I18n.t.restore();
});
it('renders the due date and a placeholder for the start date as date property', function() { it('returns false', function() {
expect(fetchPresentPropertiesWithName('date')[0].value).to.equal(placeholder + ' - Jul 10, 2014'); 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() { beforeEach(function() {
sinon.stub(I18n, 't') workPackage.embedded.watchers = [];
.withArgs('js.label_no_due_date')
.returns(placeholder);
workPackage.props.startDate = startDate;
workPackage.props.dueDate = null;
buildController(); buildController();
}); })
afterEach(function() { it('returns true', function() {
I18n.t.restore(); 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() { describe('work package properties', function() {
beforeEach(function() { describe('relations', 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() { beforeEach(function() {
workPackage.props.customProperties[0].value = userId;
workPackage.props.customProperties[0].format = 'user';
getUserSpy = sinon.spy(UserService, 'getUser');
buildController(); buildController();
}); });
it('fetches the user using the user service', function() { it('Relation::Relates', function() {
expect(UserService.getUser.calledWith(userId)).to.be.true; 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() { describe('TimezoneService', function() {
var TIME = '05/19/2014 11:49 AM'; var TIME = '2013-02-08T09:30:26';
var TimezoneService; 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_; TimezoneService = _TimezoneService_;
ConfigurationService = _ConfigurationService_;
isTimezoneSetStub = sinon.stub(ConfigurationService, "isTimezoneSet");
timezoneStub = sinon.stub(ConfigurationService, "timezone");
})); }));
describe('#parseDate', function() { describe('#parseDate', function() {
it('is UTC', 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() { describe('Non-UTC timezone', function() {
var timezone = 'Europe/Berlin'; var timezone = 'America/Vancouver';
var momentStub; var date;
var dateStub;
beforeEach(function() { beforeEach(function() {
TimezoneService.setTimezone(timezone); isTimezoneSetStub.returns(true);
timezoneStub.returns(timezone);
momentStub = sinon.stub(moment, "utc");
dateStub = sinon.stub();
momentStub.returns(dateStub);
dateStub.tz = sinon.spy();
TimezoneService.parseDate(TIME);
});
afterEach(function() { date = TimezoneService.parseDate(TIME);
momentStub.restore();
}); });
it('is Europe/Berlin', function() { it('is ' + timezone, function() {
expect(dateStub.tz.calledWithExactly(timezone)).to.be.true; expect(date.format("HH:mm")).to.eq("01:30");
}); });
}); });
}); });

@ -35,8 +35,6 @@ var modalHelperInstance = {
jQuery.fn.slider = {}; jQuery.fn.slider = {};
var Raphael = {};
var possibleData = { var possibleData = {
projects: [{ projects: [{
"id":1, "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 raise API::Errors::Unauthorized.new(current_user) unless is_authorized && allow
is_authorized is_authorized
end 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 end
rescue_from :all do |e| rescue_from :all do |e|

@ -21,6 +21,31 @@ module API
@representer.to_json @representer.to_json
end 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
end end

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

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

@ -40,13 +40,27 @@ module API
self.as_strategy = API::Utilities::CamelCasingStrategy.new 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 property :_type, exec_context: :decorator
link :self do link :self do
{ href: "#{root_url}api/v3/users/#{represented.model.id}", title: "#{represented.model.name} - #{represented.model.login}" } { href: "#{root_url}api/v3/users/#{represented.model.id}", title: "#{represented.model.name} - #{represented.model.login}" }
end 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 :id, getter: -> (*) { model.id }, render_nil: true
property :login, render_nil: true property :login, render_nil: true
@ -61,6 +75,10 @@ module API
def _type def _type
'User' 'User'
end end
def current_user_allowed_to(permission, work_package)
@current_user && @current_user.allowed_to?(permission, work_package.project)
end
end end
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