diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..73e4323b82 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +OpenProject is an open source project and we encourage you to help us out. We'd be happy if you do one of these things: + +* Create a [new work package on openproject.org](https://www.openproject.org/projects/openproject/work_packages) if you find a bug or need a feature +* Help out other people on our [forums](https://www.openproject.org/projects/openproject/boards) +* Help us [translate OpenProject to more languages](https://www.openproject.org/projects/openproject/wiki/Translations) +* Contribute code via GitHub Pull Requests, see our [contribution page](https://www.openproject.org/projects/openproject/wiki/Contribution) for more information +* See [Git Flow](https://www.openproject.org/projects/openproject/wiki/Git_Branching_Model) diff --git a/README.md b/README.md index 3a3f0f670f..50ca55eeb4 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,6 @@ OpenProject is supported by its community members, both companies and individual Please find ways to contact us on the OpenProject [support page](https://www.openproject.org/support). -## Contributing - -OpenProject is an open source project and we encourage you to help us out. We'd be happy if you do one of these things: - -* Create a [new work package on openproject.org](https://www.openproject.org/projects/openproject/work_packages) if you find a bug or need a feature -* Help out other people on our [forums](https://www.openproject.org/projects/openproject/boards) -* Help us [translate OpenProject to more languages](https://www.openproject.org/projects/openproject/wiki/Translations) -* Contribute code via GitHub Pull Requests, see our [contribution page](https://www.openproject.org/projects/openproject/wiki/Contribution) for more information - ## Community OpenProject is driven by an active group of open source enthusiasts: software engineers, project managers, creatives, and consultants. OpenProject is supported by companies as well as individuals. We share the vision to build great open source project collaboration software. diff --git a/app/assets/javascripts/angular/controllers/work-packages-controller.js b/app/assets/javascripts/angular/controllers/work-packages-controller.js index 5507be3774..ccfa886f75 100644 --- a/app/assets/javascripts/angular/controllers/work-packages-controller.js +++ b/app/assets/javascripts/angular/controllers/work-packages-controller.js @@ -33,7 +33,8 @@ angular.module('openproject.workPackages.controllers') function setUrlParams(location) { - $scope.projectIdentifier = location.pathname.split('/')[2]; + var normalisedPath = location.pathname.replace($window.appBasePath, ''); + $scope.projectIdentifier = normalisedPath.split('/')[2]; var regexp = /query_id=(\d+)/g; var match = regexp.exec(location.search); diff --git a/app/assets/javascripts/angular/directives/timelines/timeline-column-directive.js b/app/assets/javascripts/angular/directives/timelines/timeline-column-data-directive.js similarity index 68% rename from app/assets/javascripts/angular/directives/timelines/timeline-column-directive.js rename to app/assets/javascripts/angular/directives/timelines/timeline-column-data-directive.js index 735d8b4dab..bfb7cbd517 100644 --- a/app/assets/javascripts/angular/directives/timelines/timeline-column-directive.js +++ b/app/assets/javascripts/angular/directives/timelines/timeline-column-data-directive.js @@ -29,7 +29,9 @@ angular.module('openproject.timelines.directives') .constant('WORK_PACKAGE_DATE_COLUMNS', ['start_date', 'due_date']) -.directive('timelineColumn', ['WORK_PACKAGE_DATE_COLUMNS', 'I18n', 'CustomFieldHelper', function(WORK_PACKAGE_DATE_COLUMNS, I18n, CustomFieldHelper) { +.directive('timelineColumnData', ['WORK_PACKAGE_DATE_COLUMNS', 'I18n', 'CustomFieldHelper', function(WORK_PACKAGE_DATE_COLUMNS, I18n, CustomFieldHelper) { + + return { restrict: 'A', scope: { @@ -38,12 +40,10 @@ angular.module('openproject.timelines.directives') timeline: '=', customFields: '=' }, - templateUrl: '/templates/timelines/timeline_column.html', + templateUrl: '/templates/timelines/timeline_column_data.html', link: function(scope, element) { scope.isDateColumn = WORK_PACKAGE_DATE_COLUMNS.indexOf(scope.columnName) !== -1; - scope.historicalDateKind = getHistoricalDateKind(scope.rowObject, scope.columnName); - if (CustomFieldHelper.isCustomFieldKey(scope.columnName)) { // watch custom field because they are loaded after the rows are being iterated scope.$watch('timeline.custom_fields', function() { @@ -53,36 +53,31 @@ angular.module('openproject.timelines.directives') scope.columnData = getColumnData(); } - function getHistoricalDateKind(object, value) { - if (!object.does_historical_differ()) return; + setHistoricalData(scope); - var newDate = object[value]; - var oldDate = object.historical()[value]; - - if (oldDate && newDate) { - return (newDate < oldDate ? 'postponed' : 'preponed'); + function getColumnData() { + switch(scope.columnName) { + case 'start_date': + return scope.rowObject.start_date; + case 'due_date': + return scope.rowObject.due_date; + default: + return scope.rowObject.getAttribute(getAttributeAccessor(scope.columnName)); } - return "changed"; } - - function getColumnData() { - var map = { + function getAttributeAccessor(attr) { + return { "type": "getTypeName", "status": "getStatusName", "responsible": "getResponsibleName", "assigned_to": "getAssignedName", "project": "getProjectName" - }; + }[attr] || attr; + } - switch(scope.columnName) { - case 'start_date': - return scope.rowObject.start_date; - case 'due_date': - return scope.rowObject.due_date; - default: - return scope.rowObject[map[scope.columnName]](); - } + function hasChanged(planningElement, attr) { + return planningElement.does_historical_differ(getAttributeAccessor(attr)); } function getCustomFieldColumnData(object, customFieldName, customFields, users) { @@ -94,6 +89,29 @@ angular.module('openproject.timelines.directives') return CustomFieldHelper.formatCustomFieldValue(object[customFieldName], customField.field_format, users); } } + + function setHistoricalData() { + scope.historicalDataDiffers = hasChanged(scope.rowObject, scope.columnName); + + scope.historicalDateKind = getHistoricalDateKind(scope.rowObject, scope.columnName); + scope.labelTimelineChanged = I18n.t('js.timelines.change'); + + if (scope.rowObject.historical_element) { + scope.historicalData = scope.rowObject.historical_element.getAttribute(getAttributeAccessor(scope.columnName)) || I18n.t('js.timelines.empty'); + } + } + + function getHistoricalDateKind(planningElement, attr) { + if (!hasChanged(planningElement, attr)) return; + + var newDate = planningElement[attr]; + var oldDate = planningElement.historical_element[attr]; + + if (oldDate && newDate) { + return (newDate < oldDate ? 'preponed' : 'postponed'); + } + return "changed"; + } } }; }]); diff --git a/app/assets/javascripts/angular/directives/timelines/timeline-container-directive.js b/app/assets/javascripts/angular/directives/timelines/timeline-container-directive.js index 14e641e578..00d66582a5 100644 --- a/app/assets/javascripts/angular/directives/timelines/timeline-container-directive.js +++ b/app/assets/javascripts/angular/directives/timelines/timeline-container-directive.js @@ -28,7 +28,7 @@ angular.module('openproject.timelines.directives') -.directive('timelineContainer', [function() { +.directive('timelineContainer', ['Timeline', function(Timeline) { getInitialOutlineExpansion = function(timelineOptions) { initialOutlineExpansion = timelineOptions.initial_outline_expansion; if (initialOutlineExpansion && initialOutlineExpansion >= 0) { @@ -41,8 +41,16 @@ angular.module('openproject.timelines.directives') return { restrict: 'E', replace: true, + controller: function($scope) { + this.showError = function(errorMessage) { + $scope.errorMessage = errorMessage; + }; + }, transclude: true, - template: '
', + template: '
' + + '
' + + '
' + + '
', link: function(scope) { scope.timelineContainerElementId = 'timeline-container-' + (++scope.timelineContainerCount); diff --git a/app/assets/javascripts/angular/directives/timelines/timeline-table-container-directive.js b/app/assets/javascripts/angular/directives/timelines/timeline-table-container-directive.js index 495b9fc81e..bf2b4ed64c 100644 --- a/app/assets/javascripts/angular/directives/timelines/timeline-table-container-directive.js +++ b/app/assets/javascripts/angular/directives/timelines/timeline-table-container-directive.js @@ -34,8 +34,9 @@ angular.module('openproject.timelines.directives') return { restrict: 'E', replace: true, + require: '^timelineContainer', templateUrl: '/templates/timelines/timeline_table_container.html', - link: function(scope, element, attributes) { + link: function(scope, element, attributes, timelineContainerCtrl) { function showWarning() { scope.underConstruction = false; @@ -43,6 +44,11 @@ angular.module('openproject.timelines.directives') scope.$apply(); } + function showError(errorMessage) { + scope.underConstruction = false; + timelineContainerCtrl.showError(errorMessage); + } + function fetchData() { return TimelineLoaderService.loadTimelineData(scope.timeline); } @@ -126,15 +132,19 @@ angular.module('openproject.timelines.directives') function renderTimeline() { return fetchData() .then(buildWorkPackageTable) - .then(drawChart); + .then(drawChart, showError); } function reloadTimeline() { return fetchData() .then(buildWorkPackageTable) .then(function() { - scope.timeline.expandToOutlineLevel(scope.currentOutlineLevel); // also triggers rebuildAll() - }); + if (scope.currentOutlineLevel) { + scope.timeline.expandToOutlineLevel(scope.currentOutlineLevel); // also triggers rebuildAll() + } else { + scope.rebuildAll(); + } + }, showError); } function registerModalHelper() { diff --git a/app/assets/javascripts/angular/helpers/components/work-packages-helper.js b/app/assets/javascripts/angular/helpers/components/work-packages-helper.js index 8c0a286bb8..53b8cf35b0 100644 --- a/app/assets/javascripts/angular/helpers/components/work-packages-helper.js +++ b/app/assets/javascripts/angular/helpers/components/work-packages-helper.js @@ -28,7 +28,7 @@ angular.module('openproject.workPackages.helpers') -.factory('WorkPackagesHelper', ['dateFilter', 'CustomFieldHelper', function(dateFilter, CustomFieldHelper) { +.factory('WorkPackagesHelper', ['dateFilter', 'currencyFilter', 'CustomFieldHelper', function(dateFilter, currencyFilter, CustomFieldHelper) { var WorkPackagesHelper = { getRowObjectContent: function(object, option) { if(CustomFieldHelper.isCustomFieldKey(option)){ @@ -98,6 +98,8 @@ angular.module('openproject.workPackages.helpers') return dateFilter(WorkPackagesHelper.parseDateTime(value), 'medium'); case 'date': return dateFilter(value, 'mediumDate'); + case 'currency': + return currencyFilter(value, 'EUR '); default: return value; } diff --git a/app/assets/javascripts/angular/models/timelines/planning_element.js b/app/assets/javascripts/angular/models/timelines/planning_element.js index 560bbbdf51..56038aac2d 100644 --- a/app/assets/javascripts/angular/models/timelines/planning_element.js +++ b/app/assets/javascripts/angular/models/timelines/planning_element.js @@ -206,7 +206,7 @@ angular.module('openproject.timelines.models') }, hasAlternateDates: function() { return (this.does_historical_differ("start_date") || - this.does_historical_differ("end_date") || + this.does_historical_differ("due_date") || this.is_deleted); }, isDeleted: function() { diff --git a/app/assets/javascripts/angular/openproject-app.js b/app/assets/javascripts/angular/openproject-app.js index e72e640624..b242011bef 100644 --- a/app/assets/javascripts/angular/openproject-app.js +++ b/app/assets/javascripts/angular/openproject-app.js @@ -50,17 +50,18 @@ angular.module('openproject.workPackages.directives', ['openproject.uiComponents // main app var openprojectApp = angular.module('openproject', ['ui.select2', 'ui.date', 'openproject.uiComponents', 'openproject.timelines', 'openproject.workPackages', 'ngAnimate']); +window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') || ''; + openprojectApp .config(['$locationProvider', '$httpProvider', function($locationProvider, $httpProvider) { // Note: Not using this because we want to use $location to get the url params and html5Mode prevents all the links from working normally. // $locationProvider.html5Mode(true); $httpProvider.defaults.headers.common['X-CSRF-TOKEN'] = jQuery('meta[name=csrf-token]').attr('content'); // TODO find a more elegant way to keep the session alive - var appBasePath = jQuery('meta[name=app_base_path]').attr('content') || ''; $httpProvider.interceptors.push(function ($q) { return { 'request': function (config) { - config.url = appBasePath + config.url; + config.url = window.appBasePath + config.url; return config || $q.when(config); } } diff --git a/app/assets/javascripts/angular/services/timeline-loader-service.js b/app/assets/javascripts/angular/services/timeline-loader-service.js index 0a68887098..42ef215197 100644 --- a/app/assets/javascripts/angular/services/timeline-loader-service.js +++ b/app/assets/javascripts/angular/services/timeline-loader-service.js @@ -39,7 +39,7 @@ angular.module('openproject.timelines.services') -.service('TimelineLoaderService', ['$q', 'FilterQueryStringBuilder', 'Color', 'HistoricalPlanningElement', 'PlanningElement', 'PlanningElementType', 'Project', 'ProjectAssociation', 'ProjectType', 'Reporting', 'Status','Timeline', 'User', 'CustomField', function($q, FilterQueryStringBuilder, Color, HistoricalPlanningElement, PlanningElement, PlanningElementType, Project, ProjectAssociation, ProjectType, Reporting, Status,Timeline, User, CustomField) { +.service('TimelineLoaderService', ['$q', 'FilterQueryStringBuilder', 'Color', 'HistoricalPlanningElement', 'PlanningElement', 'PlanningElementType', 'Project', 'ProjectAssociation', 'ProjectType', 'Reporting', 'Status','Timeline', 'User', 'CustomField', function($q, FilterQueryStringBuilder, Color, HistoricalPlanningElement, PlanningElement, PlanningElementType, Project, ProjectAssociation, ProjectType, Reporting, Status, Timeline, User, CustomField) { /** * QueueingLoader @@ -1159,14 +1159,12 @@ angular.module('openproject.timelines.services') }); timeline.safetyHook = window.setTimeout(function() { - timeline.die(I18n.t('js.timelines.errors.report_timeout')); deferred.reject(I18n.t('js.timelines.errors.report_timeout')); }, Timeline.LOAD_ERROR_TIMEOUT); timelineLoader.load(); } catch (e) { - timeline.die(e); deferred.reject(e); } return deferred.promise; diff --git a/app/controllers/api/v3/concerns/column_data.rb b/app/controllers/api/v3/concerns/column_data.rb index a450d1f70a..7f3a9f69ab 100644 --- a/app/controllers/api/v3/concerns/column_data.rb +++ b/app/controllers/api/v3/concerns/column_data.rb @@ -68,6 +68,8 @@ module Api::V3::Concerns::ColumnData def column_data_type(column) if column.is_a?(QueryCustomFieldColumn) return column.custom_field.field_format + elsif column.class.to_s =~ /CurrencyQueryColumn/ + return 'currency' elsif (c = WorkPackage.columns_hash[column.name.to_s] and !c.nil?) return c.type.to_s elsif (c = WorkPackage.columns_hash[column.name.to_s + "_id"] and !c.nil?) diff --git a/app/helpers/calendars_helper.rb b/app/helpers/calendars_helper.rb index 6d5458e88c..283534ec93 100644 --- a/app/helpers/calendars_helper.rb +++ b/app/helpers/calendars_helper.rb @@ -27,46 +27,38 @@ # See doc/COPYRIGHT.rdoc for more details. #++ +# Provides helper methods for a project's calendar view. module CalendarsHelper - def link_to_previous_month(year, month, options={}) - target_year, target_month = if month == 1 - [year - 1, 12] - else - [year, month - 1] - end - - name = if target_month == 12 - "#{localized_month_name target_month} #{target_year}" - else - "#{localized_month_name target_month}" - end - - link_to_month(name, target_year, target_month, options.merge(:class => 'navigate-left')) + # Generates a html link to a calendar of the previous month. + # @param [Integer] year the current year + # @param [Integer] month the current month + # @param [Hash, nil] options html options passed to the link generation + # @return [String] link to the calendar + def link_to_previous_month(year, month, options = {}) + target_date = Date.new(year, month, 1) - 1.month + link_to_month(target_date, options.merge(class: 'navigate-left', + display_year: target_date.year != year)) end - def link_to_next_month(year, month, options={}) - target_year, target_month = if month == 12 - [year + 1, 1] - else - [year, month + 1] - end - - name = if target_month == 1 - "#{localized_month_name target_month} #{target_year}" - else - "#{localized_month_name target_month}" - end - - link_to_month(name, target_year, target_month, options.merge(:class => 'navigate-right')) + # Generates a html link to a calendar of the next month. + # @param [Integer] year the current year + # @param [Integer] month the current month + # @param [Hash, nil] options html options passed to the link generation + # @return [String] link to the calendar + def link_to_next_month(year, month, options = {}) + target_date = Date.new(year, month, 1) + 1.month + link_to_month(target_date, options.merge(class: 'navigate-right', + display_year: target_date.year != year)) end - def link_to_month(link_name, year, month, options={}) - link_to_content_update(link_name, params.merge(:year => year, :month => month), options) - end - - private - - def localized_month_name(month_index) - ::I18n.t('date.month_names')[month_index] + # Generates a html-link which leads to a calendar displaying the given date. + # @param [Date, Time] date the date which should be displayed + # @param [Hash, nil] options html options passed to the link generation + # @options options [Boolean] :display_year Whether the year should be displayed + # @return [String] link to the calendar + def link_to_month(date_to_show, options = {}) + date = date_to_show.to_date + name = ::I18n.l date, format: options.delete(:display_year) ? '%B %Y' : '%B' + link_to_content_update(name, params.merge(:year => date.year, :month => date.month), options) end end diff --git a/app/models/journal_manager.rb b/app/models/journal_manager.rb index 8e4c385867..4458b68316 100644 --- a/app/models/journal_manager.rb +++ b/app/models/journal_manager.rb @@ -128,9 +128,12 @@ class JournalManager def self.add_journal(journable, user = User.current, notes = "") if is_journalized? journable + # Maximum version might be nil, so use to_i here. + version = journable.journals.maximum(:version).to_i + 1 + journal_attributes = { journable_id: journable.id, journable_type: journal_class_name(journable.class), - version: (journable.journals.count + 1), + version: version, activity_type: journable.send(:activity_type), changed_data: journable.attributes.symbolize_keys } diff --git a/app/views/timelines/_timeline.html.erb b/app/views/timelines/_timeline.html.erb index 394d38ecef..e690ef5375 100644 --- a/app/views/timelines/_timeline.html.erb +++ b/app/views/timelines/_timeline.html.erb @@ -34,7 +34,7 @@ See doc/COPYRIGHT.rdoc for more details.
- +
diff --git a/config/initializers/10-patches.rb b/config/initializers/10-patches.rb index d05d06a124..d937271f78 100644 --- a/config/initializers/10-patches.rb +++ b/config/initializers/10-patches.rb @@ -233,6 +233,41 @@ module ActiveRecord end end + +# Patches to fix Hash subclasses not preserving the class on reject and select +# on Ruby 2.1.1. Apparently this will be standard behavior in Ruby 2.2, so +# check please verify things work as expected before removing this. +# +# Rails 3.2 won't receive a fix for this, but Rails 4.x has this fixed. +# Once we're using Rails 4, we can probably remove this. +# +# See +# * https://www.ruby-lang.org/en/news/2014/03/10/regression-of-hash-reject-in-ruby-2-1-1/ +# * https://github.com/rails/rails/issues/14188 +# * https://github.com/rails/rails/pull/14198/files +module ActiveSupport + class HashWithIndifferentAccess + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + end + + class OrderedHash + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + end +end + + module CollectiveIdea module Acts module NestedSet diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index afab0b3ede..f36d548149 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -60,10 +60,12 @@ See doc/COPYRIGHT.rdoc for more details. * `#5553` Integrate OmniAuth * `#5632` Check whether cookies are not shared between sub-uris * `#6309` Remove API v1 & add level_list to API v2 +* `#6310` API v1 is now deprecated and will be removed in the next major release of OpenProject * `#6310` API v2 is now deprecated and will be removed in a future version of OpenProject * `#7050` Fix: Cannot change the login when login is already taken during creation of account * `#7051` Wrong success message when user is not allowed to register himself * `#7149` Fix: Wrong success message when login is already in use +* `#7177` Fix: Journal not created in connection with deleted note * Allowed sending of mails with only cc: or bcc: fields * Allow adding attachments to created work packages via planning elements controller * Remove unused rmagick dependency diff --git a/karma.conf.js b/karma.conf.js index f412633f26..ab9dc24dce 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -34,6 +34,7 @@ module.exports = function(config) { 'app/assets/javascripts/angular/helpers/components/path-helper.js', 'app/assets/javascripts/angular/helpers/filters-helper.js', 'app/assets/javascripts/angular/helpers/components/work-packages-helper.js', + 'app/assets/javascripts/angular/helpers/work-package-loading-helper.js', 'app/assets/javascripts/angular/helpers/work-packages-table-helper.js', 'app/assets/javascripts/angular/helpers/timeline-table-helper.js', 'app/assets/javascripts/angular/helpers/function-decorators.js', @@ -76,6 +77,7 @@ module.exports = function(config) { 'app/assets/javascripts/angular/services/pagination-service.js', "app/assets/javascripts/angular/directives/work_packages/*.js", + "app/assets/javascripts/angular/directives/timelines/*.js", "app/assets/javascripts/angular/controllers/timelines-controller.js", "app/assets/javascripts/angular/controllers/work-packages-controller.js", diff --git a/karma/tests/controllers/work-packages-controller-test.js b/karma/tests/controllers/work-packages-controller-test.js new file mode 100644 index 0000000000..3e3416f55b --- /dev/null +++ b/karma/tests/controllers/work-packages-controller-test.js @@ -0,0 +1,123 @@ +//-- 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('WorkPackagesController', function() { + var scope, ctrl, win, testWorkPackageService, testQueryService, testPaginationService; + var buildController; + + beforeEach(module('openproject.workPackages.controllers')); + beforeEach(inject(function($rootScope, $controller, $timeout) { + scope = $rootScope.$new(); + win = { + location: { pathname: "" } + }; + + var workPackageData = { + meta: { + } + }; + var columnData = { + }; + + testWorkPackageService = { + getWorkPackages: function () { + }, + getWorkPackagesByQueryId: function (params) { + return $timeout(function () { + return workPackageData; + }, 10); + }, + getWorkPackagesFromUrlQueryParams: function () { + return $timeout(function () { + return workPackageData; + }, 10); + } + }; + testQueryService = { + getQuery: function () { + return { + serialiseForAngular: function () { + } + } + }, + initQuery: function () { + }, + getAvailableColumns: function () { + return $timeout(function () { + return columnData; + }, 10); + } + }; + testPaginationService = { + setPerPageOptions: function () { + }, + setPerPage: function () { + }, + setPage: function () { + } + }; + + buildController = function() { + ctrl = $controller("WorkPackagesController", { + $scope: scope, + $window: win, + QueryService: testQueryService, + PaginationService: testPaginationService, + WorkPackageService: testWorkPackageService + }); + + $timeout.flush(); + }; + + })); + + describe('initialisation', function() { + it('should initialise', function() { + buildController(); + expect(scope.loading).to.be.false; + }); + }); + + describe('setting projectIdentifier', function() { + it('should set the projectIdentifier', function() { + win.location.pathname = '/projects/my-project/something-else'; + buildController(); + expect(scope.projectIdentifier).to.eq('my-project'); + }); + + it('should set the projectIdentifier with a custom appBasePath', function() { + win.appBasePath = '/my-instanz'; + win.location.pathname = '/my-instanz/projects/my-project/something-else'; + buildController(); + expect(scope.projectIdentifier).to.eq('my-project'); + }); + }); + +}); diff --git a/karma/tests/directives/timelines/timeline-column-data-directive-test.js b/karma/tests/directives/timelines/timeline-column-data-directive-test.js new file mode 100644 index 0000000000..6ab9e774be --- /dev/null +++ b/karma/tests/directives/timelines/timeline-column-data-directive-test.js @@ -0,0 +1,136 @@ +//-- 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('timelineColumnData Directive', function() { + var compile, element, rootScope, scope; + + beforeEach(angular.mock.module('openproject.timelines.directives')); + beforeEach(module('templates', 'openproject.uiComponents', 'openproject.helpers')); + + beforeEach(inject(function($rootScope, $compile) { + var html; + html = ''; + + element = angular.element(html); + rootScope = $rootScope; + scope = $rootScope.$new(); + + compile = function() { + $compile(element)(scope); + scope.$digest(); + }; + })); + + describe('element', function() { + beforeEach(function() { + type = Factory.build('PlanningElementType'); + + scope.timeline = Factory.build('Timeline'); + scope.rowObject = Factory.build("PlanningElement", { + timeline: scope.timeline, + planning_element_type: type, + sheep: 10, + start_date: '2014-04-29', + due_date: '2014-04-28' + }); + }); + + describe('rendering an object field', function() { + beforeEach(function(){ + scope.columnName = 'type'; + compile(); + }); + + it('should render the object data', function() { + expect(element.find('.tl-column').text()).to.equal(type.name); + }); + }); + + describe('rendering a changed historical date', function() { + beforeEach(function() { + historicalStartDate = '2014-04-20'; + + scope.rowObject.historical_element = Factory.build("PlanningElement", { + start_date: historicalStartDate + }); + + scope.columnName = 'start_date'; + compile(); + }) + + it('should assign a change kind class to the current date', function() { + var container = element.find('.tl-column'); + expect(container.hasClass('tl-postponed')).to.be.true; + }); + + describe('the historical data container', function() { + beforeEach(function() { + historicalContainerElement = element.find('.tl-historical'); + historicalDataContainer = historicalContainerElement.find('.historical-data'); + }); + + it('should contain the historical data', function() { + expect(historicalDataContainer.text()).to.equal(historicalStartDate); + }); + + it('should contain a link with a css class indicating the change', function() { + expect(historicalContainerElement.find('a').hasClass('tl-icon-postponed')).to.be.true; + }); + }) + }); + + describe('rendering changed data which is not a date', function() { + beforeEach(function() { + historicalType = Factory.build('PlanningElementType'); + + scope.rowObject.historical_element = Factory.build("PlanningElement", { + planning_element_type: historicalType + }); + + scope.columnName = 'type'; + compile(); + }) + + describe('the historical data container', function() { + beforeEach(function() { + historicalContainerElement = element.find('.tl-historical'); + historicalDataContainer = historicalContainerElement.find('.historical-data'); + }); + + it('should contain the historical data', function() { + expect(historicalDataContainer.text()).to.equal(historicalType.name); + }); + + it('should contain a link with a css class indicating the change', function() { + expect(historicalContainerElement.find('a').hasClass('tl-icon-changed')).to.be.true; + }); + }) + + }); + }); +}); diff --git a/karma/tests/helpers/components/work-packages-helper-test.js b/karma/tests/helpers/components/work-packages-helper-test.js index 2673f4da3e..6629d1a869 100644 --- a/karma/tests/helpers/components/work-packages-helper-test.js +++ b/karma/tests/helpers/components/work-packages-helper-test.js @@ -105,4 +105,19 @@ describe('Work packages helper', function() { }); }); + + describe('formatValue', function() { + var formatValue; + + beforeEach(function() { + formatValue = WorkPackagesHelper.formatValue; + }); + + it('should display a currency value', function() { + expect(formatValue(99, 'currency')).to.equal("EUR 99.00"); + expect(formatValue(20.99, 'currency')).to.equal("EUR 20.99"); + expect(formatValue("20", 'currency')).to.equal("EUR 20.00"); + }); + }); + }); diff --git a/karma/tests/planning_element_test.js b/karma/tests/planning_element_test.js index b10e841bda..e78c3deae5 100644 --- a/karma/tests/planning_element_test.js +++ b/karma/tests/planning_element_test.js @@ -42,6 +42,16 @@ describe('Planning Element', function(){ "start_date": "2012-11-11", "due_date": "2012-11-12" }); + + this.peWithDueDate = Factory.build("PlanningElement", { + timeline: Factory.build("Timeline"), + "due_date": "2012-11-12" + }); + + this.peWithStartDate = Factory.build("PlanningElement", { + timeline: Factory.build("Timeline"), + "start_date": "2012-11-11", + }); }); beforeEach(module('openproject.timelines.models', 'openproject.uiComponents')); @@ -227,6 +237,18 @@ describe('Planning Element', function(){ expect(peWithHistorical.alternate_end().getMonth()).to.equal(10); expect(peWithHistorical.alternate_end().getFullYear()).to.equal(2012); }); + + it('historical should have alternate dates with only one date different', function () { + peWithHistorical = Factory.build("PlanningElement", { + historical_element: this.peWithDueDate + }); + expect(peWithHistorical.hasAlternateDates()).to.be.true; + + peWithHistorical = Factory.build("PlanningElement", { + historical_element: this.peWithStartDate + }); + expect(peWithHistorical.hasAlternateDates()).to.be.true; + }); }); describe('getAttribute', function () { diff --git a/lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb b/lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb index d475a8329d..8613032476 100644 --- a/lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb +++ b/lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb @@ -55,7 +55,7 @@ module Redmine::Acts::Journalized base.class_eval do after_save :save_journals - + attr_accessor :journal_notes, :journal_user, :extra_journal_attributes end end @@ -68,7 +68,7 @@ module Redmine::Acts::Journalized JournalManager.add_journal self, @journal_user, @journal_notes if add_journal - journals.select{|j| j.new_record?}.each {|j| j.save} + journals.select { |j| j.new_record? }.each { |j| j.save! } @journal_user = nil @journal_notes = nil @@ -79,83 +79,6 @@ module Redmine::Acts::Journalized @journal_notes ||= notes end - ## Saves the current custom values, notes and journal to include them in the next journal - ## Called before save - #def init_journal(user = User.current, notes = "") - # @journal_notes ||= notes - # @journal_user ||= user - # @associations_before_save ||= {} - - # @associations = {} - # save_possible_association :custom_values, :key => :custom_field_id, :value => :value - # save_possible_association :attachments, :key => :id, :value => :filename - - # @current_journal ||= last_journal - #end - - ## Saves the notes and custom value changes in the last Journal - ## Called before create_journal - #def update_journal - # unless (@associations || {}).empty? - # changed_associations = {} - # changed_associations.merge!(possibly_updated_association :custom_values) - # changed_associations.merge!(possibly_updated_association :attachments) - # end - - # unless changed_associations.blank? - # update_extended_journal_contents(changed_associations) - # end - #end - - #def reset_instance_variables - # if last_journal != @current_journal - # if last_journal.user != @journal_user - # last_journal.update_attribute(:user_id, @journal_user.id) - # end - # end - # @associations_before_save = @current_journal = @journal_notes = @journal_user = @extra_journal_attributes = nil - #end - - #def save_possible_association(method, options) - # @associations[method] = options - # if self.respond_to? method - # @associations_before_save[method] ||= send(method).inject({}) do |hash, cv| - # hash[cv.send(options[:key])] = cv.send(options[:value]) - # hash - # end - # end - #end - - #def possibly_updated_association(method) - # if @associations_before_save[method] - # # Has custom values from init_journal_notes - # return changed_associations(method, @associations_before_save[method]) - # end - # {} - #end - - ## Saves the notes and changed custom values to the journal - ## Creates a new journal, if no immediate attributes were changed - #def update_extended_journal_contents(changed_associations) - # journal_changes.merge!(changed_associations) - #end - - #def changed_associations(method, previous) - # send(method).reload # Make sure the associations are reloaded - # send(method).inject({}) do |hash, c| - # key = c.send(@associations[method][:key]) - # new_value = c.send(@associations[method][:value]) - - # if previous[key].blank? && new_value.blank? - # # The key was empty before, don't add a blank value - # elsif previous[key] != new_value - # # The key's value changed - # hash["#{method}#{key}"] = [previous[key], new_value] - # end - # hash - # end - #end - module ClassMethods end end diff --git a/lib/plugins/gravatar/spec/gravatar_spec.rb b/lib/plugins/gravatar/spec/gravatar_spec.rb index 4ccfa0e56c..9f81648ff5 100644 --- a/lib/plugins/gravatar/spec/gravatar_spec.rb +++ b/lib/plugins/gravatar/spec/gravatar_spec.rb @@ -13,7 +13,7 @@ describe "gravatar_url with a custom default URL" do end it "should include the \"default\" argument in the result" do - @url.should match(/&default=no_avatar.png/) + expect(@url).to match(/&default=no_avatar.png/) end after(:each) do @@ -28,17 +28,17 @@ describe "gravatar_url with default settings" do end it "should have a nil default URL" do - DEFAULT_OPTIONS[:default].should be_nil + expect(DEFAULT_OPTIONS[:default]).to be_nil end it "should not include the \"default\" argument in the result" do - @url.should_not match(/&default=/) + expect(@url).not_to match(/&default=/) end end describe "gravatar with a custom title option" do it "should include the title in the result" do - gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/) + expect(gravatar('example@example.com', :title => "This is a title attribute")).to match(/This is a title attribute/) end end diff --git a/public/templates/timelines/timeline_column.html b/public/templates/timelines/timeline_column.html deleted file mode 100644 index 59eebeeaed..0000000000 --- a/public/templates/timelines/timeline_column.html +++ /dev/null @@ -1,18 +0,0 @@ - - - {{rowObject.historical()[columnName] || I18n.t('js.timelines.empty')}} - -
-
- - - {{columnData}} - diff --git a/public/templates/timelines/timeline_column_data.html b/public/templates/timelines/timeline_column_data.html new file mode 100644 index 0000000000..3e4793191f --- /dev/null +++ b/public/templates/timelines/timeline_column_data.html @@ -0,0 +1,15 @@ + + + +
+
+ + diff --git a/public/templates/timelines/timeline_table.html b/public/templates/timelines/timeline_table.html index c463f81b12..cc11ae4eb9 100644 --- a/public/templates/timelines/timeline_table.html +++ b/public/templates/timelines/timeline_table.html @@ -57,7 +57,7 @@ -
- -
diff --git a/spec/controllers/api/v2/planning_element_type_colors_controller_spec.rb b/spec/controllers/api/v2/planning_element_type_colors_controller_spec.rb index bb43148114..8f70eba5de 100644 --- a/spec/controllers/api/v2/planning_element_type_colors_controller_spec.rb +++ b/spec/controllers/api/v2/planning_element_type_colors_controller_spec.rb @@ -91,9 +91,9 @@ describe Api::V2::PlanningElementTypeColorsController do else # but have to write it that way it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/api/v2/planning_element_types_controller_spec.rb b/spec/controllers/api/v2/planning_element_types_controller_spec.rb index d929ef40c4..5e591e0684 100644 --- a/spec/controllers/api/v2/planning_element_types_controller_spec.rb +++ b/spec/controllers/api/v2/planning_element_types_controller_spec.rb @@ -51,9 +51,9 @@ describe Api::V2::PlanningElementTypesController do describe 'with unknown project' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'index', :project_id => 'blah', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -114,17 +114,17 @@ describe Api::V2::PlanningElementTypesController do describe 'with unknown project' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => 'blah', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end describe 'with unknown planning element type' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -134,9 +134,9 @@ describe Api::V2::PlanningElementTypesController do end it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -218,9 +218,9 @@ describe Api::V2::PlanningElementTypesController do else # but have to write it that way it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/api/v2/planning_elements_controller_spec.rb b/spec/controllers/api/v2/planning_elements_controller_spec.rb index 7bb8a59f69..13bda542da 100644 --- a/spec/controllers/api/v2/planning_elements_controller_spec.rb +++ b/spec/controllers/api/v2/planning_elements_controller_spec.rb @@ -526,9 +526,9 @@ describe Api::V2::PlanningElementsController do become_member_with_view_planning_element_permissions it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => project.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end @@ -786,9 +786,9 @@ describe Api::V2::PlanningElementsController do become_member_with_delete_planning_element_permissions it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'destroy', :project_id => project.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end @@ -834,9 +834,9 @@ describe Api::V2::PlanningElementsController do it 'deletes the record' do get 'destroy', :project_id => project.id, :id => planning_element.id, :format => 'xml' - expect do + expect { planning_element.reload - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/api/v2/project_associations_controller_spec.rb b/spec/controllers/api/v2/project_associations_controller_spec.rb index c424641a0f..38859d467d 100644 --- a/spec/controllers/api/v2/project_associations_controller_spec.rb +++ b/spec/controllers/api/v2/project_associations_controller_spec.rb @@ -124,9 +124,9 @@ describe Api::V2::ProjectAssociationsController do describe 'w/ the current user being a member' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => project.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/api/v2/project_types_controller_spec.rb b/spec/controllers/api/v2/project_types_controller_spec.rb index 20120e73c9..d4ea65f37b 100644 --- a/spec/controllers/api/v2/project_types_controller_spec.rb +++ b/spec/controllers/api/v2/project_types_controller_spec.rb @@ -77,9 +77,9 @@ describe Api::V2::ProjectTypesController do describe 'show.xml' do describe 'with unknown project type' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/api/v2/projects_controller_spec.rb b/spec/controllers/api/v2/projects_controller_spec.rb index df4ecd9522..a66f8e62df 100644 --- a/spec/controllers/api/v2/projects_controller_spec.rb +++ b/spec/controllers/api/v2/projects_controller_spec.rb @@ -106,9 +106,9 @@ describe Api::V2::ProjectsController do describe 'with unknown project' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => 'unknown_project', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/api/v2/reported_project_statuses_controller_spec.rb b/spec/controllers/api/v2/reported_project_statuses_controller_spec.rb index c460d8e488..33c4be9d84 100644 --- a/spec/controllers/api/v2/reported_project_statuses_controller_spec.rb +++ b/spec/controllers/api/v2/reported_project_statuses_controller_spec.rb @@ -46,9 +46,9 @@ describe Api::V2::ReportedProjectStatusesController do describe 'index.xml' do describe 'with unknown project_type' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'index', :project_type_id => '0', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -106,17 +106,17 @@ describe Api::V2::ReportedProjectStatusesController do describe 'with unknown project_type' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_type_id => '0', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end describe 'with unknown reported_project_status' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -133,18 +133,18 @@ describe Api::V2::ReportedProjectStatusesController do end it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end describe 'with reported_project_status not available for project_type' do it 'raises ActiveRecord::RecordNotFound errors' do available_reported_project_status - expect do + expect { get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -207,9 +207,9 @@ describe Api::V2::ReportedProjectStatusesController do describe 'show.xml' do describe 'with unknown reported_project_status' do it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -221,9 +221,9 @@ describe Api::V2::ReportedProjectStatusesController do end it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/api/v2/reportings_controller_spec.rb b/spec/controllers/api/v2/reportings_controller_spec.rb index d217310a39..74fc052a4b 100644 --- a/spec/controllers/api/v2/reportings_controller_spec.rb +++ b/spec/controllers/api/v2/reportings_controller_spec.rb @@ -131,9 +131,9 @@ describe Api::V2::ReportingsController do let(:project) { FactoryGirl.create(:project, :identifier => 'test_project') } it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { get 'show', :project_id => project.id, :id => '1337', :format => 'xml' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end diff --git a/spec/controllers/api/v2/timelines_controller_spec.rb b/spec/controllers/api/v2/timelines_controller_spec.rb index c79faa0829..1007c25792 100644 --- a/spec/controllers/api/v2/timelines_controller_spec.rb +++ b/spec/controllers/api/v2/timelines_controller_spec.rb @@ -165,9 +165,9 @@ describe Api::V2::TimelinesController do become_member_with_all_permissions it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { fetch :project_id => project.id, :id => '1337' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end @@ -189,9 +189,9 @@ describe Api::V2::TimelinesController do let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') } it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { fetch :project_id => other_project.identifier,:id => timeline.id - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/api/v3/concerns/column_data_spec.rb b/spec/controllers/api/v3/concerns/column_data_spec.rb new file mode 100644 index 0000000000..6c5b130f7f --- /dev/null +++ b/spec/controllers/api/v3/concerns/column_data_spec.rb @@ -0,0 +1,52 @@ +#-- 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 File.expand_path('../../../../../spec_helper', __FILE__) + +describe 'ColumnData' do + include Api::V3::Concerns::ColumnData + + describe '#column_data_type' do + it 'should recognise custom field columns based on field format' do + field = double('field', id: 1, order_statement: '', field_format: 'user') + column = ::QueryCustomFieldColumn.new(field) + + expect(column_data_type(column)).to eq('user') + end + + it 'should recognise Currency columns based on class name' do + DodgeCoinCurrencyQueryColumn = Class.new(QueryColumn) + column = DodgeCoinCurrencyQueryColumn.new('overspend') + + expect(column_data_type(column)).to eq('currency') + end + + xit 'should test the full gamut of types' + end + +end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 29e6308184..72c72b7d40 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -204,7 +204,7 @@ describe SearchController do # NOTE: this is how it is favored to do in RSpec3 # expect(check_block).to receive :call # but we have only RSpec2 here, so: - check_block.should_receive :call + expect(check_block).to receive :call @controller.send(:scan_work_package_reference, query, &check_block) end diff --git a/spec/controllers/timelines_controller_spec.rb b/spec/controllers/timelines_controller_spec.rb index ffde174bf2..14008c6d98 100644 --- a/spec/controllers/timelines_controller_spec.rb +++ b/spec/controllers/timelines_controller_spec.rb @@ -165,9 +165,9 @@ describe TimelinesController do become_member_with_all_permissions it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { fetch :project_id => project.id, :id => '1337' - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end end @@ -189,9 +189,9 @@ describe TimelinesController do let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') } it 'raises ActiveRecord::RecordNotFound errors' do - expect do + expect { fetch :project_id => other_project.identifier,:id => timeline.id - end.to raise_error(ActiveRecord::RecordNotFound) + }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/work_packages/moves_controller_spec.rb b/spec/controllers/work_packages/moves_controller_spec.rb index da2b0ee4d3..b169ba111f 100644 --- a/spec/controllers/work_packages/moves_controller_spec.rb +++ b/spec/controllers/work_packages/moves_controller_spec.rb @@ -393,7 +393,7 @@ describe WorkPackages::MovesController do end before do - User.stub(:current).and_return(current_user) + allow(User).to receive(:current).and_return(current_user) def self.copy_child_work_package post :create, @@ -407,7 +407,7 @@ describe WorkPackages::MovesController do context "when cross_project_work_package_relations is disabled" do before do - Setting.stub(:cross_project_work_package_relations?).and_return(false) + allow(Setting).to receive(:cross_project_work_package_relations?).and_return(false) copy_child_work_package end @@ -419,7 +419,7 @@ describe WorkPackages::MovesController do context "when cross_project_work_package_relations is enabled" do before do - Setting.stub(:cross_project_work_package_relations?).and_return(true) + allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true) copy_child_work_package end diff --git a/spec/controllers/work_packages_controller_spec.rb b/spec/controllers/work_packages_controller_spec.rb index d0c2d070fc..742a845c3b 100644 --- a/spec/controllers/work_packages_controller_spec.rb +++ b/spec/controllers/work_packages_controller_spec.rb @@ -293,9 +293,9 @@ describe WorkPackagesController do it "performs a successful export" do wp = work_package - expect do + expect { get :index, :format => 'csv' - end.to_not raise_error + }.to_not raise_error data = CSV.parse(response.body) diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 5a4f01282b..e9dde92121 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -57,7 +57,7 @@ describe UserMailer do # the name method uses a format setting to determine how to concatenate first name # and last name whereby an unescaped comma will lead to have two email addresses # defined instead of one (['Bobbi', 'bob.bobbi@example.com'] vs. ['bob.bobbi@example.com']) - test_user.stub(:name).and_return('Bobbi, Bob') + allow(test_user).to receive(:name).and_return('Bobbi, Bob') end it 'escapes the name attribute properly' do diff --git a/spec/models/mail_handler_spec.rb b/spec/models/mail_handler_spec.rb index 5aed5ceef7..b66ff6ad7b 100644 --- a/spec/models/mail_handler_spec.rb +++ b/spec/models/mail_handler_spec.rb @@ -283,7 +283,7 @@ describe MailHandler do Setting.default_language = 'en' Role.non_member.update_attribute :permissions, [:add_work_packages] project.update_attribute :is_public, true - expect do + expect { work_package = submit_email('ticket_by_unknown_user.eml', {:issue => {:project => 'onlinestore'}, :unknown_user => 'create'}) work_package_created(work_package) expect(work_package.author.active?).to be_true @@ -303,7 +303,7 @@ describe MailHandler do expect(work_package.author).to eq(found_user) expect(found_user.check_password?(password)).to be_true - end.to change(User, :count).by(1) + }.to change(User, :count).by(1) end # it "should not add an work_package if from header is missing" do diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 42a2df0e44..83e39f9b27 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -108,7 +108,7 @@ describe Setting do end it "calls no callback on invalid setting" do - Setting.any_instance.stub(:valid?).and_return(false) + allow_any_instance_of(Setting).to receive(:valid?).and_return(false) Setting.notified_events = 'invalid' expect(collector).to be_empty end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6b711aa673..cc51a59b79 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -325,11 +325,11 @@ describe User do end it 'creates a SystemUser' do - expect do + expect { system_user = User.system expect(system_user.new_record?).to be_false expect(system_user.is_a?(SystemUser)).to be_true - end.to change(User, :count).by(1) + }.to change(User, :count).by(1) end end @@ -340,10 +340,10 @@ describe User do end it 'returns existing SystemUser' do - expect do + expect { system_user = User.system expect(system_user).to eq(@u) - end.to change(User, :count).by(0) + }.to change(User, :count).by(0) end end end diff --git a/spec/models/work_package/work_package_acts_as_journalized_spec.rb b/spec/models/work_package/work_package_acts_as_journalized_spec.rb index f555cde70c..5fa2f27e5e 100644 --- a/spec/models/work_package/work_package_acts_as_journalized_spec.rb +++ b/spec/models/work_package/work_package_acts_as_journalized_spec.rb @@ -191,6 +191,24 @@ describe WorkPackage do end end end + + describe "adding journal with a missing journal and an existing journal" do + before do + work_package.update_by!(current_user, notes: 'note to be deleted') + work_package.reload + work_package.update_by!(current_user, description: 'description v2') + work_package.reload + work_package.journals.find_by_notes('note to be deleted').delete + + work_package.update_by!(current_user, description: 'description v4') + end + + it 'should create a journal for the last change' do + last_journal = work_package.journals.order(:id).last + + expect(last_journal.data.description).to eql('description v4') + end + end end context "attachments" do diff --git a/spec/views/users/edit.html.erb_spec.rb b/spec/views/users/edit.html.erb_spec.rb index 1ffc390417..8f7b30371b 100644 --- a/spec/views/users/edit.html.erb_spec.rb +++ b/spec/views/users/edit.html.erb_spec.rb @@ -39,7 +39,7 @@ describe 'users/edit' do assign(:user, user) assign(:auth_sources, []) - view.stub(:current_user).and_return(current_user) + allow(view).to receive(:current_user).and_return(current_user) render end diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index b13df6baab..31d768924c 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -183,6 +183,7 @@ class WikiControllerTest < ActionController::TestCase def test_update_stale_page_should_not_raise_an_error journal = FactoryGirl.create :wiki_content_journal, journable_id: 2, + version: 1, data: FactoryGirl.build(:journal_wiki_content_journal, text: "h1. Another page\n\n\nthis is a link to ticket: #2") @request.session[:user_id] = 2 @@ -217,7 +218,7 @@ class WikiControllerTest < ActionController::TestCase c.reload assert_equal 'Previous text', c.text - assert_equal journal.version, c.version + assert_equal 2, c.version end def test_history