Merge pull request #1619 from opf/feature/13954_add_edit_quote_comments_in_wp_overview

pull/1639/head
Hagen Schink 10 years ago
commit 15ff42d0c9
  1. 56
      app/assets/javascripts/angular/directives/components/activity-comment-directive.js
  2. 2
      app/assets/javascripts/angular/directives/components/authoring-directive.js
  3. 35
      app/assets/javascripts/angular/filters/latest-items-filter.js
  4. 14
      app/assets/javascripts/angular/helpers/components/path-helper.js
  5. 74
      app/assets/javascripts/angular/services/activity-service.js
  6. 74
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  7. 43
      app/assets/javascripts/angular/work_packages/tabs/editable-comment-directive.js
  8. 50
      app/assets/javascripts/angular/work_packages/tabs/exclusive-edit-directive.js
  9. 51
      app/assets/javascripts/angular/work_packages/tabs/user-activity-directive.js
  10. 4
      app/assets/stylesheets/_work_packages.scss
  11. 41
      app/assets/stylesheets/content/_work_packages_details_activities.sass
  12. 1
      app/assets/stylesheets/default.css.sass
  13. 3
      app/assets/stylesheets/layout/_split_view.sass
  14. 2
      config/locales/js-de.yml
  15. 2
      config/locales/js-en.yml
  16. 55
      karma/tests/filters/latest-items-filter-test.js
  17. 82
      karma/tests/services/activity-service-test.js
  18. 25
      lib/api/v3/activities/activities_api.rb
  19. 7
      lib/api/v3/activities/activity_representer.rb
  20. 31
      lib/api/v3/work_packages/work_packages_api.rb
  21. 7
      public/templates/components/activity_comment.html
  22. 5
      public/templates/work_packages/tabs/_editable_comment.html
  23. 27
      public/templates/work_packages/tabs/_user_activity.html
  24. 35
      public/templates/work_packages/tabs/activity.html
  25. 25
      public/templates/work_packages/tabs/overview.html
  26. 82
      spec/requests/activities_api_spec.rb
  27. 53
      spec/requests/api_helper_spec.rb
  28. 61
      spec/requests/work_packages_api_spec.rb

@ -0,0 +1,56 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('activityComment', ['I18n', 'ActivityService', 'ConfigurationService', function(I18n, ActivityService, ConfigurationService) {
return {
restrict: 'E',
replace: true,
scope: {
workPackage: '=',
activities: '='
},
templateUrl: '/templates/components/activity_comment.html',
link: function(scope, element, attrs) {
scope.title = I18n.t('js.label_add_comment_title');
scope.buttonTitle = I18n.t('js.label_add_comment');
scope.createComment = function() {
var comment = angular.element('#add-comment-text').val();
var descending = ConfigurationService.commentsSortedInDescendingOrder();
ActivityService.createComment(scope.workPackage.props.id, scope.activities, descending, comment)
.then(function(response){
angular.element('#add-comment-text').val('');
return response;
});
}
}
};
}]);

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

@ -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.
//++
angular.module('openproject.uiComponents')
.filter('latestItems', function(){
return function(items, isDescending, visible){
return isDescending ? items.slice(0, visible) : items.slice(-visible).reverse();
}
});

