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). 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 ## 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. 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) { 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 regexp = /query_id=(\d+)/g;
var match = regexp.exec(location.search); var match = regexp.exec(location.search);

@ -29,7 +29,9 @@
angular.module('openproject.timelines.directives') angular.module('openproject.timelines.directives')
.constant('WORK_PACKAGE_DATE_COLUMNS', ['start_date', 'due_date']) .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 { return {
restrict: 'A', restrict: 'A',
scope: { scope: {
@ -38,12 +40,10 @@ angular.module('openproject.timelines.directives')
timeline: '=', timeline: '=',
customFields: '=' customFields: '='
}, },
templateUrl: '/templates/timelines/timeline_column.html', templateUrl: '/templates/timelines/timeline_column_data.html',
link: function(scope, element) { link: function(scope, element) {
scope.isDateColumn = WORK_PACKAGE_DATE_COLUMNS.indexOf(scope.columnName) !== -1; scope.isDateColumn = WORK_PACKAGE_DATE_COLUMNS.indexOf(scope.columnName) !== -1;
scope.historicalDateKind = getHistoricalDateKind(scope.rowObject, scope.columnName);
if (CustomFieldHelper.isCustomFieldKey(scope.columnName)) { if (CustomFieldHelper.isCustomFieldKey(scope.columnName)) {
// watch custom field because they are loaded after the rows are being iterated // watch custom field because they are loaded after the rows are being iterated
scope.$watch('timeline.custom_fields', function() { scope.$watch('timeline.custom_fields', function() {
@ -53,36 +53,31 @@ angular.module('openproject.timelines.directives')
scope.columnData = getColumnData(); scope.columnData = getColumnData();
} }
function getHistoricalDateKind(object, value) { setHistoricalData(scope);
if (!object.does_historical_differ()) return;
var newDate = object[value]; function getColumnData() {
var oldDate = object.historical()[value]; switch(scope.columnName) {
case 'start_date':
if (oldDate && newDate) { return scope.rowObject.start_date;
return (newDate < oldDate ? 'postponed' : 'preponed'); case 'due_date':
return scope.rowObject.due_date;
default:
return scope.rowObject.getAttribute(getAttributeAccessor(scope.columnName));
} }
return "changed";
} }
function getAttributeAccessor(attr) {
function getColumnData() { return {
var map = {
"type": "getTypeName", "type": "getTypeName",
"status": "getStatusName", "status": "getStatusName",
"responsible": "getResponsibleName", "responsible": "getResponsibleName",
"assigned_to": "getAssignedName", "assigned_to": "getAssignedName",
"project": "getProjectName" "project": "getProjectName"
}; }[attr] || attr;
}
switch(scope.columnName) { function hasChanged(planningElement, attr) {
case 'start_date': return planningElement.does_historical_differ(getAttributeAccessor(attr));
return scope.rowObject.start_date;
case 'due_date':
return scope.rowObject.due_date;
default:
return scope.rowObject[map[scope.columnName]]();
}
} }
function getCustomFieldColumnData(object, customFieldName, customFields, users) { function getCustomFieldColumnData(object, customFieldName, customFields, users) {
@ -94,6 +89,29 @@ angular.module('openproject.timelines.directives')
return CustomFieldHelper.formatCustomFieldValue(object[customFieldName], customField.field_format, users); 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') angular.module('openproject.timelines.directives')
.directive('timelineContainer', [function() { .directive('timelineContainer', ['Timeline', function(Timeline) {
getInitialOutlineExpansion = function(timelineOptions) { getInitialOutlineExpansion = function(timelineOptions) {
initialOutlineExpansion = timelineOptions.initial_outline_expansion; initialOutlineExpansion = timelineOptions.initial_outline_expansion;
if (initialOutlineExpansion && initialOutlineExpansion >= 0) { if (initialOutlineExpansion && initialOutlineExpansion >= 0) {
@ -41,8 +41,16 @@ angular.module('openproject.timelines.directives')
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
controller: function($scope) {
this.showError = function(errorMessage) {
$scope.errorMessage = errorMessage;
};
},
transclude: true, 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) { link: function(scope) {
scope.timelineContainerElementId = 'timeline-container-' + (++scope.timelineContainerCount); scope.timelineContainerElementId = 'timeline-container-' + (++scope.timelineContainerCount);

@ -34,8 +34,9 @@ angular.module('openproject.timelines.directives')
return { return {
restrict: 'E', restrict: 'E',
replace: true, replace: true,
require: '^timelineContainer',
templateUrl: '/templates/timelines/timeline_table_container.html', templateUrl: '/templates/timelines/timeline_table_container.html',
link: function(scope, element, attributes) { link: function(scope, element, attributes, timelineContainerCtrl) {
function showWarning() { function showWarning() {
scope.underConstruction = false; scope.underConstruction = false;
@ -43,6 +44,11 @@ angular.module('openproject.timelines.directives')
scope.$apply(); scope.$apply();
} }
function showError(errorMessage) {
scope.underConstruction = false;
timelineContainerCtrl.showError(errorMessage);
}
function fetchData() { function fetchData() {
return TimelineLoaderService.loadTimelineData(scope.timeline); return TimelineLoaderService.loadTimelineData(scope.timeline);
} }
@ -126,15 +132,19 @@ angular.module('openproject.timelines.directives')
function renderTimeline() { function renderTimeline() {
return fetchData() return fetchData()
.then(buildWorkPackageTable) .then(buildWorkPackageTable)
.then(drawChart); .then(drawChart, showError);
} }
function reloadTimeline() { function reloadTimeline() {
return fetchData() return fetchData()
.then(buildWorkPackageTable) .then(buildWorkPackageTable)
.then(function() { .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() { function registerModalHelper() {

@ -28,7 +28,7 @@
angular.module('openproject.workPackages.helpers') angular.module('openproject.workPackages.helpers')
.factory('WorkPackagesHelper', ['dateFilter', 'CustomFieldHelper', function(dateFilter, CustomFieldHelper) { .factory('WorkPackagesHelper', ['dateFilter', 'currencyFilter', 'CustomFieldHelper', function(dateFilter, currencyFilter, CustomFieldHelper) {
var WorkPackagesHelper = { var WorkPackagesHelper = {
getRowObjectContent: function(object, option) { getRowObjectContent: function(object, option) {
if(CustomFieldHelper.isCustomFieldKey(option)){ if(CustomFieldHelper.isCustomFieldKey(option)){
@ -98,6 +98,8 @@ angular.module('openproject.workPackages.helpers')
return dateFilter(WorkPackagesHelper.parseDateTime(value), 'medium'); return dateFilter(WorkPackagesHelper.parseDateTime(value), 'medium');
case 'date': case 'date':
return dateFilter(value, 'mediumDate'); return dateFilter(value, 'mediumDate');
case 'currency':
return currencyFilter(value, 'EUR ');
default: default:
return value; return value;
} }

@ -206,7 +206,7 @@ angular.module('openproject.timelines.models')
}, },
hasAlternateDates: function() { hasAlternateDates: function() {
return (this.does_historical_differ("start_date") || return (this.does_historical_differ("start_date") ||
this.does_historical_differ("end_date") || this.does_historical_differ("due_date") ||
this.is_deleted); this.is_deleted);
}, },
isDeleted: function() { isDeleted: function() {

@ -50,17 +50,18 @@ angular.module('openproject.workPackages.directives', ['openproject.uiComponents
// main app // main app
var openprojectApp = angular.module('openproject', ['ui.select2', 'ui.date', 'openproject.uiComponents', 'openproject.timelines', 'openproject.workPackages', 'ngAnimate']); 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 openprojectApp
.config(['$locationProvider', '$httpProvider', function($locationProvider, $httpProvider) { .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. // 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); // $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 $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) { $httpProvider.interceptors.push(function ($q) {
return { return {
'request': function (config) { 'request': function (config) {
config.url = appBasePath + config.url; config.url = window.appBasePath + config.url;
return config || $q.when(config); return config || $q.when(config);
} }
} }

@ -39,7 +39,7 @@
angular.module('openproject.timelines.services') 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 * QueueingLoader
@ -1159,14 +1159,12 @@ angular.module('openproject.timelines.services')
}); });
timeline.safetyHook = window.setTimeout(function() { timeline.safetyHook = window.setTimeout(function() {
timeline.die(I18n.t('js.timelines.errors.report_timeout'));
deferred.reject(I18n.t('js.timelines.errors.report_timeout')); deferred.reject(I18n.t('js.timelines.errors.report_timeout'));
}, Timeline.LOAD_ERROR_TIMEOUT); }, Timeline.LOAD_ERROR_TIMEOUT);
timelineLoader.load(); timelineLoader.load();
} catch (e) { } catch (e) {
timeline.die(e);
deferred.reject(e); deferred.reject(e);
} }
return deferred.promise; return deferred.promise;

@ -68,6 +68,8 @@ module Api::V3::Concerns::ColumnData
def column_data_type(column) def column_data_type(column)
if column.is_a?(QueryCustomFieldColumn) if column.is_a?(QueryCustomFieldColumn)
return column.custom_field.field_format 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?) elsif (c = WorkPackage.columns_hash[column.name.to_s] and !c.nil?)
return c.type.to_s return c.type.to_s
elsif (c = WorkPackage.columns_hash[column.name.to_s + "_id"] and !c.nil?) elsif (c = WorkPackage.columns_hash[column.name.to_s + "_id"] and !c.nil?)

@ -27,46 +27,38 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
# Provides helper methods for a project's calendar view.
module CalendarsHelper module CalendarsHelper
def link_to_previous_month(year, month, options={}) # Generates a html link to a calendar of the previous month.
target_year, target_month = if month == 1 # @param [Integer] year the current year
[year - 1, 12] # @param [Integer] month the current month
else # @param [Hash, nil] options html options passed to the link generation
[year, month - 1] # @return [String] link to the calendar
end def link_to_previous_month(year, month, options = {})
target_date = Date.new(year, month, 1) - 1.month
name = if target_month == 12 link_to_month(target_date, options.merge(class: 'navigate-left',
"#{localized_month_name target_month} #{target_year}" display_year: target_date.year != year))
else
"#{localized_month_name target_month}"
end
link_to_month(name, target_year, target_month, options.merge(:class => 'navigate-left'))
end end
def link_to_next_month(year, month, options={}) # Generates a html link to a calendar of the next month.
target_year, target_month = if month == 12 # @param [Integer] year the current year
[year + 1, 1] # @param [Integer] month the current month
else # @param [Hash, nil] options html options passed to the link generation
[year, month + 1] # @return [String] link to the calendar
end def link_to_next_month(year, month, options = {})
target_date = Date.new(year, month, 1) + 1.month
name = if target_month == 1 link_to_month(target_date, options.merge(class: 'navigate-right',
"#{localized_month_name target_month} #{target_year}" display_year: target_date.year != year))
else
"#{localized_month_name target_month}"
end
link_to_month(name, target_year, target_month, options.merge(:class => 'navigate-right'))
end end
def link_to_month(link_name, year, month, options={}) # Generates a html-link which leads to a calendar displaying the given date.
link_to_content_update(link_name, params.merge(:year => year, :month => month), options) # @param [Date, Time] date the date which should be displayed
end # @param [Hash, nil] options html options passed to the link generation
# @options options [Boolean] :display_year Whether the year should be displayed
private # @return [String] link to the calendar
def link_to_month(date_to_show, options = {})
def localized_month_name(month_index) date = date_to_show.to_date
::I18n.t('date.month_names')[month_index] 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
end end

@ -128,9 +128,12 @@ class JournalManager
def self.add_journal(journable, user = User.current, notes = "") def self.add_journal(journable, user = User.current, notes = "")
if is_journalized? journable 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, journal_attributes = { journable_id: journable.id,
journable_type: journal_class_name(journable.class), journable_type: journal_class_name(journable.class),
version: (journable.journals.count + 1), version: version,
activity_type: journable.send(:activity_type), activity_type: journable.send(:activity_type),
changed_data: journable.attributes.symbolize_keys } changed_data: journable.attributes.symbolize_keys }

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

@ -233,6 +233,41 @@ module ActiveRecord
end end
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 CollectiveIdea
module Acts module Acts
module NestedSet module NestedSet

@ -60,10 +60,12 @@ See doc/COPYRIGHT.rdoc for more details.
* `#5553` Integrate OmniAuth * `#5553` Integrate OmniAuth
* `#5632` Check whether cookies are not shared between sub-uris * `#5632` Check whether cookies are not shared between sub-uris
* `#6309` Remove API v1 & add level_list to API v2 * `#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 * `#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 * `#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 * `#7051` Wrong success message when user is not allowed to register himself
* `#7149` Fix: Wrong success message when login is already in use * `#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 * Allowed sending of mails with only cc: or bcc: fields
* Allow adding attachments to created work packages via planning elements controller * Allow adding attachments to created work packages via planning elements controller
* Remove unused rmagick dependency * 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/components/path-helper.js',
'app/assets/javascripts/angular/helpers/filters-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/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/work-packages-table-helper.js',
'app/assets/javascripts/angular/helpers/timeline-table-helper.js', 'app/assets/javascripts/angular/helpers/timeline-table-helper.js',
'app/assets/javascripts/angular/helpers/function-decorators.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/services/pagination-service.js',
"app/assets/javascripts/angular/directives/work_packages/*.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/timelines-controller.js",
"app/assets/javascripts/angular/controllers/work-packages-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", "start_date": "2012-11-11",
"due_date": "2012-11-12" "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')); 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().getMonth()).to.equal(10);
expect(peWithHistorical.alternate_end().getFullYear()).to.equal(2012); 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 () { describe('getAttribute', function () {

@ -55,7 +55,7 @@ module Redmine::Acts::Journalized
base.class_eval do base.class_eval do
after_save :save_journals after_save :save_journals
attr_accessor :journal_notes, :journal_user, :extra_journal_attributes attr_accessor :journal_notes, :journal_user, :extra_journal_attributes
end end
end end
@ -68,7 +68,7 @@ module Redmine::Acts::Journalized
JournalManager.add_journal self, @journal_user, @journal_notes if add_journal 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_user = nil
@journal_notes = nil @journal_notes = nil
@ -79,83 +79,6 @@ module Redmine::Acts::Journalized
@journal_notes ||= notes @journal_notes ||= notes
end 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 module ClassMethods
end end
end end

@ -13,7 +13,7 @@ describe "gravatar_url with a custom default URL" do
end end
it "should include the \"default\" argument in the result" do 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 end
after(:each) do after(:each) do
@ -28,17 +28,17 @@ describe "gravatar_url with default settings" do
end end
it "should have a nil default URL" do it "should have a nil default URL" do
DEFAULT_OPTIONS[:default].should be_nil expect(DEFAULT_OPTIONS[:default]).to be_nil
end end
it "should not include the \"default\" argument in the result" do it "should not include the \"default\" argument in the result" do
@url.should_not match(/&default=/) expect(@url).not_to match(/&default=/)
end end
end end
describe "gravatar with a custom title option" do describe "gravatar with a custom title option" do
it "should include the title in the result" 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
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> </span>
</td> </td>
<td timeline-column <td timeline-column-data
ng-repeat="columnName in columns" ng-repeat="columnName in columns"
column-name="columnName" column-name="columnName"
row-object="rowObject" row-object="rowObject"

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

@ -91,9 +91,9 @@ describe Api::V2::PlanningElementTypeColorsController do
else # but have to write it that way else # but have to write it that way
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => '1337', :format => 'xml' get 'show', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end

@ -51,9 +51,9 @@ describe Api::V2::PlanningElementTypesController do
describe 'with unknown project' do describe 'with unknown project' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'index', :project_id => 'blah', :format => 'xml' get 'index', :project_id => 'blah', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -114,17 +114,17 @@ describe Api::V2::PlanningElementTypesController do
describe 'with unknown project' do describe 'with unknown project' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => 'blah', :id => '1337', :format => 'xml' get 'show', :project_id => 'blah', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
describe 'with unknown planning element type' do describe 'with unknown planning element type' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml' get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -134,9 +134,9 @@ describe Api::V2::PlanningElementTypesController do
end end
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml' get 'show', :project_id => project.identifier, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -218,9 +218,9 @@ describe Api::V2::PlanningElementTypesController do
else # but have to write it that way else # but have to write it that way
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => '1337', :format => 'xml' get 'show', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end

@ -526,9 +526,9 @@ describe Api::V2::PlanningElementsController do
become_member_with_view_planning_element_permissions become_member_with_view_planning_element_permissions
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => project.id, :id => '1337', :format => 'xml' get 'show', :project_id => project.id, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
@ -786,9 +786,9 @@ describe Api::V2::PlanningElementsController do
become_member_with_delete_planning_element_permissions become_member_with_delete_planning_element_permissions
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'destroy', :project_id => project.id, :id => '1337', :format => 'xml' get 'destroy', :project_id => project.id, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
@ -834,9 +834,9 @@ describe Api::V2::PlanningElementsController do
it 'deletes the record' do it 'deletes the record' do
get 'destroy', :project_id => project.id, :id => planning_element.id, :format => 'xml' get 'destroy', :project_id => project.id, :id => planning_element.id, :format => 'xml'
expect do expect {
planning_element.reload planning_element.reload
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end

@ -124,9 +124,9 @@ describe Api::V2::ProjectAssociationsController do
describe 'w/ the current user being a member' do describe 'w/ the current user being a member' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => project.id, :id => '1337', :format => 'xml' get 'show', :project_id => project.id, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end

@ -77,9 +77,9 @@ describe Api::V2::ProjectTypesController do
describe 'show.xml' do describe 'show.xml' do
describe 'with unknown project type' do describe 'with unknown project type' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => '1337', :format => 'xml' get 'show', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end

@ -106,9 +106,9 @@ describe Api::V2::ProjectsController do
describe 'with unknown project' do describe 'with unknown project' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => 'unknown_project', :format => 'xml' get 'show', :id => 'unknown_project', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end

@ -46,9 +46,9 @@ describe Api::V2::ReportedProjectStatusesController do
describe 'index.xml' do describe 'index.xml' do
describe 'with unknown project_type' do describe 'with unknown project_type' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'index', :project_type_id => '0', :format => 'xml' get 'index', :project_type_id => '0', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -106,17 +106,17 @@ describe Api::V2::ReportedProjectStatusesController do
describe 'with unknown project_type' do describe 'with unknown project_type' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_type_id => '0', :id => '1337', :format => 'xml' get 'show', :project_type_id => '0', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
describe 'with unknown reported_project_status' do describe 'with unknown reported_project_status' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' 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
end end
@ -133,18 +133,18 @@ describe Api::V2::ReportedProjectStatusesController do
end end
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' 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
end end
describe 'with reported_project_status not available for project_type' do describe 'with reported_project_status not available for project_type' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
available_reported_project_status available_reported_project_status
expect do expect {
get 'show', :project_type_id => project_type.id, :id => '1337', :format => 'xml' 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
end end
@ -207,9 +207,9 @@ describe Api::V2::ReportedProjectStatusesController do
describe 'show.xml' do describe 'show.xml' do
describe 'with unknown reported_project_status' do describe 'with unknown reported_project_status' do
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => '1337', :format => 'xml' get 'show', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -221,9 +221,9 @@ describe Api::V2::ReportedProjectStatusesController do
end end
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :id => '1337', :format => 'xml' get 'show', :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end

@ -131,9 +131,9 @@ describe Api::V2::ReportingsController do
let(:project) { FactoryGirl.create(:project, :identifier => 'test_project') } let(:project) { FactoryGirl.create(:project, :identifier => 'test_project') }
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
get 'show', :project_id => project.id, :id => '1337', :format => 'xml' get 'show', :project_id => project.id, :id => '1337', :format => 'xml'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end

@ -165,9 +165,9 @@ describe Api::V2::TimelinesController do
become_member_with_all_permissions become_member_with_all_permissions
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
fetch :project_id => project.id, :id => '1337' fetch :project_id => project.id, :id => '1337'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
@ -189,9 +189,9 @@ describe Api::V2::TimelinesController do
let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') } let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') }
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
fetch :project_id => other_project.identifier,:id => timeline.id fetch :project_id => other_project.identifier,:id => timeline.id
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
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 # NOTE: this is how it is favored to do in RSpec3
# expect(check_block).to receive :call # expect(check_block).to receive :call
# but we have only RSpec2 here, so: # 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) @controller.send(:scan_work_package_reference, query, &check_block)
end end

@ -165,9 +165,9 @@ describe TimelinesController do
become_member_with_all_permissions become_member_with_all_permissions
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
fetch :project_id => project.id, :id => '1337' fetch :project_id => project.id, :id => '1337'
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
@ -189,9 +189,9 @@ describe TimelinesController do
let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') } let(:other_project) { FactoryGirl.create(:project, :identifier => 'other') }
it 'raises ActiveRecord::RecordNotFound errors' do it 'raises ActiveRecord::RecordNotFound errors' do
expect do expect {
fetch :project_id => other_project.identifier,:id => timeline.id fetch :project_id => other_project.identifier,:id => timeline.id
end.to raise_error(ActiveRecord::RecordNotFound) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end

@ -393,7 +393,7 @@ describe WorkPackages::MovesController do
end end
before do before do
User.stub(:current).and_return(current_user) allow(User).to receive(:current).and_return(current_user)
def self.copy_child_work_package def self.copy_child_work_package
post :create, post :create,
@ -407,7 +407,7 @@ describe WorkPackages::MovesController do
context "when cross_project_work_package_relations is disabled" do context "when cross_project_work_package_relations is disabled" do
before 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 copy_child_work_package
end end
@ -419,7 +419,7 @@ describe WorkPackages::MovesController do
context "when cross_project_work_package_relations is enabled" do context "when cross_project_work_package_relations is enabled" do
before 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 copy_child_work_package
end end

@ -293,9 +293,9 @@ describe WorkPackagesController do
it "performs a successful export" do it "performs a successful export" do
wp = work_package wp = work_package
expect do expect {
get :index, :format => 'csv' get :index, :format => 'csv'
end.to_not raise_error }.to_not raise_error
data = CSV.parse(response.body) 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 # 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 # 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']) # 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 end
it 'escapes the name attribute properly' do it 'escapes the name attribute properly' do

@ -283,7 +283,7 @@ describe MailHandler do
Setting.default_language = 'en' Setting.default_language = 'en'
Role.non_member.update_attribute :permissions, [:add_work_packages] Role.non_member.update_attribute :permissions, [:add_work_packages]
project.update_attribute :is_public, true 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 = submit_email('ticket_by_unknown_user.eml', {:issue => {:project => 'onlinestore'}, :unknown_user => 'create'})
work_package_created(work_package) work_package_created(work_package)
expect(work_package.author.active?).to be_true expect(work_package.author.active?).to be_true
@ -303,7 +303,7 @@ describe MailHandler do
expect(work_package.author).to eq(found_user) expect(work_package.author).to eq(found_user)
expect(found_user.check_password?(password)).to be_true expect(found_user.check_password?(password)).to be_true
end.to change(User, :count).by(1) }.to change(User, :count).by(1)
end end
# it "should not add an work_package if from header is missing" do # it "should not add an work_package if from header is missing" do

@ -108,7 +108,7 @@ describe Setting do
end end
it "calls no callback on invalid setting" do 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' Setting.notified_events = 'invalid'
expect(collector).to be_empty expect(collector).to be_empty
end end

@ -325,11 +325,11 @@ describe User do
end end
it 'creates a SystemUser' do it 'creates a SystemUser' do
expect do expect {
system_user = User.system system_user = User.system
expect(system_user.new_record?).to be_false expect(system_user.new_record?).to be_false
expect(system_user.is_a?(SystemUser)).to be_true expect(system_user.is_a?(SystemUser)).to be_true
end.to change(User, :count).by(1) }.to change(User, :count).by(1)
end end
end end
@ -340,10 +340,10 @@ describe User do
end end
it 'returns existing SystemUser' do it 'returns existing SystemUser' do
expect do expect {
system_user = User.system system_user = User.system
expect(system_user).to eq(@u) expect(system_user).to eq(@u)
end.to change(User, :count).by(0) }.to change(User, :count).by(0)
end end
end end
end end

@ -191,6 +191,24 @@ describe WorkPackage do
end end
end 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 end
context "attachments" do context "attachments" do

@ -39,7 +39,7 @@ describe 'users/edit' do
assign(:user, user) assign(:user, user)
assign(:auth_sources, []) assign(:auth_sources, [])
view.stub(:current_user).and_return(current_user) allow(view).to receive(:current_user).and_return(current_user)
render render
end end

@ -183,6 +183,7 @@ class WikiControllerTest < ActionController::TestCase
def test_update_stale_page_should_not_raise_an_error def test_update_stale_page_should_not_raise_an_error
journal = FactoryGirl.create :wiki_content_journal, journal = FactoryGirl.create :wiki_content_journal,
journable_id: 2, journable_id: 2,
version: 1,
data: FactoryGirl.build(:journal_wiki_content_journal, data: FactoryGirl.build(:journal_wiki_content_journal,
text: "h1. Another page\n\n\nthis is a link to ticket: #2") text: "h1. Another page\n\n\nthis is a link to ticket: #2")
@request.session[:user_id] = 2 @request.session[:user_id] = 2
@ -217,7 +218,7 @@ class WikiControllerTest < ActionController::TestCase
c.reload c.reload
assert_equal 'Previous text', c.text assert_equal 'Previous text', c.text
assert_equal journal.version, c.version assert_equal 2, c.version
end end
def test_history def test_history

Loading…
Cancel
Save