Merge remote-tracking branch 'origin/dev' into feature/api-auxiliary-resources

Conflicts:
	lib/api/v3/work_packages/work_package_representer.rb
pull/1634/head
Jens Ulferts 10 years ago
commit 1c6b684594
  1. 10
      .teatro.yml
  2. 2
      Gemfile
  3. 52
      Gemfile.lock
  4. 10
      app/assets/javascripts/angular/directives/components/activity-comment-directive.js
  5. 31
      app/assets/javascripts/angular/directives/components/date-directive.js
  6. 12
      app/assets/javascripts/angular/helpers/components/path-helper.js
  7. 44
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  8. 6
      app/assets/javascripts/angular/helpers/url-params-helper.js
  9. 4
      app/assets/javascripts/angular/routing.js
  10. 22
      app/assets/javascripts/angular/services/activity-service.js
  11. 30
      app/assets/javascripts/angular/services/timezone-service.js
  12. 155
      app/assets/javascripts/angular/work_packages/controllers/details-tab-overview-controller.js
  13. 92
      app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js
  14. 214
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  15. 48
      app/assets/javascripts/angular/work_packages/tabs/attachment-file-size-directive.js
  16. 7
      app/assets/javascripts/angular/work_packages/tabs/attachment-title-cell-directive.js
  17. 3
      app/assets/javascripts/angular/work_packages/tabs/user-activity-directive.js
  18. 81
      app/assets/javascripts/angular/work_packages/tabs/work-package-relation-directive.js
  19. 3
      app/assets/stylesheets/content/_accounts.sass
  20. 7
      app/assets/stylesheets/content/_components_add_comments_default.md
  21. 44
      app/assets/stylesheets/content/_components_add_comments_default.sass
  22. 10
      app/assets/stylesheets/content/_components_add_comments_onclick.md
  23. 28
      app/assets/stylesheets/content/_components_add_comments_onclick.sass
  24. 87
      app/assets/stylesheets/content/_work_packages_details_attachments.sass
  25. 52
      app/assets/stylesheets/content/_work_packages_relations.sass
  26. 5
      app/assets/stylesheets/default.css.sass
  27. 3
      app/assets/stylesheets/default_simple.css.sass
  28. 58
      app/assets/stylesheets/layout/_split_view.sass
  29. 67
      app/controllers/account_controller.rb
  30. 10
      app/controllers/api/experimental/concerns/query_loading.rb
  31. 12
      app/controllers/api/experimental/work_packages_controller.rb
  32. 90
      app/controllers/concerns/omniauth_login.rb
  33. 2
      app/controllers/my_controller.rb
  34. 2
      app/helpers/journals_helper.rb
  35. 1
      app/models/attachment.rb
  36. 6
      app/models/custom_field.rb
  37. 2
      app/models/journal.rb
  38. 7
      app/models/user.rb
  39. 12
      app/views/account/_auth_providers.html.erb
  40. 32
      app/views/account/_omniauth_login.html.erb
  41. 63
      app/views/account/_password_login_form.html.erb
  42. 41
      app/views/account/login.html.erb
  43. 12
      config/application.rb
  44. 2
      config/configuration.yml.example
  45. 13
      config/database.teatro.yml
  46. 28
      config/initializers/non_digest_assets.rb
  47. 28
      config/initializers/omniauth.rb
  48. 28
      config/initializers/reload_api.rb
  49. 28
      config/initializers/time_with_zone_as_json.rb
  50. 2
      config/locales/de.yml
  51. 2
      config/locales/en.yml
  52. 12
      config/locales/js-de.yml
  53. 12
      config/locales/js-en.yml
  54. 28
      db/migrate/20140411142338_clear_identity_urls_on_users.rb
  55. 28
      db/migrate/20140414141459_remove_openid_entirely.rb
  56. 28
      db/migrate/20140429152018_add_sessions_table.rb
  57. 28
      db/migrate/20140430125956_reset_content_types.rb
  58. 28
      db/migrate/20140602112515_drop_work_packages_priority_not_null_constraint.rb
  59. 28
      db/migrate/20140610125207_add_updated_at_index_to_work_packages.rb
  60. 11
      doc/CONFIGURATION.md
  61. 116
      doc/subversion_and_git_integration.md
  62. 61
      extra/svn/OpenProjectAuthentication.pm
  63. 295
      karma/tests/controllers/details-tab-overview-controller-test.js
  64. 220
      karma/tests/controllers/work-package-details-controller-test.js
  65. 18
      karma/tests/directives/components/date-time-directive-test.js
  66. 234
      karma/tests/directives/components/work-package-relation-directive-test.js
  67. 90
      karma/tests/directives/work_packages/attachment-file-size-directive-test.js
  68. 2
      karma/tests/directives/work_packages/attachment-title-cell-directive-test.js
  69. 35
      karma/tests/services/activity-service-test.js
  70. 2
      lib/api/decorators/collection.rb
  71. 23
      lib/api/root.rb
  72. 40
      lib/api/v3/activities/activities_api.rb
  73. 22
      lib/api/v3/activities/activity_representer.rb
  74. 28
      lib/api/v3/attachments/attachments_api.rb
  75. 28
      lib/api/v3/queries/queries_api.rb
  76. 8
      lib/api/v3/users/user_collection_representer.rb
  77. 28
      lib/api/v3/users/users_api.rb
  78. 53
      lib/api/v3/work_packages/relation_model.rb
  79. 79
      lib/api/v3/work_packages/relation_representer.rb
  80. 30
      lib/api/v3/work_packages/watchers_api.rb
  81. 4
      lib/api/v3/work_packages/work_package_model.rb
  82. 35
      lib/api/v3/work_packages/work_package_representer.rb
  83. 45
      lib/api/v3/work_packages/work_packages_api.rb
  84. 28
      lib/deprecated_alias.rb
  85. 19
      lib/generators/open_project/plugin/USAGE
  86. 72
      lib/generators/open_project/plugin/plugin_generator.rb
  87. 19
      lib/generators/open_project/plugin/templates/%full_name%.gemspec.tt
  88. 19
      lib/generators/open_project/plugin/templates/CHANGELOG.md
  89. 7
      lib/generators/open_project/plugin/templates/README.md.tt
  90. 1
      lib/generators/open_project/plugin/templates/lib/%full_name%.rb.tt
  91. 5
      lib/generators/open_project/plugin/templates/lib/open_project/%plugin_name%.rb.tt
  92. 16
      lib/generators/open_project/plugin/templates/lib/open_project/%plugin_name%/engine.rb.tt
  93. 5
      lib/generators/open_project/plugin/templates/lib/open_project/%plugin_name%/version.rb.tt
  94. 2
      lib/open_project/concerns/preview.rb
  95. 10
      lib/open_project/configuration.rb
  96. 45
      lib/open_project/configuration/helpers.rb
  97. 267
      lib/open_project/omni_auth/authorization.rb
  98. 35
      lib/open_project/plugins.rb
  99. 172
      lib/open_project/plugins/acts_as_op_engine.rb
  100. 41
      lib/open_project/plugins/load_dependency.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,10 @@
stage:
before:
- bower install --allow-root
- cp config/configuration.yml.example config/configuration.yml
- cp config/database.teatro.yml config/database.yml
- export SECRET_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
database:
- bundle exec rake db:create db:migrate
- bundle exec rake db:seed RAILS_ENV=development

@ -44,7 +44,7 @@ end
source 'https://rubygems.org'
gem "rails", "~> 3.2.18"
gem "rails", "~> 3.2.19"
gem "coderay", "~> 1.0.5"
gem "rubytree", "~> 0.8.3"

@ -65,12 +65,12 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actionmailer (3.2.18)
actionpack (= 3.2.18)
actionmailer (3.2.19)
actionpack (= 3.2.19)
mail (~> 2.5.4)
actionpack (3.2.18)
activemodel (= 3.2.18)
activesupport (= 3.2.18)
actionpack (3.2.19)
activemodel (= 3.2.19)
activesupport (= 3.2.19)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.4)
@ -78,20 +78,20 @@ GEM
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.2.1)
activemodel (3.2.18)
activesupport (= 3.2.18)
activemodel (3.2.19)
activesupport (= 3.2.19)
builder (~> 3.0.0)
activerecord (3.2.18)
activemodel (= 3.2.18)
activesupport (= 3.2.18)
activerecord (3.2.19)
activemodel (= 3.2.19)
activesupport (= 3.2.19)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activerecord-tableless (1.3.3)
activerecord (>= 2.3.0)
activeresource (3.2.18)
activemodel (= 3.2.18)
activesupport (= 3.2.18)
activesupport (3.2.18)
activeresource (3.2.19)
activemodel (= 3.2.19)
activesupport (= 3.2.19)
activesupport (3.2.19)
i18n (~> 0.6, >= 0.6.4)
multi_json (~> 1.0)
acts_as_list (0.2.0)
@ -308,27 +308,27 @@ GEM
rack_session_access (0.1.1)
builder (>= 2.0.0)
rack (>= 1.0.0)
rails (3.2.18)
actionmailer (= 3.2.18)
actionpack (= 3.2.18)
activerecord (= 3.2.18)
activeresource (= 3.2.18)
activesupport (= 3.2.18)
rails (3.2.19)
actionmailer (= 3.2.19)
actionpack (= 3.2.19)
activerecord (= 3.2.19)
activeresource (= 3.2.19)
activesupport (= 3.2.19)
bundler (~> 1.0)
railties (= 3.2.18)
railties (= 3.2.19)
rails-dev-tweaks (0.6.1)
actionpack (~> 3.1)
railties (~> 3.1)
rails_autolink (1.1.0)
rails (> 3.1)
railties (3.2.18)
actionpack (= 3.2.18)
activesupport (= 3.2.18)
railties (3.2.19)
actionpack (= 3.2.19)
activesupport (= 3.2.19)
rack-ssl (~> 1.3.2)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
rake (10.3.1)
rake (10.3.2)
rb-fsevent (0.9.4)
rb-inotify (0.9.4)
ffi (>= 0.5.0)
@ -492,7 +492,7 @@ DEPENDENCIES
rack-protection!
rack-test (~> 0.6.2)
rack_session_access
rails (~> 3.2.18)
rails (~> 3.2.19)
rails-dev-tweaks (~> 0.6.1)
rails_autolink
rb-readline (~> 0.5.1)

