diff --git a/app/assets/javascripts/angular/routing.js b/app/assets/javascripts/angular/routing.js index 0c2484ad0d..299b185526 100644 --- a/app/assets/javascripts/angular/routing.js +++ b/app/assets/javascripts/angular/routing.js @@ -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", diff --git a/app/assets/javascripts/angular/work_packages/controllers/details-tab-overview-controller.js b/app/assets/javascripts/angular/work_packages/controllers/details-tab-overview-controller.js new file mode 100644 index 0000000000..043e993544 --- /dev/null +++ b/app/assets/javascripts/angular/work_packages/controllers/details-tab-overview-controller.js @@ -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 + }; + + +}]); diff --git a/app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js b/app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js new file mode 100644 index 0000000000..ebb2caa93b --- /dev/null +++ b/app/assets/javascripts/angular/work_packages/controllers/details-tab-watchers-controller.js @@ -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); + }; +}]); diff --git a/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js b/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js index 4b42f40d17..8a917e215e 100644 --- a/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js +++ b/app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js @@ -28,12 +28,6 @@ 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", @@ -50,18 +44,12 @@ angular.module('openproject.workPackages.controllers') 'latestTab', 'workPackage', 'I18n', - 'DEFAULT_WORK_PACKAGE_PROPERTIES', - 'USER_TYPE', 'VISIBLE_LATEST', 'RELATION_TYPES', - 'CustomFieldHelper', - 'WorkPackagesHelper', - 'PathHelper', - 'UserService', '$q', + 'WorkPackagesHelper', 'ConfigurationService', - function($scope, latestTab, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, VISIBLE_LATEST, RELATION_TYPES, 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); }); @@ -82,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', { @@ -89,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; @@ -198,9 +121,9 @@ angular.module('openproject.workPackages.controllers') $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); }; @@ -213,96 +136,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 = { diff --git a/karma/tests/controllers/details-tab-overview-controller-test.js b/karma/tests/controllers/details-tab-overview-controller-test.js new file mode 100644 index 0000000000..72a1916848 --- /dev/null +++ b/karma/tests/controllers/details-tab-overview-controller-test.js @@ -0,0 +1,295 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +//++ + +/*jshint expr: true*/ + +describe('DetailsTabOverviewController', function() { + var scope; + var buildController; + var I18n = { t: angular.identity }, + WorkPackagesHelper = { + formatWorkPackageProperty: angular.identity + }, + UserService = { + getUser: angular.identity + }, + CustomFieldHelper = { + formatCustomFieldValue: angular.identity + }, + workPackage = { + props: { + status: 'open', + versionName: null, + customProperties: [ + { format: 'text', name: 'color', value: 'red' }, + ] + }, + embedded: { + activities: [], + watchers: [], + attachments: [] + }, + }; + + function buildWorkPackageWithId(id) { + angular.extend(workPackage.props, {id: id}); + return workPackage; + } + + beforeEach(module('openproject.api', 'openproject.services', 'openproject.workPackages.controllers')); + beforeEach(inject(function($rootScope, $controller, $timeout) { + var workPackageId = 99; + + buildController = function() { + scope = $rootScope.$new(); + scope.workPackage = workPackage; + + ctrl = $controller("DetailsTabOverviewController", { + $scope: scope, + I18n: I18n, + UserService: UserService, + CustomFieldHelper: CustomFieldHelper, + }); + + $timeout.flush(); + }; + + })); + + describe('initialisation', function() { + it('should initialise', function() { + buildController(); + }); + }); + + describe('work package properties', function() { + function fetchPresentPropertiesWithName(propertyName) { + return scope.presentWorkPackageProperties.filter(function(propertyData) { + return propertyData.property === propertyName; + }); + } + + describe('when the property has a value', function() { + var propertyName = 'status'; + + beforeEach(function() { + buildController(); + }); + + it('adds properties to present properties', function() { + expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1); + }); + }); + + describe('when the property is among the first 3 properties', function() { + var propertyName = 'responsible'; + + beforeEach(function() { + buildController(); + }); + + it('is added to present properties even if it is empty', function() { + expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1); + }); + }); + + describe('when the property is among the second group of 3 properties', function() { + var propertyName = 'priority', + label = 'Priority'; + + beforeEach(function() { + sinon.stub(I18n, 't') + .withArgs('js.work_packages.properties.' + propertyName) + .returns(label); + + buildController(); + }); + + afterEach(function() { + I18n.t.restore(); + }); + + describe('and none of these 3 properties is present', function() { + beforeEach(function() { + buildController(); + }); + + it('is added to the empty properties', function() { + expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1); + }); + }); + + describe('and at least one of these 3 properties is present', function() { + beforeEach(function() { + workPackage.props.percentageDone = '20'; + buildController(); + }); + + it('is added to the present properties', function() { + expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1); + }); + }); + }); + + describe('when the property is not among the first 6 properties', function() { + var propertyName = 'versionName', + label = 'Version'; + + beforeEach(function() { + sinon.stub(I18n, 't') + .withArgs('js.work_packages.properties.' + propertyName) + .returns(label); + + buildController(); + }); + + afterEach(function() { + I18n.t.restore(); + }); + + it('adds properties that without values to empty properties', function() { + expect(scope.emptyWorkPackageProperties.indexOf(label)).to.be.greaterThan(-1); + }); + }); + + describe('date property', function() { + var startDate = '2014-07-09', + dueDate = '2014-07-10', + placeholder = 'placeholder'; + + + describe('when only the due date is present', function() { + beforeEach(function() { + sinon.stub(I18n, 't') + .withArgs('js.label_no_start_date') + .returns(placeholder); + + workPackage.props.startDate = null; + workPackage.props.dueDate = dueDate; + + buildController(); + }); + + afterEach(function() { + I18n.t.restore(); + }); + + it('renders the due date and a placeholder for the start date as date property', function() { + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal(placeholder + ' - Jul 10, 2014'); + }); + }); + + describe('when only the start date is present', function() { + beforeEach(function() { + sinon.stub(I18n, 't') + .withArgs('js.label_no_due_date') + .returns(placeholder); + + workPackage.props.startDate = startDate; + workPackage.props.dueDate = null; + + buildController(); + }); + + afterEach(function() { + I18n.t.restore(); + }); + + it('renders the start date and a placeholder for the due date as date property', function() { + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - ' + placeholder); + }); + }); + + describe('when both - start and due date are present', function() { + beforeEach(function() { + workPackage.props.startDate = startDate; + workPackage.props.dueDate = dueDate; + + buildController(); + }); + + it('combines them and renders them as date property', function() { + expect(fetchPresentPropertiesWithName('date')[0].value).to.equal('Jul 9, 2014 - Jul 10, 2014'); + }); + }); + }); + + describe('custom field properties', function() { + var customPropertyName = 'color'; + + describe('when the property has a value', function() { + beforeEach(function() { + formatCustomFieldValueSpy = sinon.spy(CustomFieldHelper, 'formatCustomFieldValue'); + + buildController(); + }); + + afterEach(function() { + CustomFieldHelper.formatCustomFieldValue.restore(); + }); + + it('adds properties to present properties', function() { + expect(fetchPresentPropertiesWithName(customPropertyName)).to.have.length(1); + }); + + it('formats values using the custom field helper', function() { + expect(CustomFieldHelper.formatCustomFieldValue.calledWith('red', 'text')).to.be.true; + }); + }); + + describe('when the property does not have a value', function() { + beforeEach(function() { + workPackage.props.customProperties[0].value = null; + buildController(); + }); + + it('adds the custom property to empty properties', function() { + expect(scope.emptyWorkPackageProperties.indexOf(customPropertyName)).to.be.greaterThan(-1); + }); + }); + + describe('user custom property', function() { + var userId = '1'; + + beforeEach(function() { + workPackage.props.customProperties[0].value = userId; + workPackage.props.customProperties[0].format = 'user'; + + getUserSpy = sinon.spy(UserService, 'getUser'); + buildController(); + }); + + it('fetches the user using the user service', function() { + expect(UserService.getUser.calledWith(userId)).to.be.true; + }); + }); + }); + }); + + +}); diff --git a/karma/tests/controllers/work-package-details-controller-test.js b/karma/tests/controllers/work-package-details-controller-test.js index 04eeeff422..56869f1521 100644 --- a/karma/tests/controllers/work-package-details-controller-test.js +++ b/karma/tests/controllers/work-package-details-controller-test.js @@ -104,8 +104,6 @@ describe('WorkPackageDetailsController', function() { return false; } }, - UserService: UserService, - CustomFieldHelper: CustomFieldHelper, WorkPackagesDetailsHelper: { attachmentsTitle: function() { return ''; } }, @@ -124,215 +122,13 @@ describe('WorkPackageDetailsController', function() { }); describe('work package properties', function() { - function fetchPresentPropertiesWithName(propertyName) { - return scope.presentWorkPackageProperties.filter(function(propertyData) { - return propertyData.property === propertyName; - }); - } - - describe('when the property has a value', function() { - var propertyName = 'status'; - - beforeEach(function() { - buildController(); - }); - - it('adds properties to present properties', function() { - expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1); - }); - }); - - describe('when the property is among the first 3 properties', function() { - var propertyName = 'responsible'; - - beforeEach(function() { - buildController(); - }); - - it('is added to present properties even if it is empty', function() { - expect(fetchPresentPropertiesWithName(propertyName)).to.have.length(1); - }); - }); - - describe('when the property is among the second group of 3 properties', function() { - var propertyName = 'priority', - label = 'Priority'; - + describe('relations', function() { 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; - }); - }); - - describe('relations', function() { - beforeEach(function() { - buildController(); - }); - - it('Relation::Relates', function() { - expect(scope.relatedTo.length).to.eq(1); - }); + it('Relation::Relates', function() { + expect(scope.relatedTo.length).to.eq(1); }); }); }); diff --git a/lib/api/v3/work_packages/work_package_representer.rb b/lib/api/v3/work_packages/work_package_representer.rb index b9b7bdfd2a..94db9804f4 100644 --- a/lib/api/v3/work_packages/work_package_representer.rb +++ b/lib/api/v3/work_packages/work_package_representer.rb @@ -147,7 +147,7 @@ module API 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 - collection :watchers, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter + property :watchers, embedded: true, exec_context: :decorator collection :attachments, embedded: true, class: ::API::V3::Attachments::AttachmentModel, decorator: ::API::V3::Attachments::AttachmentRepresenter property :relations, embedded: true, exec_context: :decorator @@ -155,6 +155,10 @@ module API 'WorkPackage' 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