@ -36,7 +36,19 @@ angular.module('openproject.helpers')
apiV3: '/api/v3',
staticBase: window.appBasePath ? window.appBasePath : '',
activityPath: function(projectIdentifier, from) {
activityPath: function(activityId) {
return 'activities/' + activityId;
},
activitiesPath: function(workPackageId) {
var path = '';
if(workPackageId) {
path = '/work_packages/' + workPackageId;
}
path = path + '/activities';
return path;
},
activityFromPath: function(projectIdentifier, from) {
var link = '/activity';
if (projectIdentifier) {

@ -0,0 +1,74 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.services')
.service('ActivityService', ['HALAPIResource',
'$http',
'PathHelper', function(HALAPIResource, $http, PathHelper){
var ActivityService = {
createComment: function(workPackageId, activities, descending, comment) {
var resource = HALAPIResource.setup(PathHelper.activitiesPath(workPackageId));
var options = {
ajax: {
method: "POST",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
// We are unable to add to the work package's embedded activities directly
if(activity) {
if(descending){
activities.unshift(activity);
} else {
activities.push(activity);
}
return activity;
}
});
},
updateComment: function(activityId, comment) {
var resource = HALAPIResource.setup(PathHelper.activityPath(activityId));
var options = {
ajax: {
method: "PUT",
data: { comment: comment }
}
};
return resource.fetch(options).then(function(activity){
return activity;
});
}
}
return ActivityService;
}]);

@ -34,6 +34,7 @@ angular.module('openproject.workPackages.controllers')
'estimatedTime', 'versionName'
])
.constant('USER_TYPE', 'user')
.constant('VISIBLE_LATEST')
.controller('WorkPackageDetailsController', [
'$scope',
@ -42,20 +43,26 @@ angular.module('openproject.workPackages.controllers')
'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES',
'USER_TYPE',
'VISIBLE_LATEST',
'CustomFieldHelper',
'WorkPackagesHelper',
'PathHelper',
'UserService',
'$q',
'ConfigurationService',
function($scope, latestTab, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, CustomFieldHelper, WorkPackagesHelper, PathHelper, UserService, $q, ConfigurationService) {
function($scope, latestTab, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, VISIBLE_LATEST, CustomFieldHelper, WorkPackagesHelper, PathHelper, UserService, $q, ConfigurationService) {
$scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name);
});
$scope.$on('workPackageRefreshRequired', function(event, toState){
refreshWorkPackage();
});
// initialization
setWorkPackage(workPackage);
setWorkPackageScopeProperties(workPackage);
$scope.I18n = I18n;
$scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id;
$scope.maxDescriptionLength = 800;
@ -63,14 +70,7 @@ angular.module('openproject.workPackages.controllers')
function refreshWorkPackage() {
workPackage.links.self
.fetch({force: true})
.then(setWorkPackage);
}
function setWorkPackage(workPackage) {
$scope.workPackage = workPackage;
$scope.isWatched = !!workPackage.links.unwatch;
$scope.toggleWatchLink = workPackage.links.watch === undefined ? workPackage.links.unwatch : workPackage.links.watch;
$scope.watchers = workPackage.embedded.watchers;
.then(setWorkPackageScopeProperties);
}
function outputError(error) {
@ -86,10 +86,6 @@ angular.module('openproject.workPackages.controllers')
.then(refreshWorkPackage, outputError);
};
// resources for tabs
$scope.author = workPackage.embedded.author;
// available watchers
$scope.$watch('watchers.length', fetchAvailableWatchers)
@ -145,19 +141,32 @@ angular.module('openproject.workPackages.controllers')
.then(refreshWorkPackage, outputError)
};
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
function setWorkPackageScopeProperties(workPackage){
$scope.workPackage = workPackage;
// activities and latest activities
$scope.isWatched = !!workPackage.links.unwatch;
$scope.toggleWatchLink = workPackage.links.watch === undefined ? workPackage.links.unwatch : workPackage.links.watch;
$scope.watchers = workPackage.embedded.watchers;
$scope.activities = workPackage.embedded.activities;
$scope.activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
// activities and latest activities
$scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
$scope.activities = displayedActivities($scope.workPackage);
// watchers
$scope.latestActitivies = $scope.activities.reverse().slice(0, 3); // this leaves the activities in reverse order
$scope.watchers = workPackage.embedded.watchers;
$scope.author = workPackage.embedded.author;
$scope.activitiesSortedInDescendingOrder = ConfigurationService.commentsSortedInDescendingOrder();
// Attachments
$scope.attachments = workPackage.embedded.attachments;
// restore former order of actvities unless comments are to be sorted in descending order
if (!$scope.activitiesSortedInDescendingOrder) {
$scope.activities.reverse();
// Author
$scope.author = workPackage.embedded.author;
}
$scope.deleteWatcher = function(watcher) {
@ -166,19 +175,14 @@ angular.module('openproject.workPackages.controllers')
.then(refreshWorkPackage, outputError);
};
// Attachments
$scope.attachments = workPackage.embedded.attachments;
// Author
$scope.author = workPackage.embedded.author;
// work package properties
$scope.presentWorkPackageProperties = [];
$scope.emptyWorkPackageProperties = [];
$scope.userPath = PathHelper.staticUserPath;
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
function displayedActivities(workPackage) {
var activities = workPackage.embedded.activities;
activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically)
if ($scope.activitiesSortedInDescendingOrder) {
activities.reverse();
}
return activities;
}
function getPropertyValue(property, format) {
if (format === USER_TYPE) {

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

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

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

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

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

@ -56,6 +56,7 @@
@import content/work_packages
@import content/work_packages_filters
@import content/work_packages_table
@import content/work_packages_details_activities
@import content/expandable_group_content
@import content/control_colors
@import content/tabular

@ -352,6 +352,9 @@ i
.work-package-details-activities-activity-contents
padding: 10px 0
textarea
width: 100%
resize: none
@media only screen and (max-width: 1280px)
.split-view .work-packages--details, .work-packages--split-view .work-packages--details

@ -54,6 +54,8 @@ de:
general_text_No: "Nein"
general_text_Yes: "Ja"
label_add_columns: "Ausgewählte Spalten hinzufügen"
label_add_comment: "Kommentar hinzufügen"
label_add_comment_title: "Fügen Sie Ihre Kommentare hier zu"
label_added_by: "hinzugefügt von"
label_added_time_by: "Von %{author} %{age} hinzugefügt"
label_ago: "vor"

@ -54,6 +54,8 @@ en:
general_text_No: "No"
general_text_Yes: "Yes"
label_add_columns: "Add selected columns"
label_add_comment: "Add comment"
label_add_comment_title: "Add your comments here"
label_added_by: "added by"
label_added_time_by: "Added by %{author} %{age}"
label_ago: "days ago"

@ -0,0 +1,55 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('Latest items filter', function() {
beforeEach(module('openproject.workPackages.filters'));
describe('latestItems', function() {
var items;
beforeEach(function(){
items = [1,2,3,4,5,6,7,8,9];
});
it('should be defined', inject(function($filter) {
expect($filter('latestItems')).not.to.equal(null);
}));
it('should return the first 3 items', inject(function($filter) {
expect($filter('latestItems')(items, false, 3)).to.eql([9,8,7]);
}));
it('should return the last 3 items reversed', inject(function($filter) {
expect($filter('latestItems')(items, true, 3)).to.eql([1,2,3]);
}));
});
});

@ -0,0 +1,82 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('ActivityService', function() {
var $httpBackend, ActivityService;
beforeEach(module('openproject.api', 'openproject.services', 'openproject.models'));
beforeEach(inject(function(_$httpBackend_, _ActivityService_) {
$httpBackend = _$httpBackend_;
ActivityService = _ActivityService_;
}));
describe('createComment', function() {
var setupFunction;
var workPackageId = 5;
var actvityId = 10;
var activities = [];
var descending = false;
var comment = "Jack Bauer 24 hour power shower";
var apiResource;
var apiFetchResource;
beforeEach(inject(function($q) {
apiResource = {
fetch: function() {
deferred = $q.defer();
deferred.resolve({ id: actvityId, comment: comment } );
return deferred.promise;
}
}
}));
beforeEach(inject(function(HALAPIResource) {
setupFunction = sinon.stub(HALAPIResource, 'setup').returns(apiResource);
}));
beforeEach(inject(function() {
apiFetchResource = ActivityService.createComment(workPackageId, activities, descending, comment);
}));
it('makes an api setup call', function() {
expect(setupFunction).to.have.been.calledWith("/work_packages/" + workPackageId + "/activities");
});
it('returns an activity', function() {
apiFetchResource.then(function(activity){
expect(activity.id).to.equal(activityId);
expect(activity.comment).to.equal(comment);
expect(activities.length).to.equal(1);
});
});
});
});

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

@ -80,10 +80,9 @@ module API
private
def render_details(journal, no_html: false)
journal.details.map{ |d| journal.render_detail(d, no_html: no_html) }
end
def render_details(journal, no_html: false)
journal.details.map{ |d| journal.render_detail(d, no_html: no_html) }
end
end
end
end

@ -13,7 +13,7 @@ module API
before do
@work_package = WorkPackage.find(params[:id])
model = ::API::V3::WorkPackages::WorkPackageModel.new(work_package: @work_package)
@representer = ::API::V3::WorkPackages::WorkPackageRepresenter.new(model, current_user: current_user)
@representer = ::API::V3::WorkPackages::WorkPackageRepresenter.new(model, { current_user: current_user }, :activities, :users)
end
get do
@ -21,6 +21,35 @@ module API
@representer.to_json
end
resource :activities do
helpers do
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.to_json
else
errors = work_package.errors.full_messages.join(", ")
fail Errors::Validation.new(work_package, description: errors)
end
end
end
params do
requires :comment, type: String
end
post do
authorize({ controller: :journals, action: :new }, context: @work_package.project)
@work_package.journal_notes = params[:comment]
save_work_package(@work_package)
end
end
mount ::API::V3::WorkPackages::WatchersAPI
end

@ -0,0 +1,7 @@
<div class="activity-comment">
<label>
<h3>{{ title }}</h3>
<textarea id="add-comment-text" name="add-comment"></textarea>
<button class="button" ng-click="createComment()">{{ buttonTitle }}</button>
</label>
</div>

@ -0,0 +1,5 @@
<span class="message"
ng-show="activity.props._type == 'Activity::Comment'"
ng-bind-html="activity.props.comment"/>
<textarea ng-if="commentInEdit = activity.props.id" id="edit-comment-text"></textarea>

@ -1,13 +1,32 @@
<div class="work-package-details-activities-activity-contents">
<div class="work-package-details-activities-activity-contents"
ng-mouseover="showActions()"
ng-mouseout="hideActions()">
<div class="comments-number"><a ng-href="#{{ currentAnchor }}" ng-bind="'#' + activityNo"></a>
<div class="comments-icons"
ng-show="activity.props._type == 'Activity::Comment' && inFocus">
<i class="icon-quote action-icon" ng-click="quoteComment()"></i>
<i class="icon-edit action-icon" ng-click="editComment()"></i>
</div>
</div>
<img class="avatar" ng-src="{{ userAvatar }}" />
<span class="user"><a ng-href="{{ userPath(userId) }}" name="{{ currentAnchor }}" ng-bind="userName"></a></span>
<span class="date">{{ I18n.t('js.label_commented_on') }} <date-time date-time-value="activity.props.createdAt"/></date-time>
<span class="comment wiki">
<span class="message"
ng-show="activity.props._type == 'Activity::Comment'"
ng-bind-html="activity.props.comment"/>
<div ng-if="inEdit">
<div>
<textarea id="edit-comment-text"
ng-bind-html="activity.props.rawComment"
rows="5"></textarea>
</div>
<div>
<button class="button" ng-click="updateComment()">{{ I18n.t('js.button_update') }}</button>
<span><a href="" ng-click="cancelEdit()">cancel</a></span>
</div>
</div>
<span ng-if="!inEdit"
class="message"
ng-show="activity.props._type == 'Activity::Comment'"
ng-bind-html="activity.props.comment"/>
<ul class="work-package-details-activities-messages">
<li ng-repeat="detail in activity.props.htmlDetails track by $index">
<span class="message" ng-bind-html="detail"/>

@ -1,19 +1,24 @@
<div class="detail-panel-content" >
<div class="detail-activity">
<ul class="work-package-details-activities-list">
<li ng-repeat="activity in activities"
class="work-package-details-activities-activity"
ng-init="activityNo = activitiesSortedInDescendingOrder && activities.length - $index || $index + 1;
currentDate = (activity.props.createdAt|date:'longDate');
currentAnchor = 'note-' + activityNo">
<h3 class="activity-date"
ng-if="currentDate !== (activities[$index-1].props.createdAt | date:'longDate')"
ng-bind="currentDate"/>
<user-activity activity="activity"
activity-no="activityNo"
current-anchor="currentAnchor">
</user-activity>
</li>
</ul>
<exclusive-edit>
<ul class="work-package-details-activities-list">
<li ng-repeat="activity in activities"
class="work-package-details-activities-activity"
ng-init="activityNo = activitiesSortedInDescendingOrder && activities.length - $index || $index + 1;
currentDate = (activity.props.createdAt|date:'longDate');
currentAnchor = 'note-' + activityNo">
<h3 class="activity-date"
ng-if="currentDate !== (activities[$index-1].props.createdAt | date:'longDate')"
ng-bind="currentDate"/>
<user-activity activity="activity"
activity-no="activityNo"
input-element-id="'add-comment-text'">
</user-activity>
</li>
</ul>
</exclusive-edit>
</div>
<activity-comment work-package="workPackage"
activities="activities">
</activity-comment>
</div>

@ -37,14 +37,19 @@
<div class="detail-panel-latest-activity">
<h3>{{ I18n.t('js.label_latest_activity') }}</h3>
<ul>
<li ng-repeat="activity in latestActitivies"
class="work-package-details-activities-activity"
ng-init="currentAnchor = 'note-' + ($index+1);
activityNo = activities.length - $index;">
<user-activity activity="activity"
activity-no="activityNo"
currentAnchor="currentAnchor">
</li>
</ul>
<exclusive-edit>
<ul>
<li ng-repeat="activity in activities | latestItems:activitiesSortedInDescendingOrder:3"
class="work-package-details-activities-activity"
ng-init="currentAnchor = 'note-' + ($index+1);
activityNo = activities.length - $index;">
<user-activity activity="activity"
activity-no="activityNo"
input-element-id="'add-comment-text'">
</li>
</ul>
</exclusive-edit>
<activity-comment work-package="workPackage"
activities="activities">
</activity-comment>
</div>

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe API::V3::Activities::ActivitiesAPI do
let(:admin) { FactoryGirl.create(:admin) }
let(:comment) { "This is a test comment!" }
shared_examples_for "safeguarded API" do
it { expect(response.response_code).to eq(403) }
end
shared_examples_for "valid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
subject { JSON.parse(response.body) }
it { expect(subject['_type']).to eq("Activity::Comment") }
it { expect(subject['rawComment']).to eq(comment) }
end
shared_examples_for "invalid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(422) }
end
describe "PUT /api/v3/activities/:activityId" do
let(:work_package) { FactoryGirl.create(:work_package) }
let(:wp_journal) { FactoryGirl.build(:journal_work_package_journal) }
let(:journal) { FactoryGirl.create(:work_package_journal,
data: wp_journal,
journable_id: work_package.id) }
shared_context "edit activity" do
before { put "/api/v3/activities/#{journal.id}",
comment: comment }
end
it_behaves_like "safeguarded API" do
include_context "edit activity"
end
it_behaves_like "valid activity request" do
include_context "edit activity"
end
it_behaves_like "invalid activity request" do
before { allow_any_instance_of(Journal).to receive(:save).and_return(false) }
include_context "edit activity"
end
end
end

@ -0,0 +1,53 @@
#-- 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.
#++
shared_examples_for "safeguarded API" do
it { expect(response.response_code).to eq(403) }
end
shared_examples_for "valid activity request" do
let(:status_code) { 200 }
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(status_code) }
describe 'response body' do
subject { JSON.parse(response.body) }
it { expect(subject['_type']).to eq("Activity::Comment") }
it { expect(subject['rawComment']).to eq(comment) }
end
end
shared_examples_for "invalid activity request" do
before { allow(User).to receive(:current).and_return(admin) }
it { expect(response.response_code).to eq(422) }
end

@ -0,0 +1,61 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'rack/test'
describe API::V3::WorkPackages::WorkPackagesAPI do
describe "activities" do
let(:admin) { FactoryGirl.create(:admin) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:comment) { "This is a test comment!" }
describe "POST /api/v3/work_packages/:id/activities" do
shared_context "create activity" do
before { post "/api/v3/work_packages/#{work_package.id}/activities",
comment: comment }
end
it_behaves_like "safeguarded API" do
include_context "create activity"
end
it_behaves_like "valid activity request" do
let(:status_code) { 201 }
include_context "create activity"
end
it_behaves_like "invalid activity request" do
before { allow_any_instance_of(WorkPackage).to receive(:save).and_return(false) }
include_context "create activity"
end
end
end
end
Loading…
Cancel
Save