@ -41,13 +41,15 @@ angular.module('openproject.uiComponents')
link: function(scope, element, attrs) {
scope.title = I18n.t('js.label_add_comment_title');
scope.buttonTitle = I18n.t('js.label_add_comment');
scope.canAddComment = !!scope.workPackage.links.addComment;
scope.activity = { 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('');
ActivityService.createComment(scope.workPackage, scope.activities, descending, scope.activity.comment)
.then(function(response) {
scope.activity.comment = '';
scope.$emit('workPackageRefreshRequired', '');
return response;
});
}

@ -33,15 +33,12 @@ angular.module('openproject.uiComponents')
return {
restrict: 'EA',
replace: true,
scope: { dateValue: '=' },
template: '<span>{{date}}</span>',
scope: { dateValue: '=', hideTitle: '@' },
template: '<span title="{{ dateTitle }}">{{date}}</span>',
link: function(scope, element, attrs) {
if (ConfigurationService.dateFormatPresent()) {
scope.date = TimezoneService.parseDate(scope.dateValue).format(ConfigurationService.dateFormat());
} else {
moment.lang(I18n.locale);
scope.date = TimezoneService.parseDate(scope.dateValue).format('L');
scope.date = TimezoneService.formattedDate(scope.dateValue);
if (!scope.hideTitle) {
scope.dateTitle = scope.date;
}
}
};
@ -51,15 +48,12 @@ angular.module('openproject.uiComponents')
return {
restrict: 'EA',
replace: true,
scope: { timeValue: '=' },
template: '<span>{{time}}</span>',
scope: { timeValue: '=', hideTitle: '@' },
template: '<span title="{{ timeTitle }}">{{time}}</span>',
link: function(scope, element, attrs) {
if (ConfigurationService.timeFormatPresent()) {
scope.time = TimezoneService.parseDate(scope.timeValue).format(ConfigurationService.timeFormat());
} else {
moment.lang(I18n.locale);
scope.time = TimezoneService.parseDate(scope.timeValue).format('LT');
scope.time = TimezoneService.formattedTime(scope.timeValue);
if (!scope.hideTitle) {
scope.timeTitle = scope.time;
}
}
};
@ -70,8 +64,11 @@ angular.module('openproject.uiComponents')
restrict: 'EA',
replace: true,
scope: { dateTimeValue: '=' },
template: '<span><date date-value="dateTimeValue"></date> <time time-value="dateTimeValue"></time></span>',
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);
$compile(element.contents())(scope);
}
};

@ -36,18 +36,6 @@ angular.module('openproject.helpers')
apiV3: '/api/v3',
staticBase: window.appBasePath ? window.appBasePath : '',
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';

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

