Merge branch 'dev' into dev-angular

pull/1255/head
Alex Coles 11 years ago
commit 9b34fb5f66
  1. 7
      CONTRIBUTING.md
  2. 9
      README.md
  3. 3
      app/assets/javascripts/angular/controllers/work-packages-controller.js
  4. 66
      app/assets/javascripts/angular/directives/timelines/timeline-column-data-directive.js
  5. 12
      app/assets/javascripts/angular/directives/timelines/timeline-container-directive.js
  6. 18
      app/assets/javascripts/angular/directives/timelines/timeline-table-container-directive.js
  7. 4
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  8. 2
      app/assets/javascripts/angular/models/timelines/planning_element.js
  9. 5
      app/assets/javascripts/angular/openproject-app.js
  10. 4
      app/assets/javascripts/angular/services/timeline-loader-service.js
  11. 2
      app/controllers/api/v3/concerns/column_data.rb
  12. 64
      app/helpers/calendars_helper.rb
  13. 5
      app/models/journal_manager.rb
  14. 2
      app/views/timelines/_timeline.html.erb
  15. 35
      config/initializers/10-patches.rb
  16. 2
      doc/CHANGELOG.md
  17. 2
      karma.conf.js
  18. 123
      karma/tests/controllers/work-packages-controller-test.js
  19. 136
      karma/tests/directives/timelines/timeline-column-data-directive-test.js
  20. 15
      karma/tests/helpers/components/work-packages-helper-test.js
  21. 22
      karma/tests/planning_element_test.js
  22. 81
      lib/plugins/acts_as_journalized/lib/redmine/acts/journalized/save_hooks.rb
  23. 8
      lib/plugins/gravatar/spec/gravatar_spec.rb
  24. 18
      public/templates/timelines/timeline_column.html
  25. 15
      public/templates/timelines/timeline_column_data.html
  26. 2
      public/templates/timelines/timeline_table.html
  27. 2
      public/templates/timelines/timeline_table_container.html
  28. 4
      spec/controllers/api/v2/planning_element_type_colors_controller_spec.rb
  29. 20
      spec/controllers/api/v2/planning_element_types_controller_spec.rb
  30. 12
      spec/controllers/api/v2/planning_elements_controller_spec.rb
  31. 4
      spec/controllers/api/v2/project_associations_controller_spec.rb
  32. 4
      spec/controllers/api/v2/project_types_controller_spec.rb
  33. 4
      spec/controllers/api/v2/projects_controller_spec.rb
  34. 28
      spec/controllers/api/v2/reported_project_statuses_controller_spec.rb
  35. 4
      spec/controllers/api/v2/reportings_controller_spec.rb
  36. 8
      spec/controllers/api/v2/timelines_controller_spec.rb
  37. 52
      spec/controllers/api/v3/concerns/column_data_spec.rb
  38. 2
      spec/controllers/search_controller_spec.rb
  39. 8
      spec/controllers/timelines_controller_spec.rb
  40. 6
      spec/controllers/work_packages/moves_controller_spec.rb
  41. 4
      spec/controllers/work_packages_controller_spec.rb
  42. 2
      spec/mailers/user_mailer_spec.rb
  43. 4
      spec/models/mail_handler_spec.rb
  44. 2
      spec/models/setting_spec.rb
  45. 8
      spec/models/user_spec.rb
  46. 18
      spec/models/work_package/work_package_acts_as_journalized_spec.rb
  47. 2
      spec/views/users/edit.html.erb_spec.rb
  48. 3
      test/functional/wiki_controller_test.rb

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

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

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

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

@ -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: '<div ng-transclude id="{{timelineContainerElementId}}"/>',
template: '<div>' +
'<div ng-hide="!!errorMessage" ng-transclude id="{{timelineContainerElementId}}"/>' +
'<div ng-if="!!errorMessage" ng-bind="errorMessage" class="flash error"/>' +
'</div>',
link: function(scope) {
scope.timelineContainerElementId = 'timeline-container-' + (++scope.timelineContainerCount);

@ -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() {

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

@ -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() {

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

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

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

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

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

@ -34,7 +34,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div ng-controller="TimelinesController">
<timeline-container>
<timeline-toolbar></timeline-toolbar>
<timeline-table-container/>
<timeline-table-container></timeline-table-container>
</timeline-container>
</div>

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

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

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

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

@ -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 = '<span timeline-column-data row-object="rowObject" column-name="columnName" timeline="timeline" custom-fields="timeline.custom_fields"></span>';
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;
});
})
});
});
});

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

@ -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 () {

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

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

@ -1,18 +0,0 @@
<span class="tl-historical" ng-show="rowObject.does_historical_differ(columnName)">
<a href="javascript://" title="{{I18n.t('js.timelines.change')}}"
ng-class="[
'icon',
'tl-icon-' + isDateColumn && historicalDateKind || 'tl-icon-changed'
]">
{{rowObject.historical()[columnName] || I18n.t('js.timelines.empty')}}
</a>
<br/>
</span>
<span ng-class="[
'tl-column',
isDateColumn && 'tl-current',
isDateColumn && rowObject.does_historical_differ(columnName) && 'tl-' + (historicalDateKind)
]">
{{columnData}}
</span>

@ -0,0 +1,15 @@
<span class="tl-historical" ng-if="historicalDataDiffers">
<span ng-bind="historicalData" class="historical-data"></span>
<a href="javascript://" title="{{labelTimelineChanged}}"
ng-class="[
'icon',
'tl-icon-' + (isDateColumn && historicalDateKind || 'changed')
]"></a>
<br/>
</span>
<span ng-class="[
'tl-column',
isDateColumn && 'tl-current',
isDateColumn && historicalDataDiffers && 'tl-' + historicalDateKind
]" ng-bind="columnData"></span>

@ -57,7 +57,7 @@
</span>
</td>
<td timeline-column
<td timeline-column-data
ng-repeat="columnName in columns"
column-name="columnName"
row-object="rowObject"

@ -2,9 +2,7 @@
<div class="tl-left">
<div class="tl-left-top tl-decoration"></div>
<div class="tl-left-main">
<timeline-table/>
</div>
</div>
<div class="tl-right">

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save