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
|