@ -52,11 +52,6 @@ angular.module('openproject.helpers')
},
buildQueryExportOptions: function(query){
// Note: This is all rather hard-codey
// The alternative would be to pass back export URLs from the server with the meta data but given that columns
// can be added/removed without making a further work packages index call the meta data wouldn't be up to date.
// Therefor I think it makes sense to build up the URL from the javascript query object and let the server build
// up the exact query from the params.
var relativeUrl = "/work_packages";
if (query.project_id){
relativeUrl = "/projects/" + query.project_id + relativeUrl;
@ -72,6 +67,7 @@ angular.module('openproject.helpers')
url = url + query.getQueryString();
return {
identifier: format.identifier,
label: I18n.t('js.' + format.label_locale),
format: format.format,
url: url

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

@ -33,8 +33,7 @@ angular.module('openproject.services')
'PathHelper', function(HALAPIResource, $http, PathHelper){
var ActivityService = {
createComment: function(workPackageId, activities, descending, comment) {
var resource = HALAPIResource.setup(PathHelper.activitiesPath(workPackageId));
createComment: function(workPackage, activities, descending, comment) {
var options = {
ajax: {
method: "POST",
@ -42,29 +41,18 @@ angular.module('openproject.services')
}
};
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;
}
});
return workPackage.links.addComment.fetch(options);
},
updateComment: function(activityId, comment) {
var resource = HALAPIResource.setup(PathHelper.activityPath(activityId));
updateComment: function(activity, comment) {
var options = {
ajax: {
method: "PUT",
method: 'PATCH',
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
return activity.links.update.fetch(options).then(function(activity){
return activity;
});
}

@ -28,7 +28,7 @@
angular.module('openproject.services')
.service('TimezoneService', ['ConfigurationService', function(ConfigurationService) {
.service('TimezoneService', ['ConfigurationService', 'I18n', function(ConfigurationService, I18n) {
TimezoneService = {
parseDate: function(date) {
var d = moment.utc(date);
@ -40,6 +40,34 @@ angular.module('openproject.services')
return d;
},
formattedDate: function(date) {
var date;
if (ConfigurationService.dateFormatPresent()) {
date = TimezoneService.parseDate(date).format(ConfigurationService.dateFormat());
} else {
moment.lang(I18n.locale);
date = TimezoneService.parseDate(date).format('L');
}
return date;
},
formattedTime: function(date) {
var time;
if (ConfigurationService.timeFormatPresent()) {
time = TimezoneService.parseDate(date).format(ConfigurationService.timeFormat());
} else {
moment.lang(I18n.locale);
time = TimezoneService.parseDate(date).format('LT');
}
return time;
},
};
return TimezoneService;

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

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

@ -28,30 +28,28 @@
angular.module('openproject.workPackages.controllers')
.constant('DEFAULT_WORK_PACKAGE_PROPERTIES', [
'status', 'assignee', 'responsible',
'date', 'percentageDone', 'priority',
'estimatedTime', 'versionName'
])
.constant('USER_TYPE', 'user')
.constant('VISIBLE_LATEST')
.constant('RELATION_TYPES', {
relatedTo: "Relation::Relates",
duplicates: "Relation::Duplicates",
duplicated: "Relation::Duplicated",
blocks: "Relation::Blocks",
blocked: "Relation::Blocked",
precedes: "Relation::Precedes",
follows: "Relation::Follows"
})
.controller('WorkPackageDetailsController', [
'$scope',
'latestTab',
'workPackage',
'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES',
'USER_TYPE',
'VISIBLE_LATEST',
'CustomFieldHelper',
'WorkPackagesHelper',
'PathHelper',
'UserService',
'RELATION_TYPES',
'$q',
'WorkPackagesHelper',
'ConfigurationService',
function($scope, latestTab, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, VISIBLE_LATEST, CustomFieldHelper, WorkPackagesHelper, PathHelper, UserService, $q, ConfigurationService) {
function($scope, latestTab, workPackage, I18n, VISIBLE_LATEST, RELATION_TYPES, $q, WorkPackagesHelper, ConfigurationService) {
$scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name);
});
@ -72,6 +70,7 @@ angular.module('openproject.workPackages.controllers')
.fetch({force: true})
.then(setWorkPackageScopeProperties);
}
$scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers
function outputError(error) {
$scope.$emit('flashMessage', {
@ -79,73 +78,7 @@ angular.module('openproject.workPackages.controllers')
text: error.message
});
}
$scope.toggleWatch = function() {
$scope.toggleWatchLink
.fetch({ ajax: $scope.toggleWatchLink.props })
.then(refreshWorkPackage, outputError);
};
// available watchers
$scope.$watch('watchers.length', 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() {
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) {
workPackage.link('addWatcher', {user_id: id})
.fetch({ajax: {method: 'POST'}})
.then(refreshWorkPackage, outputError)
};
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
$scope.outputError = outputError; // expose to child controllers
function setWorkPackageScopeProperties(workPackage){
$scope.workPackage = workPackage;
@ -157,6 +90,7 @@ angular.module('openproject.workPackages.controllers')
// activities and latest activities
$scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
$scope.activities = displayedActivities($scope.workPackage);
// watchers
$scope.watchers = workPackage.embedded.watchers;
@ -165,16 +99,38 @@ angular.module('openproject.workPackages.controllers')
// Attachments
$scope.attachments = workPackage.embedded.attachments;
// relations
$q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parent) {
$scope.wpParent = parent;
});
$q.all(WorkPackagesHelper.getChildren(workPackage)).then(function(children) {
$scope.wpChildren = children;
});
for (var key in RELATION_TYPES) {
if (RELATION_TYPES.hasOwnProperty(key)) {
(function(key) {
$q.all(WorkPackagesHelper.getRelationsOfType(workPackage, RELATION_TYPES[key])).then(function(relations) {
$scope[key] = relations;
});
})(key);
}
}
// Author
$scope.author = workPackage.embedded.author;
}
$scope.deleteWatcher = function(watcher) {
watcher.links.removeWatcher
.fetch({ ajax: watcher.links.removeWatcher.props })
$scope.toggleWatch = function() {
$scope.toggleWatchLink
.fetch({ ajax: $scope.toggleWatchLink.props })
.then(refreshWorkPackage, outputError);
};
$scope.canViewWorkPackageWatchers = function() {
return !!($scope.workPackage && $scope.workPackage.embedded.watchers !== undefined);
};
function displayedActivities(workPackage) {
var activities = workPackage.embedded.activities;
activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
@ -184,96 +140,6 @@ angular.module('openproject.workPackages.controllers')
return activities;
}
function getPropertyValue(property, format) {
if (format === USER_TYPE) {
return workPackage.embedded[property];
} else {
return getFormattedPropertyValue(property);
}
}
function getFormattedPropertyValue(property) {
if (property === 'date') {
return getDateProperty();
} else {
return WorkPackagesHelper.formatWorkPackageProperty(workPackage.props[property], property);
}
}
function getDateProperty() {
if (workPackage.props.startDate || workPackage.props.dueDate) {
var displayedStartDate = WorkPackagesHelper.formatWorkPackageProperty(workPackage.props.startDate, 'startDate') || I18n.t('js.label_no_start_date'),
displayedEndDate = WorkPackagesHelper.formatWorkPackageProperty(workPackage.props.dueDate, 'dueDate') || I18n.t('js.label_no_due_date');
return displayedStartDate + ' - ' + displayedEndDate;
}
}
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 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(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 = {

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

@ -38,13 +38,6 @@ angular.module('openproject.workPackages.directives')
},
link: function(scope, element, attributes) {
scope.attachmentPath = PathHelper.staticAttachmentPath(scope.attachment.props.id, scope.attachment.props.fileName);
scope.displayTitle = scope.attachment.props.fileName + " (" + formattedFilesize(scope.attachment.props.fileSize) + ")";
function formattedFilesize(fileSize) {
var size = parseFloat(fileSize);
return isNaN(size) ? "0kB" : (size / 1000).toFixed(2) + "kB";
};
}
};
}]);

@ -46,6 +46,7 @@ angular.module('openproject.workPackages.tabs')
scope.userPath = PathHelper.staticUserPath;
scope.inEdit = false;
scope.inFocus = false;
scope.userCanEdit = !!scope.activity.links.update;
scope.activity.links.user.fetch().then(function(user) {
scope.userId = user.props.id;
@ -70,7 +71,7 @@ angular.module('openproject.workPackages.tabs')
scope.updateComment = function(comment) {
var comment = angular.element('#edit-comment-text').val();
ActivityService.updateComment(scope.activity.props.id, comment).then(function(activity){
ActivityService.updateComment(scope.activity, comment).then(function(activity){
scope.$emit('workPackageRefreshRequired', '');
scope.inEdit = false;
});

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

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

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

@ -0,0 +1,44 @@
/*-- copyright
* OpenProject is a project management system.
* Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 3.
*
* OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
* Copyright (C) 2006-2013 Jean-Philippe Lang
* Copyright (C) 2010-2013 the ChiliProject Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* See doc/COPYRIGHT.rdoc for more details. ++*/
.activity-comment
textarea
border: 1px solid #cacaca
background: #ffffff
border-radius: 2px
padding: 8px
font-family: $font_family_normal
font-size: $global_font_size
width: 100%
box-sizing: border-box
&:hover
border: 1px solid #aaaaaa
&:focus
border: 1px solid #aaaaaa
box-shadow: 1px 1px 1px #dddddd inset
.add-comment-text
resize: none

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

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

@ -0,0 +1,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,10 +55,15 @@
@import content/tabular
@import content/work_packages
@import content/work_packages_filters
@import content/work_packages_relations
@import content/work_packages_table
@import content/work_packages_details_activities
@import content/work_packages_details_attachments
@import content/expandable_group_content
@import content/control_colors
@import content/components_add_comments_default
@import content/components_add_comments_onclick
@import content/tabular
@import content/headings
@import content/timelines

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

@ -114,6 +114,8 @@ div
padding-bottom: 10px
fieldset
border: 0
#detail-panel-watchers-add-watcher
clear: left
&.detail-panel-attributes
float: left
@ -168,62 +170,6 @@ div.detail-panel-latest-activity
margin: 0 0 20px 0
padding: 0
.attachments-container
float: left
margin: 0 0 30px 0
width: 100%
.attachments-container ul
margin: 0
padding: 0
list-style-type: none
.attachments-container ul li
margin: 0
padding: 0
line-height: 20px
.add-file
float: left
padding: 8px 0 0 10px
.add-file 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
.attachments-container table
padding: 0
margin: 0px 0 10px 0
float: left
border-collapse: collapse
border: 0px solid #ddd
.attachments-container table tr 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
.attachments-container table tr td
text-align: left
font-weight: normal
border-bottom: 0px solid #ddd
padding: 6px 10px 6px 0
.attachments-container table tr:hover
background: #ffffae
img
&.avatar
width: 36px

@ -27,8 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'concerns/omniauth_login'
class AccountController < ApplicationController
include CustomFieldsHelper
include Concerns::OmniauthLogin
@ -45,7 +43,11 @@ class AccountController < ApplicationController
if User.current.logged?
redirect_to home_url
elsif Concerns::OmniauthLogin.direct_login?
redirect_to Concerns::OmniauthLogin.direct_login_provider_url
ps = {}.tap do |p|
p[:origin] = params[:back_url] if params[:back_url]
end
redirect_to Concerns::OmniauthLogin.direct_login_provider_url(ps)
elsif request.post?
authenticate_user
end
@ -59,7 +61,8 @@ class AccountController < ApplicationController
# Enable user to choose a new password
def lost_password
redirect_to(home_url) && return unless Setting.lost_password?
return redirect_to(home_url) unless allow_lost_password_recovery?
if params[:token]
@token = Token.find_by_action_and_value("recovery", params[:token].to_s)
redirect_to(home_url) && return unless @token and !@token.expired?
@ -104,9 +107,7 @@ class AccountController < ApplicationController
# User self-registration
def register
unless Setting.self_registration? || pending_auth_source_registration?
return self_registration_disabled
end
return self_registration_disabled unless allow_registration?
if request.get?
session[:auth_source_registration] = nil
@ -117,7 +118,7 @@ class AccountController < ApplicationController
@user.register
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if session[:auth_source_registration][:omniauth]
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
@ -125,16 +126,30 @@ class AccountController < ApplicationController
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting(@user)
register_user_according_to_setting @user
end
end
end
def allow_registration?
allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login?
get = request.get? && allow
post = request.post? && (session[:auth_source_registration] || allow)
get || post
end
def allow_lost_password_recovery?
Setting.lost_password? && !OpenProject::Configuration.disable_password_login?
end
# Token based account activation
def activate
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
return redirect_to(home_url) unless Setting.self_registration? && params[:token]
token = Token.find_by_action_and_value('register', params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
@ -151,6 +166,8 @@ class AccountController < ApplicationController
# to change the password.
# When making changes here, also check MyController.change_password
def change_password
return render_404 if OpenProject::Configuration.disable_password_login?
@user = User.find_by_login(params[:username])
@username = @user.login
@ -187,7 +204,11 @@ class AccountController < ApplicationController
end
def authenticate_user
password_authentication(params[:username], params[:password])
if OpenProject::Configuration.disable_password_login?
render_404
else
password_authentication(params[:username], params[:password])
end
end
def password_authentication(username, password)
@ -256,7 +277,11 @@ class AccountController < ApplicationController
end
def pending_auth_source_registration?
session[:auth_source_registration] && !session[:auth_source_registration][:omniauth]
session[:auth_source_registration] && !pending_omniauth_registration?
end
def pending_omniauth_registration?
Hash(session[:auth_source_registration])[:omniauth]
end
def register_and_login_via_authsource(user, session, permitted_params)
@ -299,21 +324,21 @@ class AccountController < ApplicationController
end
# Register a user depending on Setting.self_registration
def register_user_according_to_setting(user, &block)
def register_user_according_to_setting(user, opts = {}, &block)
case Setting.self_registration
when '1'
register_by_email_activation(user, &block)
register_by_email_activation(user, opts, &block)
when '3'
register_automatically(user, &block)
register_automatically(user, opts, &block)
else
register_manually_by_administrator(user, &block)
register_manually_by_administrator(user, opts, &block)
end
end
# Register a user for email activation.
#
# Pass a block for behavior when a user fails to save
def register_by_email_activation(user, &block)
def register_by_email_activation(user, opts = {})
token = Token.new(:user => user, :action => "register")
if user.save and token.save
UserMailer.user_signed_up(token).deliver
@ -327,13 +352,15 @@ class AccountController < ApplicationController
# Automatically register a user
#
# Pass a block for behavior when a user fails to save
def register_automatically(user, &block)
def register_automatically(user, opts = {})
# Automatic activation
user.activate
user.last_login_on = Time.now
if user.save
self.logged_user = user
opts[:after_login].call user if opts[:after_login]
flash[:notice] = l(:notice_account_registered_and_logged_in)
redirect_after_login(user)
else
@ -344,7 +371,7 @@ class AccountController < ApplicationController
# Manual activation by the administrator
#
# Pass a block for behavior when a user fails to save
def register_manually_by_administrator(user, &block)
def register_manually_by_administrator(user, opts = {})
if user.save
# Sends an email to the administrators
admins = User.admin.active

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

@ -214,13 +214,13 @@ module Api
end
def export_formats
export_formats = [{ format: "atom", label_locale: "label_format_atom" },
{ format: "pdf", label_locale: "label_format_pdf"},
{ format: "pdf", label_locale: "label_format_pdf_with_descriptions", flags: ["show_descriptions"]},
{ format: "csv", label_locale: "label_format_csv"}]
export_formats = [{ identifier: "atom", format: "atom", label_locale: "label_format_atom" },
{ identifier: "pdf", format: "pdf", label_locale: "label_format_pdf"},
{ identifier: "pdf-descr", format: "pdf", label_locale: "label_format_pdf_with_descriptions", flags: ["show_descriptions"]},
{ identifier: "csv", format: "csv", label_locale: "label_format_csv"}]
if Redmine::Plugin.all.sort.map{|f| f.id}.include?(:openproject_xls_export)
export_formats.push({ format: "xls", label_locale: "label_format_xls"})
export_formats.push({ format: "xls", label_locale: "label_format_xls_with_descriptions", flags: ["show_descriptions"]})
export_formats.push({ identifier: "xls", format: "xls", label_locale: "label_format_xls"})
export_formats.push({ identifier: "xls-descr", format: "xls", label_locale: "label_format_xls_with_descriptions", flags: ["show_descriptions"]})
end
export_formats
end

@ -1,6 +1,13 @@
require 'uri'
##
# Intended to be used by the AccountController to handle omniauth logins
module Concerns::OmniauthLogin
def self.included(base)
# disable CSRF protection since that should be covered by the omniauth strategy
base.skip_before_filter :verify_authenticity_token, :only => [:omniauth_login]
end
def omniauth_login
auth_hash = request.env['omniauth.auth']
@ -8,20 +15,19 @@ module Concerns::OmniauthLogin
# Set back url to page the omniauth login link was clicked on
params[:back_url] = request.env['omniauth.origin']
user = User.find_or_initialize_by_identity_url identity_url_from_omniauth(auth_hash)
user = User.find_or_initialize_by_identity_url(identity_url_from_omniauth(auth_hash))
if user.new_record?
create_user_from_omniauth(user, auth_hash)
decision = OpenProject::OmniAuth::Authorization.authorized? auth_hash
if decision.approve?
authorization_successful user, auth_hash
else
user.log_successful_login if user.active?
login_user_if_active(user)
authorization_failed user, decision.message
end
end
def omniauth_failure
logger.warn(params[:message]) if params[:message]
flash[:error] = I18n.t(:error_external_authentication_failed)
redirect_to :action => 'login'
show_error I18n.t(:error_external_authentication_failed)
end
def self.direct_login?
@ -42,22 +48,46 @@ module Concerns::OmniauthLogin
OpenProject::Configuration['omniauth_direct_login_provider']
end
def self.direct_login_provider_url
"/auth/#{direct_login_provider}" if direct_login?
def self.direct_login_provider_url(params = {})
url_with_params "/auth/#{direct_login_provider}", params if direct_login?
end
private
def authorization_successful(user, auth_hash)
if user.new_record?
create_user_from_omniauth user, auth_hash
else
if user.active?
user.log_successful_login
OpenProject::OmniAuth::Authorization.after_login! user, auth_hash
end
login_user_if_active(user)
end
end
def authorization_failed(user, error)
logger.warn "Authorization for User #{user.id} failed: #{error}"
show_error error
end
def show_error(error)
flash[:error] = error
redirect_to :action => 'login'
end
# a user may login via omniauth and (if that user does not exist
# in our database) will be created using this method.
def create_user_from_omniauth(user, auth_hash)
# Self-registration off
return self_registration_disabled unless Setting.self_registration?
# Create on the fly
fill_user_fields_from_omniauth(user, auth_hash)
fill_user_fields_from_omniauth user, auth_hash
register_user_according_to_setting(user) do
opts = { after_login: ->(u) { OpenProject::OmniAuth::Authorization.after_login! u, auth_hash } }
# Create on the fly
register_user_according_to_setting(user, opts) do
# Allow registration form to show provider-specific title
@omniauth_strategy = auth_hash[:provider]
@ -73,22 +103,30 @@ module Concerns::OmniauthLogin
auth = session[:auth_source_registration]
return if handle_omniauth_registration_expired(auth)
fill_user_fields_from_omniauth(@user, auth)
@user.update_attributes(permitted_params.user_register_via_omniauth)
register_user_according_to_setting(@user)
fill_user_fields_from_omniauth(user, auth)
user.update_attributes(permitted_params.user_register_via_omniauth)
opts = { after_login: ->(u) { OpenProject::OmniAuth::Authorization.after_login! u, auth } }
register_user_according_to_setting user, opts
end
def fill_user_fields_from_omniauth(user, auth)
info = auth[:info]
user.update_attributes login: info[:email],
mail: info[:email],
firstname: info[:first_name] || info[:name],
lastname: info[:last_name],
identity_url: identity_url_from_omniauth(auth)
user.update_attributes omniauth_hash_to_user_attributes(auth)
user.register
user
end
def omniauth_hash_to_user_attributes(auth)
info = auth[:info]
{
login: info[:email],
mail: info[:email],
firstname: info[:first_name] || info[:name],
lastname: info[:last_name],
identity_url: identity_url_from_omniauth(auth)
}
end
def identity_url_from_omniauth(auth)
"#{auth[:provider]}:#{auth[:uid]}"
end
@ -101,4 +139,14 @@ module Concerns::OmniauthLogin
redirect_to(signin_url)
end
end
def self.url_with_params(url, params = {})
URI.parse(url).tap do |uri|
query = URI.decode_www_form(uri.query || '')
params.each do |key, value|
query << [key, value]
end
uri.query = URI.encode_www_form(query) unless query.empty?
end.to_s
end
end

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

@ -78,7 +78,7 @@ module JournalsHelper
end
def render_notes(model, journal, options={})
editable = model.journal_editable_by?(User.current) if User.current.logged?
editable = journal.editable_by?(User.current) if User.current.logged?
unless journal.notes.blank?

@ -40,6 +40,7 @@ class Attachment < ActiveRecord::Base
validates_presence_of :container, :filename, :author, :content_type
validates_length_of :filename, :maximum => 255
validates_length_of :description, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
validate :filesize_below_allowed_maximum

@ -74,6 +74,12 @@ class CustomField < ActiveRecord::Base
validate :validate_default_value_in_translations
validate :validate_name
validates :min_length, numericality: { only_integer: true, greater_than_or_equal: 0 }
validates :max_length, numericality: { only_integer: true, greater_than_or_equal: 0 }
validates :min_length, numericality: { less_than_or_equal_to: :max_length, message: :greater_than_or_equal_to_max_length}, unless: Proc.new { |cf| cf.max_length.blank?}
def initialize(attributes = nil, options = {})
super

@ -93,7 +93,7 @@ class Journal < ActiveRecord::Base
end
def editable_by?(user)
journable.journal_editable_by?(user)
(journable.journal_editable_by?(user) && self.user == user) || user.admin?
end
def details

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

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

@ -0,0 +1,32 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See doc/COPYRIGHT.rdoc for more details.
++#%>
<div id="nav-login-content">
<%= render :partial => 'account/auth_providers' %>
</div>

@ -0,0 +1,63 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See doc/COPYRIGHT.rdoc for more details.
++#%>
<%= form_tag({:action=> "login"}, autocomplete: 'off') do %>
<%= back_url_hidden_field_tag %>
<div class="attribute_wrapper">
<label for="username"><%= User.human_attribute_name :login %></label>
<%= text_field_tag 'username', nil %>
</div>
<div class="attribute_wrapper">
<label for="password"><%= User.human_attribute_name :password %></label>
<%= password_field_tag 'password', nil %>
</div>
<div class="login-options-container">
<div class="login-links">
<% if Setting.lost_password? %>
<%= link_to l(:label_password_lost), :controller => '/account', :action => 'lost_password' %>
<br>
<% end %>
<% if Setting.self_registration? %>
<%= link_to l(:label_register), { :controller => '/account', :action => 'register' } %>
<% end %>
</div>
<% if Setting.autologin? %>
<div class="attribute_wrapper indented">
<label for="autologin">
<%= check_box_tag 'autologin', 1, false %> <%= l(:label_stay_logged_in) %>
</label>
</div>
<% end %>
</div>
<input type="submit" name="login" value="<%=l(:button_login)%>" class="button_highlight" />
<%= javascript_tag "Form.Element.focus('username');" %>
<% end %>

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

@ -52,6 +52,18 @@ SimpleBenchmark.bench "require 'rails/all'" do
end
if defined?(Bundler)
# lib directory has to be added to the load path so that
# the open_project/plugins files can be found (places under lib).
# Now it would be possible to remove that and use require with
# lib included but some plugins already use
#
# require 'open_project/plugins'
#
# to ensure the code to be loaded. So we provide a compaibility
# layer here. One might remove this later.
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'open_project/plugins'
SimpleBenchmark.bench 'Bundler.require' do
Bundler.require(:default, :assets, :opf_plugins, Rails.env)
end

@ -186,6 +186,8 @@ default:
# Note that this does not stop a user from manually navigating to any other
# omniauth provider if additional ones are configured.
# omniauth_direct_login_provider: developer
#
# disable_password_login: true
# specific configuration options for production environment
# that overrides the default ones

@ -0,0 +1,13 @@
default: &default
adapter: postgresql
encoding: unicode
database: teatro
pool: 5
min_messages: warning
username: postgres
development:
<<: *default
production:
<<: *default

@ -1,3 +1,31 @@
#-- 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.
#++
NonStupidDigestAssets.whitelist = [
'styleguide.html',
/jquery-ui\/.*/,

@ -1,3 +1,31 @@
#-- 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.
#++
Rails.application.config.middleware.use OmniAuth::Builder do
unless Rails.env.production?
provider :developer, :fields => [:first_name, :last_name, :email]

@ -1,3 +1,31 @@
#-- 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.
#++
if Rails.env.development?
ActiveSupport::Dependencies.explicitly_unloadable_constants << "API"

@ -1,3 +1,31 @@
#-- 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.
#++
class ActiveSupport::TimeWithZone
def as_json(options = {})
%(#{time.strftime("%m/%d/%Y/ %H:%M %p")})

@ -196,6 +196,7 @@ de:
exclusion: "ist nicht verfügbar"
greater_than: "muss größer als %{count} sein"
greater_than_or_equal_to: "muss größer oder gleich %{count} sein"
greater_than_or_equal_to_max_length: "muss größer oder gleich der maximalen Länge sein"
greater_than_start_date: "muss größer als Anfangsdatum sein"
inclusion: "ist kein gültiger Wert"
invalid: "ist nicht gültig"
@ -1599,6 +1600,7 @@ de:
status_user_and_brute_force: "%{user} und %{brute_force}"
unlock: "Entsperren"
unlock_and_reset_failed_logins: "Entsperren und fehlgeschlagene Logins zurücksetzen"
authorization_rejected: "Sie dürfen sich nicht einloggen."
version_status_closed: "abgeschlossen"
version_status_locked: "gesperrt"

@ -196,6 +196,7 @@ en:
exclusion: "is reserved"
greater_than: "must be greater than %{count}"
greater_than_or_equal_to: "must be greater than or equal to %{count}"
greater_than_or_equal_to_max_length: "must be greater than or equal to maximum length"
greater_than_start_date: "must be greater than start date"
inclusion: "is not included in the list"
invalid: "is invalid"
@ -1589,6 +1590,7 @@ en:
status_user_and_brute_force: "%{user} and %{brute_force}"
unlock: "Unlock"
unlock_and_reset_failed_logins: "Unlock and reset failed logins"
authorization_rejected: "You are not allowed to sign in."
version_status_closed: "closed"

@ -128,6 +128,7 @@ de:
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?"
@ -159,6 +160,17 @@ de:
version: "Version"
watcher: "Beobachter"
relation_labels:
parent: "Übergeordnete Aufgabe"
children: "Untergeordnete Aufgaben"
relatedTo: "Beziehung mit"
duplicates: "Dupliziert"
duplicated: "Dupliziert durch"
blocks: "Blockiert"
blocked: "Blockiert durch"
precedes: "Vorgänger von"
follows: "Folgt"
select2:
input_too_short:
one: "Bitte geben Sie ein weiteres Zeichen ein"

@ -127,6 +127,7 @@ en:
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?"
@ -158,6 +159,17 @@ en:
version: "Version"
watcher: "Watcher"
relation_labels:
parent: "Parent"
children: "Children"
relatedTo: "Related To"
duplicates: "Duplicates"
duplicated: "Duplicated by"
blocks: "Blocks"
blocked: "Blocked by"
precedes: "Precedes"
follows: "Follows"
select2:
input_too_short:
one: "Please enter one more character"

@ -1,3 +1,31 @@
#-- 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.
#++
class ClearIdentityUrlsOnUsers < ActiveRecord::Migration
def up
create_table "legacy_user_identity_urls" do |t|

@ -1,3 +1,31 @@
#-- 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.
#++
class RemoveOpenidEntirely < ActiveRecord::Migration
def up
drop_table 'open_id_authentication_nonces'

@ -1,3 +1,31 @@
#-- 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.
#++
class AddSessionsTable < ActiveRecord::Migration
def change
create_table :sessions do |t|

@ -1,3 +1,31 @@
#-- 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.
#++
class ResetContentTypes < ActiveRecord::Migration
def up
Attachment.all.each do |attachment|

@ -1,3 +1,31 @@
#-- 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.
#++
class DropWorkPackagesPriorityNotNullConstraint < ActiveRecord::Migration
def change
change_column :work_packages, :priority_id, :integer, :null => true

@ -1,3 +1,31 @@
#-- 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.
#++
class AddUpdatedAtIndexToWorkPackages < ActiveRecord::Migration
def change
add_index :work_packages, :updated_at

@ -63,6 +63,17 @@ In case you want to use environment variables, but you have no easy way to set t
* `scm_subversion_command` (default: 'git')
* `session_store`: `active_record_store`, `cache_store`, or `cookie_store` (default: cache_store)
* [`omniauth_direct_login_provider`](#omniauth-direct-login-provider) (default: nil)
* [`disable_password_login`](#disable-password-login) (default: false)
### 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

@ -0,0 +1,116 @@
# Subversion and Git Integration
OpenProject can (by default) browse subversion and git repositories.
But it does not serves them to git/svn clients.
However, with the help of the apache webserver it is possible to serve repositories.
## Set-up
OpenProject should run integrated in your apache setup. This can be done in several ways
(for example by using the passenger module).
In this document we assume that you run OpenProject using a separate process, which listens
for requests on http://localhost:3000.
We let apache serve svn and git repositories (with the help of some modules) and
authenticate against the OpenProject user database.
Therefore we use an authentication perl script located in extra/svn/OpenProjectAuthentication.pm .
It requires some apache modules to be enabled and installed:
<pre>
aptitude install libapache2-mod-perl2 libapache2-svn
a2enmod proxy proxy_http
</pre>
Also, the extra/svn/OpenProjectAuthentication.pm script needs to be in your apache perl path
(for example it might be sym-linked into /etc/apache2/Apache).
To make the authentication work, you need to generate a secret repository API key. To do this, open
your OpenProject installation in your favourite web browser, log in as an administrator and go to
Modules -> Administration -> Settings -> Repositories.
On that page, enable the "Enable WS for repository management" setting and generate an API key (do not
for get to save the settings). We need that API key later in our apache config.
Find a place to store the repositories. For this guide we assume that you put your svn repositories in
/srv/openproject/svn . All things in that repository should be accessible by the apache system user and
by the user running your openproject server.
## An example apache configuration
We provide an example apache configuration. Some details are explained inline as comments.
<pre>
# Load OpenProject per module used to authenticate requests against the user database.
# Be sure that the OpenProjectAuthentication.pm script in located in your perl path.
PerlSwitches -I/srv/www/perl-lib -T
PerlLoadModule Apache::OpenProjectAuthentication
&lt;VirtualHost *:80&gt;
ErrorLog /var/log/apache2/error
# The /sys endpoint is an internal API used to authenticate repository
# access requests. It shall not be reachable from remote.
&lt;LocationMatch "/sys"&gt;
Order Deny,Allow
Deny from all
Allow from 127.0.0.1
&lt;/LocationMatch&gt;
# This fixes COPY for webdav over https
RequestHeader edit Destination ^https: http: early
# Serves svn repositories locates in /srv/openproject/svn via WebDAV
# It is secure with basic auth against the OpenProject user database.
&lt;Location /svn&gt;
DAV svn
SVNParentPath "/srv/openproject/svn"
DirectorySlash Off
AuthType Basic
AuthName "Secured Area"
Require valid-user
PerlAccessHandler Apache::Authn::OpenProject::access_handler
PerlAuthenHandler Apache::Authn::OpenProject::authen_handler
OpenProjectUrl 'http://127.0.0.1:3000'
OpenProjectApiKey 'REPLACE WITH REPOSITORY API KEY'
&lt;Limit OPTIONS PROPFIND GET REPORT MKACTIVITY PROPPATCH PUT CHECKOUT MKCOL MOVE COPY DELETE LOCK UNLOCK MERGE&gt;
Allow from all
&lt;/Limit&gt;
&lt;/Location&gt;
# Requires the apache module mod_proxy. Enable it with
# a2enmod proxy proxy_http
# See: http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#ProxyPass
# Note that the ProxyPass with the longest path should be listed first, otherwise
# a shorter path may match and will do an early redirect (without looking for other
# more specific matching paths).
ProxyPass /svn !
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
&lt;/VirtualHost&gt;
</pre>
## Automatically create repositories with reposman.rb
The reposman.rb script can create repositories for your newly created OpenProject projects.
It is useful when run from a cron job (so that repositories appear 'magically' some time after you created
a project in the OpenProject administration view).
<pre>
ruby extra/svn/reposman.rb \
--openproject-host "http://127.0.0.1:3000" \
--owner "www-data" \
--group "openproject" \
--public-mode '2750' \
--private-mode '2750' \
--svn-dir "/srv/openproject/svn" \
--url "file:///srv/openproject/svn" \
--key "REPLACE WITH REPOSITORY API KEY" \
--scm Subversion \
--verbose
</pre>

@ -1,66 +1,5 @@
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';

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

@ -52,9 +52,25 @@ describe('WorkPackageDetailsController', function() {
embedded: {
activities: [],
watchers: [],
attachments: []
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}; }
}
@ -88,8 +104,6 @@ describe('WorkPackageDetailsController', function() {
return false;
}
},
UserService: UserService,
CustomFieldHelper: CustomFieldHelper,
WorkPackagesDetailsHelper: {
attachmentsTitle: function() { return ''; }
},
@ -107,206 +121,38 @@ describe('WorkPackageDetailsController', function() {
});
});
describe('work package properties', function() {
function fetchPresentPropertiesWithName(propertyName) {
return scope.presentWorkPackageProperties.filter(function(propertyData) {
return propertyData.property === propertyName;
});
}
describe('when the property has a value', function() {
var propertyName = 'status';
beforeEach(function() {
buildController();
});
it('adds properties to present properties', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
});
});
describe('when the property is among the first 3 properties', function() {
var propertyName = 'responsible';
describe('#scope.canViewWorkPackageWatchers', function() {
describe('when the work package does not contain the embedded watchers property', function() {
beforeEach(function() {
workPackage.embedded.watchers = undefined;
buildController();
});
})
it('is added to present properties even if it is empty', function() {
expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1);
it('returns false', function() {
expect(scope.canViewWorkPackageWatchers()).to.be.false;
});
});
describe('when the property is among the second group of 3 properties', function() {
var propertyName = 'priority',
label = 'Priority';
describe('when the work package contains the embedded watchers property', function() {
beforeEach(function() {
sinon.stub(I18n, 't')
.withArgs('js.work_packages.properties.' + propertyName)
.returns(label);
workPackage.embedded.watchers = [];
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);
});
it('returns true', function() {
expect(scope.canViewWorkPackageWatchers()).to.be.true;
});
});
});
describe('when the property is not among the first 6 properties', function() {
var propertyName = 'versionName',
label = 'Version';
describe('work package properties', function() {
describe('relations', function() {
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;
});
it('Relation::Relates', function() {
expect(scope.relatedTo.length).to.eq(1);
});
});
});

@ -61,6 +61,12 @@ describe('date time Directives', function() {
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>';
@ -74,6 +80,8 @@ describe('date time Directives', function() {
it('should use default formatting', function() {
expect(formattedDate()).to.contain('02/08/2013');
});
shouldBehaveLikeHashTitle('02/08/2013');
});
describe('with configuration', function() {
@ -87,6 +95,8 @@ describe('date time Directives', function() {
it('should use user specified formatting', function() {
expect(formattedDate()).to.contain('08-02-2013');
});
shouldBehaveLikeHashTitle('08-02-2013');
});
});
@ -103,6 +113,8 @@ describe('date time Directives', function() {
it('should use default formatting', function() {
expect(formattedDate()).to.contain('9:30 AM');
});
shouldBehaveLikeHashTitle('9:30 AM');
});
describe('with configuration', function() {
@ -116,6 +128,8 @@ describe('date time Directives', function() {
it('should use user specified formatting', function() {
expect(formattedDate()).to.contain('09:30 am');
});
shouldBehaveLikeHashTitle('09:30 am');
});
});
@ -147,6 +161,8 @@ describe('date time Directives', 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() {
@ -163,6 +179,8 @@ describe('date time Directives', 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)');
});
});
});
});

@ -65,7 +65,7 @@ describe('attachmentTitleCell Directive', function() {
it('should render link to attachment', function() {
var link = element.find('a');
expect(link.text()).to.equal('hearmi.now (12.34kB)');
expect(link.text()).to.equal('hearmi.now');
expect(link.attr('href')).to.equal('/attachments/1/hearmi.now');
});
});

@ -40,7 +40,12 @@ describe('ActivityService', function() {
describe('createComment', function() {
var setupFunction;
var workPackageId = 5;
var workPackage = {
id: 5,
links: {
addComment: { fetch: angular.noop }
}
};
var actvityId = 10;
var activities = [];
var descending = false;
@ -49,26 +54,18 @@ describe('ActivityService', function() {
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);
sinon.stub(workPackage.links.addComment, 'fetch')
.returns({
then: function() {
return $q.when({ id: actvityId, comment: comment });
}
});
apiFetchResource = ActivityService.createComment(workPackage, activities, descending, comment);
}));
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");
});
afterEach(function() {
workPackage.links.addComment.fetch.restore();
})
it('returns an activity', function() {
apiFetchResource.then(function(activity){

@ -50,7 +50,7 @@ module API
property :total, as: :_total, exec_context: :decorator
def total
represented.first.model.class.count
represented.empty? ? 0 : represented.first.model.class.count
end
end
end

@ -68,17 +68,18 @@ module API
end
end
rescue_from :all do |e|
case e.class.to_s
when 'API::Errors::Validation', 'API::Errors::UnwritableProperty', 'API::Errors::Unauthorized', 'API::Errors::Unauthenticated'
Rack::Response.new(e.to_json, e.code, e.headers).finish
when 'ActiveRecord::RecordNotFound'
not_found = API::Errors::NotFound.new(e.message)
Rack::Response.new(not_found.to_json, not_found.code, not_found.headers).finish
when 'ActiveRecord::RecordInvalid'
error = API::Errors::Validation.new(e.record)
Rack::Response.new(error.to_json, error.code, error.headers).finish
end
rescue_from ActiveRecord::RecordInvalid do |e|
error = API::Errors::Validation.new(e.record)
Rack::Response.new(error.to_json, error.code, error.headers).finish
end
rescue_from ActiveRecord::RecordNotFound do |e|
error = API::Errors::NotFound.new(e.message)
Rack::Response.new(error.to_json, error.code, error.headers).finish
end
rescue_from API::Errors::Unauthorized, API::Errors::Unauthenticated, API::Errors::Validation do |e|
Rack::Response.new(e.to_json, e.code, e.headers).finish
end
# run authentication before each request

@ -1,3 +1,31 @@
#-- 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 Activities
@ -13,7 +41,7 @@ module API
before do
@activity = Journal.find(params[:id])
model = ::API::V3::Activities::ActivityModel.new(@activity)
@representer = ::API::V3::Activities::ActivityRepresenter.new(model)
@representer = ::API::V3::Activities::ActivityRepresenter.new(model, { current_user: current_user})
end
get do
@ -33,13 +61,19 @@ module API
fail Errors::Validation.new(activity, description: errors)
end
end
def authorize_edit_own(activity)
return authorize({ controller: :journals, action: :edit }, context: @activity.journable.project)
raise API::Errors::Unauthorized.new(current_user) unless activity.editable_by?(current_user)
end
end
params do
requires :comment, type: String
end
put do
authorize({ controller: :journals, action: :edit }, context: @activity.journable.project)
patch do
authorize_edit_own(@activity)
@activity.notes = params[:comment]

@ -40,6 +40,12 @@ module API
self.as_strategy = API::Utilities::CamelCasingStrategy.new
def initialize(model, options = {})
@current_user = options[:current_user]
super(model)
end
property :_type, exec_context: :decorator
link :self do
@ -54,6 +60,14 @@ module API
{ href: "#{root_url}api/v3/users/#{represented.model.user.id}", title: "#{represented.model.user.name} - #{represented.model.user.login}" }
end
link :update do
{
href: "#{root_url}api/v3/activities/#{represented.model.id}",
method: :patch,
title: "#{represented.model.id}"
} if current_user_allowed_to_edit?
end
property :id, getter: -> (*) { model.id }, render_nil: true
property :notes, as: :comment, render_nil: true
property :raw_notes, as: :rawComment, render_nil: true
@ -80,6 +94,14 @@ module API
private
def current_user_allowed_to_edit?
(current_user_allowed_to(:edit_own_work_package_notes, represented.model.journable) && represented.model.editable_by?(@current_user)) || current_user_allowed_to(:edit_work_package_notes, represented.model.journable)
end
def current_user_allowed_to(permission, work_package)
@current_user && @current_user.allowed_to?(permission, work_package.project)
end
def render_details(journal, no_html: false)
journal.details.map{ |d| journal.render_detail(d, no_html: no_html) }
end

@ -1,3 +1,31 @@
#-- 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 Attachments

@ -1,3 +1,31 @@
#-- 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 Queries

@ -29,12 +29,12 @@
module API
module V3
module Watchers
class WatchersRepresenter < ::API::Decorators::Collection
module Users
class UserCollectionRepresenter < ::API::Decorators::Collection
collection :watchers, as: -> (*) { as || :watchers }, exec_context: :decorator, embedded: true
collection :users, as: -> (*) { as || :users }, exec_context: :decorator, embedded: true
def watchers
def users
represented.map { |model| ::API::V3::Users::UserRepresenter.new(model) }
end
end

@ -1,3 +1,31 @@
#-- 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 Users

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

@ -1,3 +1,31 @@
#-- 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 WorkPackages
@ -8,7 +36,7 @@ module API
build_representer(
available_watchers,
::API::V3::Users::UserModel,
::API::V3::Watchers::WatchersRepresenter,
::API::V3::Users::UserCollectionRepresenter,
as: :available_watchers
)
end

@ -149,6 +149,10 @@ module API
.map{ |u| ::API::V3::Users::UserModel.new(u) }
end
def relations
work_package.relations.map{ |relation| RelationModel.new(relation) }
end
validates_presence_of :subject, :project_id, :type, :author, :status
validates_length_of :subject, maximum: 255
end

@ -120,6 +120,27 @@ module API
} if current_user_allowed_to(:add_work_package_watchers, represented.work_package)
end
link :addComment do
{
href: "#{root_url}api/v3/work_packages/#{represented.work_package.id}/activities",
method: :post,
title: 'Add comment'
} if current_user_allowed_to(:add_work_package_notes, represented.work_package)
end
link :parent do
{
href: "#{root_url}/api/v3/work_packages/#{represented.work_package.parent.id}",
title: represented.work_package.parent.subject
} unless represented.work_package.parent.nil?
end
links :children do
represented.work_package.children.map do |child|
{ href: "#{root_url}/api/v3/work_packages/#{child.id}", title: child.subject }
end unless represented.work_package.children.empty?
end
property :id, getter: -> (*) { work_package.id }, render_nil: true
property :subject, render_nil: true
property :type, render_nil: true
@ -144,19 +165,27 @@ module API
property :responsible, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !responsible.nil? }
property :assignee, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !assignee.nil? }
collection :activities, embedded: true, class: ::API::V3::Activities::ActivityModel, decorator: ::API::V3::Activities::ActivityRepresenter
property :watchers, embedded: true, exec_context: :decorator
# collection :watchers, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter
property :activities, embedded: true, exec_context: :decorator
property :watchers, embedded: true, exec_context: :decorator, if: -> (*) { current_user_allowed_to(:view_work_package_watchers, represented.work_package) }
collection :attachments, embedded: true, class: ::API::V3::Attachments::AttachmentModel, decorator: ::API::V3::Attachments::AttachmentRepresenter
property :relations, embedded: true, exec_context: :decorator
def _type
'WorkPackage'
end
def activities
represented.activities.map{ |activity| ::API::V3::Activities::ActivityRepresenter.new(activity, current_user: @current_user) }
end
def watchers
represented.watchers.map{ |watcher| ::API::V3::Users::UserRepresenter.new(watcher, work_package: represented.work_package, current_user: @current_user) }
end
def relations
represented.relations.map{ |relation| RelationRepresenter.new(relation, work_package: represented.work_package) }
end
def custom_properties
values = represented.work_package.custom_field_values
values.map { |v| { name: v.custom_field.name, format: v.custom_field.field_format, value: v.value }}

@ -1,3 +1,31 @@
#-- 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 WorkPackages
@ -31,7 +59,7 @@ module API
def save_work_package(work_package)
if work_package.save
model = ::API::V3::Activities::ActivityModel.new(work_package.journals.last)
representer = ::API::V3::Activities::ActivityRepresenter.new(model)
representer = ::API::V3::Activities::ActivityRepresenter.new(model, { current_user: current_user })
representer
else
@ -54,6 +82,21 @@ module API
end
resource :available_assignees do
get do
authorize(:add_work_packages, context: @work_package.project) \
|| authorize(:edit_work_packages, context: @work_package.project)
available_assignees = @work_package.assignable_assignees
build_representer(available_assignees,
::API::V3::Users::UserModel,
::API::V3::Users::UserCollectionRepresenter,
as: :available_assignees)
end
end
mount ::API::V3::WorkPackages::WatchersAPI
mount ::API::V3::WorkPackages::StatusesAPI
end

@ -1,3 +1,31 @@
#-- 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 DeprecatedAlias
def deprecated_alias(old_method, new_method)
define_method(old_method) do |*args, &block|

@ -0,0 +1,19 @@
Description:
Creates a new plugin with the given name in the specified directory
Usage:
rails generate open_project:plugin NAME DIRECTORY
Example:
rails generate open_project:plugin stuff ~/
This will create:
~/openproject-stuff
~/openproject-stuff/CHANGELOG.md
~/openproject-stuff/openproject-stuff.gemspec
~/openproject-stuff/lib
~/openproject-stuff/lib/openproject-stuff.rb
~/openproject-stuff/lib/open_project
~/openproject-stuff/lib/open_project/stuff.rb
~/openproject-stuff/lib/open_project/stuff
~/openproject-stuff/lib/open_project/stuff/engine.rb

@ -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.
#++
class OpenProject::PluginGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
argument :plugin_name, :type => :string, :default => "openproject-new-plugin"
argument :root_folder, :type => :string, :default => "vendor/gems"
# every public method is run when the generator is invoked
def generate_plugin
plugin_dir
lib_dir
end
def full_name
@full_name ||= begin
"openproject-#{plugin_name}"
end
end
private
def raise_on_params
puts plugin_name
puts root_folder
end
def plugin_path
"#{root_folder}/openproject-#{plugin_name}"
end
def plugin_dir
@plugin_dir ||= begin
directory('', plugin_path, :recursive => false)
end
end
def lib_path
"#{plugin_path}/lib"
end
def lib_dir
@lib_dir ||= begin
directory('lib', lib_path)
end
end
end

@ -0,0 +1,19 @@
# encoding: UTF-8
$:.push File.expand_path("../lib", __FILE__)
require 'open_project/<%= plugin_name %>/version'
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = "<%= full_name %>"
s.version = OpenProject::<%= plugin_name.camelcase %>::VERSION
s.authors = "Finn GmbH"
s.email = "info@finn.de"
s.homepage = "https://www.openproject.org/projects/<%= plugin_name.gsub('_','-') %>" # TODO check this URL
s.summary = 'OpenProject <%= plugin_name.gsub('_', ' ').titleize %>'
s.description = FIXME
s.license = FIXME # e.g. "MIT" or "GPLv3"
s.files = Dir["{app,config,db,lib}/**/*"] + %w(CHANGELOG.md README.md)
s.add_dependency "rails", "~> 3.2.14"
end

@ -0,0 +1,19 @@
<!---- copyright
OpenProject Plugins Plugin
Copyright (C) 2013 - 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.
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.md for more details.
++-->
# Changelog
* `#<ticket number>` Create plugin

@ -0,0 +1,7 @@
# OpenProject <%= plugin_name.gsub('_',' ').titlecase %> Plugin
FIXME Add description and check issue tracker link below
## Issue Tracker
https://www.openproject.org/projects/<%= plugin_name.gsub('_','-') %>/work_packages

@ -0,0 +1 @@
require 'open_project/<%= plugin_name %>'

@ -0,0 +1,5 @@
module OpenProject
module <%= plugin_name.camelcase %>
require "open_project/<%= plugin_name %>/engine"
end
end

@ -0,0 +1,16 @@
# Prevent load-order problems in case openproject-plugins is listed after a plugin in the Gemfile
# or not at all
require 'open_project/plugins'
module OpenProject::<%= plugin_name.camelcase %>
class Engine < ::Rails::Engine
engine_name :openproject_<%= plugin_name %>
include OpenProject::Plugins::ActsAsOpEngine
register '<%= full_name %>',
:author_url => 'http://finn.de',
:requires_openproject => '>= 3.0.0pre13'
end
end

@ -0,0 +1,5 @@
module OpenProject
module <%= plugin_name.camelcase %>
VERSION = "0.0.1"
end
end

@ -23,7 +23,7 @@
# 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.
#++

@ -27,8 +27,11 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require_relative 'configuration/helpers'
module OpenProject
module Configuration
extend Helpers
# Configuration default values
@defaults = {
@ -61,8 +64,11 @@ module OpenProject
'smtp_password' => nil,
'smtp_enable_starttls_auto' => nil,
'smtp_openssl_verify_mode' => nil, # 'none', 'peer', 'client_once' or 'fail_if_no_peer_cert'
'sendmail_location' => nil,
'sendmail_arguments' => nil
'sendmail_location' => '/usr/sbin/sendmail',
'sendmail_arguments' => '-i',
'disable_password_login' => false,
'omniauth_direct_login_provider' => nil
}
@config = nil

@ -0,0 +1,45 @@
#-- 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 OpenProject
module Configuration
##
# To be included into OpenProject::Configuration in order to provide
# helper methods for easier access to certain configuration options.
module Helpers
##
# Activating this leaves omniauth as the only way to authenticate.
def disable_password_login?
value = self['disable_password_login']
['true', true].include? value # former to accommodate ENV
end
end
end
end

@ -0,0 +1,267 @@
#-- 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 OpenProject
module OmniAuth
##
# Provides authorization mechanisms for OmniAuth-based authentication.
module Authorization
##
# Checks whether the given user is authorized to login by calling
# all registered callbacks. If all callbacks approve the user is authorized and may log in.
def self.authorized?(auth_hash)
rejection = callbacks.find_map do |callback|
d = callback.authorize auth_hash
if d.is_a? Decision
d if d.reject?
else
fail ArgumentError, 'Expecting Callback#authorize to return a Decision.'
end
end
rejection || Approval.new
end
##
# Signals that the given user has been logged in.
#
# Note: Only call if you know what you are doing.
def self.after_login!(user, auth_hash)
after_login_callbacks.each do |callback|
callback.after_login user, auth_hash
end
end
##
# Adds a callback to be executed before a user is logged in.
# The given callback may reject the user to prevent authorization by
# calling dec#reject(error) or approve by calling dec#approve.
#
# If not approved a user is implicitly rejected.
#
# @param opts [Hash] options for the callback registration
# @option opts [Symbol] :provider Only call for given provider
#
# @yield [decision, user, auth_hash] Callback to be executed before the user is logged in.
# @yieldparam [DecisionStore] dec object providing #approve and #reject
# @yieldparam [User] user The OpenProject user to be logged in.
# @yieldparam [AuthHash] OmniAuth authentication information including user info
# and credentials.
# @yieldreturn [Decision] A Decision indicating whether or not to authorize the user.
def self.authorize_user(opts = {}, &block)
if opts[:provider]
authorize_user_for_provider opts[:provider], &block
else
add_authorize_user_callback AuthorizationBlockCallback.new(&block)
end
end
def self.authorize_user_for_provider(provider, &block)
callback = AuthorizationBlockCallback.new do |dec, auth_hash|
if auth_hash.provider.to_sym == provider.to_sym
block.call dec, auth_hash
else
dec.approve
end
end
add_authorize_user_callback callback
end
##
# Registers a callback on the event of a successful login.
#
# Called directly after logging in.
# This usually happens when the user logged in normally or was logged in
# automatically after on-the-fly registration via automated account activation.
#
# @yield [user] Callback called with the successfully logged in user.
# @yieldparam user [User] User who has been logged in.
def self.after_login(&block)
add_after_login_callback AfterLoginBlockCallback.new(&block)
end
##
# Registers a new callback to decide whether or not a user is to be authorized.
#
# @param [AuthorizationCallback] Callback to be called upon user authorization.
def self.add_authorize_user_callback(callback)
callbacks << callback
end
def self.callbacks
@callbacks ||= []
end
##
# Registers a new callback to successful user login.
#
# @param [AfterLoginCallback] Callback to be called upon successful authorization.
def self.add_after_login_callback(callback)
after_login_callbacks << callback
end
def self.after_login_callbacks
@after_login_callbacks ||= []
end
##
# Performs user authorization.
class AuthorizationCallback
##
# Given an OmniAuth auth hash this decides if a user is authorized or not.
#
# @param [AuthHash] auth_hash OmniAuth authentication information including user info
# and credentials.
#
# @return [Decision] A decision indicating whether the user is authorized or not.
def authorize(auth_hash)
fail "subclass responsibility: authorize(#{auth_hash})"
end
end
##
# A callback triggering a given block.
class AuthorizationBlockCallback < AuthorizationCallback
attr_reader :block
def initialize(&block)
@block = block
end
def authorize(auth_hash)
store = DecisionStore.new
block.call store, auth_hash
# failure to make a decision results in a rejection
store.decision || Rejection.new(I18n.t('user.authorization_rejected'))
end
end
##
# A callback for reacting to a user being logged in.
class AfterLoginCallback
##
# Is called after a user has been logged in successfully.
#
# @param [User] User who has been logged in.
# @param [Omniauth::AuthHash] Omniauth authentication info including credentials.
def after_login(user, auth_hash)
fail "subclass responsibility: after_login(#{user}, #{auth_hash})"
end
end
##
# A after_login callback triggering a given block.
class AfterLoginBlockCallback < AfterLoginCallback
attr_reader :block
def initialize(&block)
@block = block
end
def after_login(user, auth_hash)
block.call user, auth_hash
end
end
##
# Abstract base class for an authorization decision.
# Any subclass must either override #approve? or #reject?
# the both of which are defined in terms of each other.
class Decision
def approve?
!reject?
end
def reject?
!approve?
end
def self.approve
Approval.new
end
def self.reject(error_message)
Rejection.new error_message
end
end
##
# Indicates a rejected authorization attempt.
class Rejection < Decision
attr_reader :message
def initialize(message)
@message = message
end
def reject?
true
end
end
##
# Indicates an approved authorization.
class Approval < Decision
def approve?
true
end
end
##
# Stores a decision.
class DecisionStore
attr_accessor :decision
def approve
self.decision = Approval.new
end
def reject(error_message)
self.decision = Rejection.new error_message
end
end
Enumerable.class_eval do
##
# Passes each element to the given block and returns the
# result of the block as soon as it's truthy.
def find_map(&block)
each do |e|
result = block.call e
return result if result
end
nil
end
end
end
end
end

@ -0,0 +1,35 @@
#-- 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 OpenProject
module Plugins
require 'open_project/plugins/patch_registry'
require 'open_project/plugins/load_dependency'
require 'open_project/plugins/acts_as_op_engine'
end
end

@ -0,0 +1,172 @@
#-- 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 OpenProject::Plugins
module ActsAsOpEngine
def self.included(base)
base.send(:define_method, :name) do
ActiveSupport::Inflector.demodulize(base).downcase
end
# Don't use the PatchRegistry for now, as the core classes doesn't notify of class loading
# Use the old config.to_prepare method, but we can hopefully someday switch to on-demand
# patching once the PatchRegistry works.
# base.send(:define_method, :patch) do |target, patch|
# OpenProject::Plugins::PatchRegistry.register(target, patch)
# end
# Disable LoadDependency for the same reason
# base.send(:define_method, :load_dependent) do |target, *dependencies|
# OpenProject::Plugins::LoadDependency.register(target, *dependencies)
# end
# Patch classes
#
# Looks for patches via autoloading in
# <plugin root>/lib/openproject/<plugin name>/patches/<patched_class>_patch.rb
# Make sure the patch module has the name the Rails autoloading expects.
#
# Example:
# patches [:IssuesController]
# This looks for OpenProject::XlsExport::Patches::IssuesControllerPatch
# in openproject/xls_export/patches/issues_controller_patch.rb
base.send(:define_method, :patches) do |patched_classes|
plugin_name = engine_name
base.config.to_prepare do
patched_classes.each do |klass_name|
plugin_module = plugin_name.sub(/^openproject_/, '').camelcase
patch = "OpenProject::#{plugin_module}::Patches::#{klass_name.to_s}Patch".constantize
klass = klass_name.to_s.constantize
klass.send(:include, patch) unless klass.included_modules.include?(patch)
end
end
end
# Define assets provided by the plugin
base.send(:define_method, :assets) do |assets|
base.initializer "#{engine_name}.precompile_assets" do |app|
app.config.assets.precompile += assets.to_a
end
end
# Add permitted attributes (strong_parameters)
#
# Useful when adding a field to an OpenProject core model. We discourage adding
# a field to a core model, but at the moment there's no API to do this in a better way
# and a lot of existing plugins already do it.
#
# See PermittedParams in OpenProject for available models
#
# Example:
# additional_permitted_attributes :user => [:registration_reason]
base.send(:define_method, :additional_permitted_attributes) do |attributes|
config.to_prepare do
::PermittedParams.send(:add_permitted_attributes, attributes)
end
end
# Register a plugin with OpenProject
#
# Uses Gem specification for plugin name, author etc.
#
# gem_name: The gem name, used for querying the gem for metadata like author
# options: An options Hash, at least :requires_openproject is recommended to
# define the minimal version of OpenProject the plugin is compatible with
# Another common option is :author_url.
# block: Pass a block to the plugin (for defining permissions, menu items and the like)
base.send(:define_method, :register) do |gem_name, options, &block|
base.initializer "#{engine_name}.register_plugin" do
spec = Bundler.environment.specs[gem_name][0]
p = Redmine::Plugin.register engine_name.to_sym do
name spec.summary
author spec.authors.kind_of?(Array) ? spec.authors[0] : spec.authors
description spec.description
version spec.version
url spec.homepage
options.each do |name, value|
send(name, value)
end
end
p.instance_eval(&block) if (p && block)
end
# Workaround to ensure settings are available after unloading in development mode
plugin_name = engine_name
if options.include? :settings
base.class_eval do
config.to_prepare do
Setting.create_setting("plugin_#{plugin_name}",
{'default' => options[:settings][:default], 'serialized' => true})
Setting.create_setting_accessors("plugin_#{plugin_name}")
end
end
end
end
base.class_eval do
config.autoload_paths += Dir["#{config.root}/lib/"]
config.before_configuration do |app|
# This is required for the routes to be loaded first
# as the routes should be prepended so they take precedence over the core.
app.config.paths['config/routes'].unshift File.join(config.root, "config", "routes.rb")
end
initializer "#{engine_name}.remove_duplicate_routes", :after => "add_routing_paths" do |app|
# removes duplicate entry from app.routes_reloader
# As we prepend the plugin's routes to the load_path up front and rails
# adds all engines' config/routes.rb later, we have double loaded the routes
# This is not harmful as such but leads to duplicate routes which decreases performance
app.routes_reloader.paths.uniq!
end
initializer "#{engine_name}.register_test_paths" do |app|
app.config.plugins_to_test_paths << self.root
end
# adds our factories to factory girl's load path
initializer "#{engine_name}.register_factories", :after => "factory_girl.set_factory_paths" do |app|
FactoryGirl.definition_file_paths << File.expand_path(self.root.to_s + '/spec/factories') if defined?(FactoryGirl)
end
initializer "#{engine_name}.append_migrations" do |app|
unless app.root.to_s.match root.to_s
config.paths["db/migrate"].expanded.each do |expanded_path|
app.config.paths["db/migrate"] << expanded_path
end
end
end
end
end
end
end

@ -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.
#++
module OpenProject::Plugins
module LoadDependency
def self.register(target, *dependencies)
ActiveSupport.on_load(target) do
dependencies.each do |dependency|
require_dependency dependency
end
end
end
end
end

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

Loading…
Cancel
Save