Merge remote-tracking branch 'origin/feature/rails3' into fix/vertical_pe_loading

Conflicts:
	app/controllers/api/v2/planning_elements_controller.rb
pull/541/head
Nils Kenneweg 11 years ago
commit 30a4a008aa
  1. 3
      .travis.yml
  2. 2
      Gemfile
  3. 9
      Gemfile.lock
  4. 90
      app/assets/javascripts/keyboard_shortcuts.js
  5. 11
      app/assets/javascripts/modal.js
  6. 54
      app/assets/javascripts/timelines.js
  7. 6
      app/assets/javascripts/timelines_select_boxes.js
  8. 12
      app/assets/stylesheets/default/application.css.erb
  9. 8
      app/controllers/api/v1/issues_controller.rb
  10. 57
      app/controllers/api/v2/planning_element_statuses_controller.rb
  11. 88
      app/controllers/api/v2/planning_elements_controller.rb
  12. 10
      app/controllers/application_controller.rb
  13. 50
      app/controllers/planning_element_statuses_controller.rb
  14. 38
      app/controllers/work_packages/bulk_controller.rb
  15. 36
      app/controllers/work_packages_controller.rb
  16. 23
      app/helpers/application_helper.rb
  17. 2
      app/helpers/timelines_helper.rb
  18. 20
      app/helpers/work_packages_helper.rb
  19. 2
      app/models/enumeration.rb
  20. 2
      app/models/query.rb
  21. 2
      app/models/time_entry.rb
  22. 2
      app/models/timeline.rb
  23. 6
      app/models/work_package.rb
  24. 97
      app/models/work_package/ask_before_destruction.rb
  25. 4
      app/models/work_package/pdf_exporter.rb
  26. 50
      app/models/work_package/time_entries.rb
  27. 2
      app/views/admin/info.html.erb
  28. 2
      app/views/common/feed.atom.builder
  29. 2
      app/views/enumerations/index.html.erb
  30. 8
      app/views/help/keyboard_shortcuts.erb
  31. 5
      app/views/layouts/_action_menu.html.erb
  32. 2
      app/views/layouts/base.html.erb
  33. 6
      app/views/statuses/_form.html.erb
  34. 26
      app/views/timelines/_comparison.html.erb
  35. 1
      app/views/timelines/_timeline.html.erb
  36. 5
      app/views/work_packages/_action_menu.html.erb
  37. 45
      app/views/work_packages/bulk/destroy.html.erb
  38. 2
      app/views/work_packages/bulk/edit.html.erb
  39. 4
      app/views/work_packages/context_menus/index.html.erb
  40. 38
      config/locales/de.yml
  41. 39
      config/locales/en.yml
  42. 8
      config/routes.rb
  43. 33
      db/migrate/20131015064141_migrate_timelines_end_date_property_in_options.rb
  44. 78
      db/migrate/20131024115743_migrate_remaining_core_settings.rb
  45. 157
      db/migrate/20131024140048_migrate_timelines_options.rb
  46. 4
      db/migrate/migration_utils/attachable_utils.rb
  47. 66
      db/migrate/migration_utils/timelines.rb
  48. 12
      doc/CHANGELOG.md
  49. BIN
      features/step_definitions/.role_steps.rb.swp
  50. 59
      features/step_definitions/general_steps.rb
  51. 29
      features/step_definitions/planning_element_status_steps.rb
  52. 92
      features/step_definitions/role_steps.rb
  53. 8
      features/step_definitions/timelines_given_steps.rb
  54. 3
      features/support/paths.rb
  55. 3
      features/timelines/timeline_wiki_macro.feature
  56. BIN
      features/work_packages/.destroy.feature.swp
  57. 68
      features/work_packages/destroy.feature
  58. 10
      lib/open_project/access_keys.rb
  59. 4
      lib/open_project/info.rb
  60. 2
      lib/pagination/controller.rb
  61. 7
      lib/redmine.rb
  62. 8
      lib/redmine/menu_manager/top_menu_helper.rb
  63. 55
      lib/tasks/remove_timelines_historical_comparison_from_options.rake
  64. 2
      script/about
  65. 122
      spec/controllers/api/v2/planning_element_statuses_controller_spec.rb
  66. 38
      spec/controllers/statuses_controller_spec.rb
  67. 53
      spec/controllers/work_packages/bulk_controller_spec.rb
  68. 16
      spec/controllers/work_packages/context_menus_controller_spec.rb
  69. 4
      spec/factories/custom_field_factory.rb
  70. 4
      spec/helpers/application_helper_spec.rb
  71. 289
      spec/models/work_package/ask_before_destruction_spec.rb
  72. 31
      spec/models/work_package_spec.rb
  73. 11
      spec/permissions/delete_work_packages_spec.rb
  74. 6
      spec/permissions/work_packages_bulk_spec.rb
  75. 19
      spec/routing/work_package_bulk_spec.rb
  76. 70
      spec/views/api/v2/planning_element_statuses/_planning_element_status_api_rsb_spec.rb
  77. 104
      spec/views/api/v2/planning_element_statuses/index_api_rsb_spec.rb
  78. 74
      spec/views/api/v2/planning_element_statuses/show_api_rsb_spec.rb
  79. 4
      test/unit/lib/redmine/menu_manager/mapper_test.rb

@ -29,6 +29,9 @@
language: ruby language: ruby
rvm: rvm:
- 1.9.3 - 1.9.3
branches:
only:
- feature/rails3
env: env:
# mysql2 # mysql2
- "TEST_SUITE=cucumber RAILS_ENV=test DB=mysql2 BUNDLE_WITHOUT=rmagick:mysql:postgres:sqlite:development" - "TEST_SUITE=cucumber RAILS_ENV=test DB=mysql2 BUNDLE_WITHOUT=rmagick:mysql:postgres:sqlite:development"

@ -100,7 +100,7 @@ gem "i18n-js", :git => "https://github.com/fnando/i18n-js.git", :ref => '8801f8d
group :test do group :test do
gem 'shoulda' gem 'shoulda'
gem 'object-daddy', :git => 'https://github.com/awebneck/object_daddy.git' gem 'object-daddy', '~> 1.1.0'
gem 'mocha', '~> 0.13.1', :require => false gem 'mocha', '~> 0.13.1', :require => false
gem "launchy", "~> 2.3.0" gem "launchy", "~> 2.3.0"
gem "factory_girl_rails", "~> 4.0" gem "factory_girl_rails", "~> 4.0"

@ -1,9 +1,3 @@
GIT
remote: https://github.com/awebneck/object_daddy.git
revision: cf5abf001cdbd14b47a3b51421446717a3183f0a
specs:
object-daddy (1.1.0)
GIT GIT
remote: https://github.com/fnando/i18n-js.git remote: https://github.com/fnando/i18n-js.git
revision: 8801f8d17ef96c48a7a0269e251fcf1648c8f441 revision: 8801f8d17ef96c48a7a0269e251fcf1648c8f441
@ -193,6 +187,7 @@ GEM
mysql2 (0.3.11) mysql2 (0.3.11)
net-ldap (0.2.2) net-ldap (0.2.2)
nokogiri (1.5.9) nokogiri (1.5.9)
object-daddy (1.1.1)
oj (2.1.6) oj (2.1.6)
paper_trail (2.7.2) paper_trail (2.7.2)
activerecord (~> 3.0) activerecord (~> 3.0)
@ -391,7 +386,7 @@ DEPENDENCIES
mysql mysql
mysql2 (~> 0.3.11) mysql2 (~> 0.3.11)
net-ldap (~> 0.2.2) net-ldap (~> 0.2.2)
object-daddy! object-daddy (~> 1.1.0)
oj oj
pg pg
prototype-rails prototype-rails

@ -90,6 +90,27 @@
} }
}; };
var go_edit = function(){
edit_link = $('[accesskey=3]')[0];
if (edit_link !== undefined) {
edit_link.click();
}
};
var open_more_menu = function(){
more_menu = $('[accesskey=7]')[0];
if (more_menu !== undefined) {
more_menu.click();
}
};
var go_preview = function(){
preview_link = $('[accesskey=1]')[0];
if (preview_link !== undefined) {
preview_link.click();
}
};
var new_work_package = function(){ var new_work_package = function(){
if (we_are_in_project()) { if (we_are_in_project()) {
menu_sidebar().find('.new-work-package')[0].click(); menu_sidebar().find('.new-work-package')[0].click();
@ -104,20 +125,65 @@
$('#search_wrap .search_field').focus(); $('#search_wrap .search_field').focus();
}; };
var find_list_in_page = function(){
var dom_list, focus_elements;
dom_list = $($(document.activeElement).parents('table.list')[0] ||
$('table.list'));
if (dom_list.size() === 0) { return null; }
focus_elements = [];
dom_list.find('tbody tr').each(function(index, tr){
focus_elements.push($(tr).find('a')[0]);
});
return focus_elements;
};
Mousetrap.bind('?', function(){ show_help_modal(); return false; }); var focus_item_offset = function(offset){
var list, index;
list = find_list_in_page();
if (list === null) { return; }
index = list.indexOf($(document.activeElement).parents('table.list tr').find('a')[0]);
$(list[(index+offset+list.length) % list.length]).focus();
};
Mousetrap.bind('g o', function(){ go_overview(); return false; }); var focus_next_item = function(){
Mousetrap.bind('g m', function(){ go_my_page(); return false; }); focus_item_offset(1);
Mousetrap.bind('g w p', function(){ go_work_packages(); return false; }); };
Mousetrap.bind('g w i', function(){ go_wiki(); return false; });
Mousetrap.bind('g a', function(){ go_activity(); return false; }); var focus_previous_item = function(){
Mousetrap.bind('g c', function(){ go_calendar(); return false; }); focus_item_offset(-1);
Mousetrap.bind('g n', function(){ go_news(); return false; }); };
Mousetrap.bind('g t', function(){ go_timelines(); return false; });
Mousetrap.bind('n w p', function(){ new_work_package(); return false; });
Mousetrap.bind('s p', function(){ search_project(); return false; }); Mousetrap.bind('?', function(){ show_help_modal(); return false; });
Mousetrap.bind('s g', function(){ search_global(); return false; });
Mousetrap.bind('g o', function(){ go_overview(); return false; });
Mousetrap.bind('g m', function(){ go_my_page(); return false; });
Mousetrap.bind('g w p', function(){ go_work_packages(); return false; });
Mousetrap.bind('g w i', function(){ go_wiki(); return false; });
Mousetrap.bind('g a', function(){ go_activity(); return false; });
Mousetrap.bind('g c', function(){ go_calendar(); return false; });
Mousetrap.bind('g n', function(){ go_news(); return false; });
Mousetrap.bind('g t', function(){ go_timelines(); return false; });
Mousetrap.bind('g e', function(){ go_edit(); return false; });
Mousetrap.bind('g p', function(){ go_preview(); return false; });
Mousetrap.bind('n w p', function(){ new_work_package(); return false; });
Mousetrap.bind('j', function(){ focus_previous_item(); return false; });
Mousetrap.bind('k', function(){ focus_next_item(); return false; });
Mousetrap.bind('m', function(){ open_more_menu(); return false; });
Mousetrap.bind('s p', function(){ search_project(); return false; });
Mousetrap.bind('s g', function(){ search_global(); return false; });
})(jQuery); })(jQuery);
jQuery(function(){
// simulated hover effect on table lists when using the keyboard
var tables = jQuery('table.list');
if (tables.size() === 0) { return; }
tables.on('blur', 'tr *', function(){
jQuery(this).parents('table.list tr').removeClass('keyboard_hover');
});
tables.on('focus', 'tr *', function(){
jQuery(this).parents('table.list tr').addClass('keyboard_hover');
});
});

@ -61,6 +61,11 @@ var ModalHelper = (function() {
// close when body is clicked // close when body is clicked
body.on("click", ".ui-widget-overlay", jQuery.proxy(modalHelper.close, modalHelper)); body.on("click", ".ui-widget-overlay", jQuery.proxy(modalHelper.close, modalHelper));
body.on("keyup", function (e) {
if (e.which == 27) {
modalHelper.close();
}
});
ModalHelper._done = true; ModalHelper._done = true;
} else { } else {
@ -96,6 +101,12 @@ var ModalHelper = (function() {
this.hideLoadingModal(); this.hideLoadingModal();
this.loadingModal = false; this.loadingModal = false;
body.on("keyup", function (e) {
if (e.which == 27) {
modalHelper.close();
}
});
modalDiv.data('changed', false); modalDiv.data('changed', false);
var document_host = document.location.href.split("/")[2]; var document_host = document.location.href.split("/")[2];

@ -304,7 +304,7 @@ Timeline = {
timeline.reload(); timeline.reload();
}); });
timelineLoader = this.provideTimelineLoader() timelineLoader = this.provideTimelineLoader();
jQuery(timelineLoader).on('complete', jQuery.proxy(function(e, data) { jQuery(timelineLoader).on('complete', jQuery.proxy(function(e, data) {
jQuery.extend(this, data); jQuery.extend(this, data);
@ -329,7 +329,7 @@ Timeline = {
reload: function() { reload: function() {
delete this.lefthandTree; delete this.lefthandTree;
var timelineLoader = this.provideTimelineLoader() var timelineLoader = this.provideTimelineLoader();
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) { jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) {
@ -471,7 +471,7 @@ Timeline = {
FilterQueryStringBuilder.prototype.prepareAdditionalQueryData = function(key, value) { FilterQueryStringBuilder.prototype.prepareAdditionalQueryData = function(key, value) {
this.queryStringParts.push({name: key, value: value}); this.queryStringParts.push({name: key, value: value});
} };
FilterQueryStringBuilder.prototype.prepareFilterDataForKeyAndArrayOfValues = function(key, value) { FilterQueryStringBuilder.prototype.prepareFilterDataForKeyAndArrayOfValues = function(key, value) {
jQuery.each(value, jQuery.proxy( function(i, e) { jQuery.each(value, jQuery.proxy( function(i, e) {
@ -480,9 +480,11 @@ Timeline = {
}; };
FilterQueryStringBuilder.prototype.buildFilterDataForValue = function(key, value) { FilterQueryStringBuilder.prototype.buildFilterDataForValue = function(key, value) {
return value instanceof Array ? if (value instanceof Array) {
this.prepareFilterDataForKeyAndArrayOfValues(key, value) : this.prepareFilterDataForKeyAndArrayOfValues(key, value);
} else {
this.prepareFilterDataForKeyAndValue(key, value); this.prepareFilterDataForKeyAndValue(key, value);
}
}; };
FilterQueryStringBuilder.prototype.registerKeyAndValue = function(key, value) { FilterQueryStringBuilder.prototype.registerKeyAndValue = function(key, value) {
@ -766,7 +768,8 @@ Timeline = {
}; };
DataEnhancer.prototype.setElement = function (type, id, element) { DataEnhancer.prototype.setElement = function (type, id, element) {
return this.data[type.identifier][id] = element; this.data[type.identifier][id] = element;
return this.data[type.identifier][id];
}; };
DataEnhancer.prototype.getProject = function () { DataEnhancer.prototype.getProject = function () {
@ -976,7 +979,7 @@ Timeline = {
var pe = dataEnhancer.getElement(Timeline.PlanningElement, e.id); var pe = dataEnhancer.getElement(Timeline.PlanningElement, e.id);
var pet = pe.getPlanningElementType(); var pet = pe.getPlanningElementType();
pe.vertical = this.timeline.verticalPlanningElementIds().indexOf(pe.id) != -1; pe.vertical = this.timeline.verticalPlanningElementIds().indexOf(pe.id) !== -1;
//this.timeline.optionsfalse || Math.random() < 0.5 || (pet && pet.is_milestone); //this.timeline.optionsfalse || Math.random() < 0.5 || (pet && pet.is_milestone);
}); });
}; };
@ -1090,7 +1093,7 @@ Timeline = {
this.loader.register(Timeline.Reporting.identifier, this.loader.register(Timeline.Reporting.identifier,
{ url : url }); { url : url });
}, };
TimelineLoader.prototype.registerGlobalElements = function () { TimelineLoader.prototype.registerGlobalElements = function () {
@ -1194,10 +1197,11 @@ Timeline = {
); );
// load historical planning elements. // load historical planning elements.
// TODO: load historical PEs here!
if (this.options.target_time) { if (this.options.target_time) {
this.loader.register( this.loader.register(
Timeline.HistoricalPlanningElement.identifier + '_IDS_' + i, Timeline.HistoricalPlanningElement.identifier + '_IDS_' + i,
{ url : planningElementPrefix + { url : projectPrefix +
'/planning_elements.json?ids=' + '/planning_elements.json?ids=' +
planningElementIdsOfPacket.join(',') }, planningElementIdsOfPacket.join(',') },
{ storeIn: Timeline.HistoricalPlanningElement.identifier, { storeIn: Timeline.HistoricalPlanningElement.identifier,
@ -1662,7 +1666,7 @@ Timeline = {
}, },
hiddenForTimeFrame: function () { hiddenForTimeFrame: function () {
var types = this.timeline.options.planning_element_time_types; var types = this.timeline.options.planning_element_time_types;
if (!types) { if (!types || types.length === 0) {
return false; return false;
} }
@ -1803,15 +1807,15 @@ Timeline = {
var dataBGrouping = b.getFirstLevelGroupingData(); var dataBGrouping = b.getFirstLevelGroupingData();
// order first level grouping. // order first level grouping.
if (dataAGrouping.id != dataBGrouping.id) { if (parseInt(dataAGrouping.id, 10) !== parseInt(dataBGrouping.id, 10)) {
/** other is always at bottom */ /** other is always at bottom */
if (dataAGrouping.id == 0) { if (parseInt(dataAGrouping.id, 10) === 0) {
return 1; return 1;
} else if (dataBGrouping.id == 0) { } else if (parseInt(dataBGrouping.id, 10) === 0) {
return -1; return -1;
} }
if (timeline.options.grouping_one_sort == 1) { if (parseInt(timeline.options.grouping_one_sort, 10) === 1) {
ag = dataAGrouping.number; ag = dataAGrouping.number;
bg = dataBGrouping.number; bg = dataBGrouping.number;
} else { } else {
@ -1844,7 +1848,7 @@ Timeline = {
dc = -1; dc = -1;
} }
identifier_methods = [a, b].map(function(e) { return e.hasOwnProperty("subject") ? "subject" : "name" }) var identifier_methods = [a, b].map(function(e) { return e.hasOwnProperty("subject") ? "subject" : "name"; });
if (!a.identifierLower) { if (!a.identifierLower) {
a.identifierLower = a[identifier_methods[0]].toLowerCase(); a.identifierLower = a[identifier_methods[0]].toLowerCase();
@ -1862,7 +1866,7 @@ Timeline = {
} }
if (a.hasSecondLevelGroupingAdjustment && b.hasSecondLevelGroupingAdjustment) { if (a.hasSecondLevelGroupingAdjustment && b.hasSecondLevelGroupingAdjustment) {
if (timeline.options.grouping_two_sort == 1) { if (parseInt(timeline.options.grouping_two_sort, 10) === 1) {
if (dc !== 0) { if (dc !== 0) {
return dc; return dc;
} }
@ -1870,7 +1874,7 @@ Timeline = {
if (nc !== 0) { if (nc !== 0) {
return nc; return nc;
} }
} else if (timeline.options.grouping_two_sort == 2) { } else if (parseInt(timeline.options.grouping_two_sort, 10) === 2) {
if (nc !== 0) { if (nc !== 0) {
return nc; return nc;
} }
@ -1881,7 +1885,7 @@ Timeline = {
} }
} }
if (timeline.options.project_sort == 1 && a.is(Timeline.Project) && b.is(Timeline.Project)) { if (parseInt(timeline.options.project_sort, 10) === 1 && a.is(Timeline.Project) && b.is(Timeline.Project)) {
if (nc !== 0) { if (nc !== 0) {
return nc; return nc;
} }
@ -2888,6 +2892,8 @@ Timeline = {
var has_one_date = this.hasOneDate(); var has_one_date = this.hasOneDate();
var has_start_date = this.hasStartDate(); var has_start_date = this.hasStartDate();
var hoverElement;
color = this.getColor(); color = this.getColor();
if (has_one_date) { if (has_one_date) {
@ -2904,7 +2910,7 @@ Timeline = {
'stroke-dasharray': '- ' 'stroke-dasharray': '- '
}); });
var hoverElement = paper.rect( hoverElement = paper.rect(
left + scale.day / 2 - 2 * Timeline.HOVER_THRESHOLD, left + scale.day / 2 - 2 * Timeline.HOVER_THRESHOLD,
timeline.decoHeight(), // 8px margin-top timeline.decoHeight(), // 8px margin-top
4 * Timeline.HOVER_THRESHOLD, 4 * Timeline.HOVER_THRESHOLD,
@ -2927,7 +2933,7 @@ Timeline = {
'opacity': 0.2 'opacity': 0.2
}); });
var hoverElement = paper.rect( hoverElement = paper.rect(
left - Timeline.HOVER_THRESHOLD, left - Timeline.HOVER_THRESHOLD,
timeline.decoHeight(), // 8px margin-top timeline.decoHeight(), // 8px margin-top
width + 2 * Timeline.HOVER_THRESHOLD, width + 2 * Timeline.HOVER_THRESHOLD,
@ -3185,7 +3191,8 @@ Timeline = {
} else { } else {
this.childNodes.push(node); this.childNodes.push(node);
} }
return node.parentNode = this; node.parentNode = this;
return node.parentNode;
}, },
removeChild: function(node) { removeChild: function(node) {
var result; var result;
@ -3213,7 +3220,8 @@ Timeline = {
return this.expanded; return this.expanded;
}, },
setExpand: function(state) { setExpand: function(state) {
return this.expanded = state; this.expanded = state;
return this.expanded;
}, },
expand: function() { expand: function() {
return this.setExpand(true); return this.setExpand(true);
@ -4671,7 +4679,7 @@ Timeline = {
getRelativeVerticalBottomOffset: function(offset) { getRelativeVerticalBottomOffset: function(offset) {
var result; var result;
result = this.getRelativeVerticalOffset(offset); result = this.getRelativeVerticalOffset(offset);
if (offset.find("div").length == 1) { if (offset.find("div").length === 1) {
result -= jQuery(offset.find("div")[0]).height(); result -= jQuery(offset.find("div")[0]).height();
} }
if (offset !== undefined) if (offset !== undefined)

@ -55,7 +55,7 @@ jQuery(document).ready(function($) {
$("#timeline_options_planning_element_responsibles"), $("#timeline_options_planning_element_responsibles"),
$("#timeline_options_grouping_two_selection") $("#timeline_options_grouping_two_selection")
].each(function (item) { ].each(function (item) {
$(item).timelinesAutocomplete({ ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} }); $(item).timelinesAutocomplete({ ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.noneElement")}} });
}); });
[ [
@ -104,7 +104,7 @@ jQuery(document).ready(function($) {
query : OpenProject.Helpers.Search.projectQueryWithHierarchy( query : OpenProject.Helpers.Search.projectQueryWithHierarchy(
jQuery.proxy(openProject, 'fetchProjects'), jQuery.proxy(openProject, 'fetchProjects'),
20), 20),
ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.noneElement")}}
}); });
}); });
@ -121,7 +121,7 @@ jQuery(document).ready(function($) {
query : OpenProject.Helpers.Search.projectQueryWithHierarchy( query : OpenProject.Helpers.Search.projectQueryWithHierarchy(
jQuery.proxy(openProject, 'fetchProjects'), jQuery.proxy(openProject, 'fetchProjects'),
20), 20),
ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.noneElement")}}
}); });
}); });

@ -270,8 +270,8 @@ tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; displa
tr.group:hover a.toggle-all { display: inline;} tr.group:hover a.toggle-all { display: inline;}
a.toggle-all:hover {text-decoration: none;} a.toggle-all:hover {text-decoration: none;}
table.list tbody tr:hover { background-color: #ffffdd; } table.list tbody tr:hover, table.list tr.keyboard_hover { background-color: #ffff99 !important; }
table.list tbody tr.group:hover { background-color: inherit; } table.list tbody tr.group:hover, table.list tr.group.keyboard_hover { background-color: inherit !important; }
table td {padding: 2px;} table td {padding: 2px;}
table p {margin: 0;} table p {margin: 0;}
@ -2407,6 +2407,10 @@ div#sidebar > h3:first-child {
float: left; float: left;
width: 170px; width: 170px;
} }
.box li.decorated {
margin-left: 20px;
list-style: disc outside none;
}
fieldset#columns table { fieldset#columns table {
width: auto; width: auto;
} }
@ -3461,3 +3465,7 @@ h4.comment {
h4.comment img { h4.comment img {
margin-right: 3px; margin-right: 3px;
} }
.bold {
font-weight: bold;
}

@ -287,6 +287,14 @@ module Api
return false return false
end end
def find_issues
@issues = WorkPackage.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
end end
end end
end end

@ -1,57 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
module Api
module V2
class PlanningElementStatusesController < PlanningElementStatusesController
unloadable
helper :timelines
include ::Api::V2::ApiController
accept_key_auth :index, :show
def index
@planning_element_statuses = PlanningElementStatus.active
respond_to do |format|
format.api
end
end
def show
@planning_element_status = PlanningElementStatus.active.find(params[:id])
respond_to do |format|
format.api
end
end
end
end
end

@ -117,46 +117,66 @@ module Api
protected protected
def filter_authorized_projects
# authorize
# Ignoring projects, where user has no view_work_packages permission.
permission = params[:controller].sub api_version, ''
@projects = @projects.select do |project|
User.current.allowed_to?({:controller => permission,
:action => params[:action]},
project)
end
end
def load_multiple_projects(ids, identifiers)
@projects = []
@projects |= Project.all(:conditions => {:id => ids}) unless ids.empty?
@projects |= Project.all(:conditions => {:identifier => identifiers}) unless identifiers.empty?
end
def projects_contain_certain_ids_and_identifiers(ids, identifiers)
(@projects.map(&:id) & ids).size == ids.size &&
(@projects.map(&:identifier) & identifiers).size == identifiers.size
end
def find_single_project
find_project_by_project_id unless performed?
authorize unless performed?
assign_planning_elements(@project) unless performed?
end
def find_multiple_projects
# find_project_by_project_id
ids, identifiers = params[:project_id].split(/,/).map(&:strip).partition { |s| s =~ /^\d*$/ }
ids = ids.map(&:to_i).sort
identifiers = identifiers.sort
load_multiple_projects(ids, identifiers)
if !projects_contain_certain_ids_and_identifiers(ids, identifiers)
# => not all projects could be found
render_404
return
end
filter_authorized_projects
if @projects.blank?
@planning_elements = []
return
end
assign_planning_elements(@projects)
end
# Filters # Filters
def find_all_projects_by_project_id def find_all_projects_by_project_id
if !params[:project_id] and params[:ids] then if !params[:project_id] and params[:ids] then
@planning_elements = WorkPackage.visible(User.current).find_all_by_id(params[:ids]) @planning_elements = WorkPackage.visible(User.current).find_all_by_id(params[:ids])
elsif params[:project_id] !~ /,/ elsif params[:project_id] !~ /,/
find_project_by_project_id unless performed? find_single_project
authorize unless performed?
assign_planning_elements(@project) unless performed?
else else
# find_project_by_project_id find_multiple_projects
ids, identifiers = params[:project_id].split(/,/).map(&:strip).partition { |s| s =~ /^\d*$/ }
ids = ids.map(&:to_i).sort
identifiers = identifiers.sort
@projects = []
@projects |= Project.all(:conditions => {:id => ids}) unless ids.empty?
@projects |= Project.all(:conditions => {:identifier => identifiers}) unless identifiers.empty?
if (@projects.map(&:id) & ids).size != ids.size ||
(@projects.map(&:identifier) & identifiers).size != identifiers.size
# => not all projects could be found
render_404
return
end
# authorize
# Ignoring projects, where user has no view_work_packages permission.
permission = params[:controller].sub api_version, ''
@projects = @projects.select do |project|
User.current.allowed_to?({:controller => permission,
:action => params[:action]},
project)
end
if @projects.blank?
@planning_elements = []
return
end
assign_planning_elements(@projects)
end end
end end

@ -380,16 +380,6 @@ class ApplicationController < ActionController::Base
render_404 render_404
end end
# TODO: remove this once all subclasses use find_work_packages
def find_issues
@issues = WorkPackage.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
# Check if project is unique before bulk operations # Check if project is unique before bulk operations
def check_project_uniqueness def check_project_uniqueness
unless @project unless @project

@ -1,50 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
class PlanningElementStatusesController < ApplicationController
unloadable
helper :timelines
before_filter :disable_api
accept_key_auth :index, :show
def index
@planning_element_statuses = PlanningElementStatus.active
respond_to do |format|
format.html { render_404 }
end
end
def show
@planning_element_status = PlanningElementStatus.active.find(params[:id])
respond_to do |format|
format.html { render_404 }
end
end
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class WorkPackageBulkController < ApplicationController class WorkPackages::BulkController < ApplicationController
before_filter :disable_api before_filter :disable_api
before_filter :find_work_packages, only: [:edit, :update] before_filter :find_work_packages
before_filter :authorize before_filter :authorize
include JournalsHelper include JournalsHelper
@ -66,8 +66,38 @@ class WorkPackageBulkController < ApplicationController
redirect_back_or_default({controller: '/work_packages', action: :index, project_id: @project}) redirect_back_or_default({controller: '/work_packages', action: :index, project_id: @project})
end end
def destroy
unless WorkPackage.cleanup_associated_before_destructing_if_required(@work_packages, current_user, params[:to_do])
respond_to do |format|
format.html { render :locals => { work_packages: @work_packages,
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
}
end
else
destroy_work_packages(@work_packages)
respond_to do |format|
format.html { redirect_back_or_default(project_work_packages_path(@work_packages.first.project)) }
end
end
end
private private
def destroy_work_packages(work_packages)
work_packages.each do |work_package|
begin
work_package.reload.destroy
rescue ::ActiveRecord::RecordNotFound
# raised by #reload if work package no longer exists
# nothing to do, work package was already deleted (eg. by a parent)
end
end
end
def parse_params_for_bulk_work_package_attributes(params) def parse_params_for_bulk_work_package_attributes(params)
attributes = (params[:work_package] || {}).reject {|k,v| v.blank?} attributes = (params[:work_package] || {}).reject {|k,v| v.blank?}
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
@ -90,4 +120,8 @@ private
:ids => '#' + unsaved_work_package_ids.join(', #')) :ids => '#' + unsaved_work_package_ids.join(', #'))
end end
end end
def default_breadcrumb
l(:label_work_package_plural)
end
end end

@ -38,9 +38,8 @@ class WorkPackagesController < ApplicationController
include SortHelper include SortHelper
include PaginationHelper include PaginationHelper
accept_key_auth :index, :show, :create, :update, :destroy accept_key_auth :index, :show, :create, :update
before_filter :find_work_packages, :only => [:destroy]
before_filter :disable_api before_filter :disable_api
before_filter :not_found_unless_work_package, before_filter :not_found_unless_work_package,
:project, :project,
@ -182,39 +181,6 @@ class WorkPackagesController < ApplicationController
end end
end end
def destroy
@hours = TimeEntry.sum(:hours, :conditions => ['work_package_id IN (?)', work_package]).to_f
if @hours > 0
case params[:todo]
when 'destroy'
# nothing to do
when 'nullify'
TimeEntry.update_all('work_package_id = NULL', ['work_package_id IN (?)', work_package])
when 'reassign'
reassign_to = @project.work_packages.find_by_id(params[:reassign_to_id])
if reassign_to.nil?
flash.now[:error] = l(:error_work_package_not_found_in_project)
return
else
TimeEntry.update_all("work_package_id = #{reassign_to.id}", ['work_package_id IN (?)', work_package])
end
else
# display the destroy form if it's a user request
return unless api_request?
end
end
begin
work_package.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if work package no longer exists
# nothing to do, work package was already deleted (eg. by a parent)
end
respond_to do |format|
format.html { redirect_back_or_default(controller: '/work_packages', action: 'index', project_id: @project) }
end
end
def index def index
query = retrieve_query query = retrieve_query

@ -100,7 +100,8 @@ module ApplicationHelper
link_to l(:label_preview), link_to l(:label_preview),
url, url,
:id => id, :id => id,
:class => 'preview' :class => 'preview',
:accesskey => accesskey(:preview)
end end
@ -265,7 +266,7 @@ module ApplicationHelper
def join_flash_messages(messages) def join_flash_messages(messages)
if messages.respond_to?(:join) if messages.respond_to?(:join)
messages.join('<br />').html_safe messages.join('<br />').html_safe
else else
messages messages
end end
@ -477,7 +478,7 @@ module ApplicationHelper
end end
def accesskey(s) def accesskey(s)
Redmine::AccessKeys.key_for s OpenProject::AccessKeys.key_for s
end end
# Formats text according to system settings. # Formats text according to system settings.
@ -953,12 +954,12 @@ module ApplicationHelper
ret += content_tag :ul do ret += content_tag :ul do
args[:collection].collect do |(s, name)| args[:collection].collect do |(s, name)|
content_tag :li do content_tag :li do
context_menu_link (name || s), work_package_bulk_update_path(:ids => args[:updated_object_ids], context_menu_link (name || s), work_packages_bulk_path(:ids => args[:updated_object_ids],
:work_package => { db_attribute => s }, :work_package => { db_attribute => s },
:back_url => args[:back_url]), :back_url => args[:back_url]),
:method => :put, :method => :put,
:selected => args[:selected].call(s), :selected => args[:selected].call(s),
:disabled => args[:disabled].call(s) :disabled => args[:disabled].call(s)
end end
end.join.html_safe end.join.html_safe
end end
@ -1075,8 +1076,8 @@ module ApplicationHelper
# #
def footer_content def footer_content
elements = [] elements = []
elements << I18n.t(:text_powered_by, :link => link_to(Redmine::Info.app_name, elements << I18n.t(:text_powered_by, :link => link_to(OpenProject::Info.app_name,
Redmine::Info.url)) OpenProject::Info.url))
unless OpenProject::Footer.content.nil? unless OpenProject::Footer.content.nil?
OpenProject::Footer.content.each do |name, value| OpenProject::Footer.content.each do |name, value|
content = value.respond_to?(:call) ? value.call : value content = value.respond_to?(:call) ? value.call : value

@ -260,7 +260,7 @@ module TimelinesHelper
end end
def none_option def none_option
result = [[l('timelines.filter.none'), -1]] result = [[l('timelines.filter.noneSelection'), -1]]
end end
def filter_select_i18n_array_with_index_and_none(array, i18n_prefix) def filter_select_i18n_array_with_index_and_none(array, i18n_prefix)

@ -97,7 +97,7 @@ module WorkPackagesHelper
parts[:link] << h(package.kind.to_s) if options[:type] parts[:link] << h(package.kind.to_s) if options[:type]
parts[:link] << "##{package.id}" if options[:id] parts[:link] << "##{h(package.id)}" if options[:id]
# Hidden link part # Hidden link part
@ -119,7 +119,7 @@ module WorkPackagesHelper
subject subject
end end
parts[:suffix] << subject parts[:suffix] << h(subject)
end end
# title part # title part
@ -577,4 +577,20 @@ module WorkPackagesHelper
WorkPackageAttribute.new(:"work_package_#{value.id}", field) WorkPackageAttribute.new(:"work_package_#{value.id}", field)
end end
end end
def work_package_associations_to_address(associated)
ret = "".html_safe
ret += content_tag(:p, l(:text_destroy_with_associated), :class => "bold" )
ret += content_tag(:ul) do
associated.inject("".html_safe) do |list, associated_class|
list += content_tag(:li, associated_class.model_name.human, :class => "decorated")
list
end
end
ret
end
end end

@ -177,7 +177,7 @@ end
# Force load the subclasses in development mode # Force load the subclasses in development mode
['time_entry_activity', 'issue_priority', ['time_entry_activity', 'issue_priority',
'reported_project_status', 'planning_element_status'].each do |enum_subclass| 'reported_project_status'].each do |enum_subclass|
require_dependency enum_subclass require_dependency enum_subclass
end end

@ -82,7 +82,7 @@ class Query < ActiveRecord::Base
@@available_columns = [ @@available_columns = [
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
QueryColumn.new(:type, :sortable => "#{Type.table_name}.position", :groupable => true), QueryColumn.new(:type, :sortable => "#{Type.table_name}.position", :groupable => true),
QueryColumn.new(:parent, :sortable => ["#{WorkPackage.table_name}.root_id", "#{WorkPackage.table_name}.lft ASC"], :default_order => 'desc', :caption => :parent_issue), QueryColumn.new(:parent, :sortable => ["#{WorkPackage.table_name}.root_id", "#{WorkPackage.table_name}.lft ASC"], :default_order => 'desc'),
QueryColumn.new(:status, :sortable => "#{Status.table_name}.position", :groupable => true), QueryColumn.new(:status, :sortable => "#{Status.table_name}.position", :groupable => true),
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
QueryColumn.new(:subject, :sortable => "#{WorkPackage.table_name}.subject"), QueryColumn.new(:subject, :sortable => "#{WorkPackage.table_name}.subject"),

@ -57,6 +57,8 @@ class TimeEntry < ActiveRecord::Base
:conditions => Project.allowed_to_condition(args.first || User.current, :view_time_entries) :conditions => Project.allowed_to_condition(args.first || User.current, :view_time_entries)
}} }}
scope :on_work_packages, ->(work_packages) { where(work_package_id: work_packages) }
after_initialize :set_default_activity after_initialize :set_default_activity
before_validation :set_default_project before_validation :set_default_project

@ -35,7 +35,7 @@ class Timeline < ActiveRecord::Base
end end
def name def name
@name ||= ::I18n.t('timelines.filter.none') @name ||= ::I18n.t('timelines.filter.noneElement')
end end
end end

@ -35,6 +35,8 @@ class WorkPackage < ActiveRecord::Base
include WorkPackage::Validations include WorkPackage::Validations
include WorkPackage::SchedulingRules include WorkPackage::SchedulingRules
include WorkPackage::StatusTransitions include WorkPackage::StatusTransitions
include WorkPackage::AskBeforeDestruction
include WorkPackage::TimeEntries
include OpenProject::Journal::AttachmentHelper include OpenProject::Journal::AttachmentHelper
@ -145,6 +147,10 @@ class WorkPackage < ActiveRecord::Base
after_validation :set_attachments_error_details, if: lambda {|work_package| work_package.errors.messages.has_key? :attachments} after_validation :set_attachments_error_details, if: lambda {|work_package| work_package.errors.messages.has_key? :attachments}
associated_to_ask_before_destruction TimeEntry,
->(work_packages) { TimeEntry.on_work_packages(work_packages).count > 0 },
self.method(:cleanup_time_entries_before_destruction_of)
# Mapping attributes, that are passed in as id's onto their respective associations # Mapping attributes, that are passed in as id's onto their respective associations
# (eg. type=4711 onto type=Type.find(4711)) # (eg. type=4711 onto type=Type.find(4711))
include AssociationsMapper include AssociationsMapper

@ -0,0 +1,97 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
module WorkPackage::AskBeforeDestruction
extend ActiveSupport::Concern
DestructionRegistration = Struct.new(:klass, :check, :action)
def self.included(base)
base.extend(ClassMethods)
base.class_attribute :registered_associated_to_ask_before_destruction
end
module ClassMethods
def cleanup_action_required_before_destructing?(work_packages)
!associated_to_ask_before_destruction_of(work_packages).empty?
end
def cleanup_associated_before_destructing_if_required(work_packages, user, to_do = { :action => 'destroy' })
cleanup_required = cleanup_action_required_before_destructing?(work_packages)
(!cleanup_required ||
(cleanup_required &&
cleanup_each_associated_class(work_packages, user, to_do)))
end
def associated_classes_to_address_before_destruction_of(work_packages)
associated = []
registered_associated_to_ask_before_destruction.each do |registration|
associated << registration.klass if registration.check.call(work_packages)
end
associated
end
private
def associated_to_ask_before_destruction_of(work_packages)
associated = {}
registered_associated_to_ask_before_destruction.each do |registration|
associated[registration.klass] = registration.action if registration.check.call(work_packages)
end
associated
end
def associated_to_ask_before_destruction(klass, check, action)
self.registered_associated_to_ask_before_destruction ||= []
registration = DestructionRegistration.new(klass, check, action)
self.registered_associated_to_ask_before_destruction << registration
end
def cleanup_each_associated_class(work_packages, user, to_do)
ret = false
self.transaction do
associated_to_ask_before_destruction_of(work_packages).each do |klass, method|
ret = method.call(work_packages, user, to_do)
end
raise ActiveRecord::Rollback unless ret
end
ret
end
end
end

@ -340,7 +340,7 @@ module WorkPackage::PdfExporter
set_language_if_valid lang set_language_if_valid lang
@font_for_content = 'FreeSans' @font_for_content = 'FreeSans'
@font_for_footer = 'FreeSans' @font_for_footer = 'FreeSans'
SetCreator(Redmine::Info.app_name) SetCreator(OpenProject::Info.app_name)
SetFont(@font_for_content) SetFont(@font_for_content)
end end
@ -394,7 +394,7 @@ module WorkPackage::PdfExporter
@font_for_content = 'Arial' @font_for_content = 'Arial'
@font_for_footer = 'Helvetica' @font_for_footer = 'Helvetica'
SetCreator(Redmine::Info.app_name) SetCreator(OpenProject::Info.app_name)
SetFont(@font_for_content) SetFont(@font_for_content)
end end

@ -1,3 +1,4 @@
#-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF) # Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
@ -25,24 +26,47 @@
# #
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
module WorkPackage::TimeEntries
extend ActiveSupport::Concern
class PlanningElementStatus < Enumeration def self.included(base)
unloadable base.extend(ClassMethods)
end
has_many :planning_elements, :class_name => "PlanningElement", module ClassMethods
:foreign_key => 'planning_element_status_id'
OptionName = :enumeration_planning_element_statuses protected
def option_name def cleanup_time_entries_before_destruction_of(work_packages, user, to_do = { :action => 'destroy'} )
OptionName return false unless to_do.present?
end
def objects_count case to_do[:action]
planning_elements.count when 'destroy'
end true
# nothing to do
when 'nullify'
WorkPackage.update_time_entries(work_packages, 'work_package_id = NULL')
when 'reassign'
reassign_to = WorkPackage.includes(:project)
.where(Project.allowed_to_condition(user, :edit_time_entries))
.find_by_id(to_do[:reassign_to_id])
if reassign_to.nil?
Array(work_packages).each do |wp|
wp.errors.add(:base, :is_not_a_valid_target_for_time_entries, id: to_do[:reassign_to_id])
end
false
else
WorkPackage.update_time_entries(work_packages, "work_package_id = #{reassign_to.id}, project_id = #{reassign_to.project_id}")
end
else
false
end
end
def transfer_relations(to) def update_time_entries(work_packages, action)
planning_elements.update_all(:planning_element_status_id => to.id) TimeEntry.update_all(action, ['work_package_id IN (?)', work_packages])
end
end end
end end

@ -31,7 +31,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= call_hook(:view_admin_info_top) %> <%= call_hook(:view_admin_info_top) %>
<%= content_tag :h3, I18n.t('label_core_version') %> <%= content_tag :h3, I18n.t('label_core_version') %>
<p><strong><%= Redmine::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p> <p><strong><%= OpenProject::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p>
<%= content_tag :h3, I18n.t('label_system') %> <%= content_tag :h3, I18n.t('label_system') %>
<table class="list"> <table class="list">

@ -38,7 +38,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.id url_for(:controller => '/welcome', :only_path => false) xml.id url_for(:controller => '/welcome', :only_path => false)
xml.updated(updated_time.xmlschema) xml.updated(updated_time.xmlschema)
xml.author { xml.name "#{Setting.app_title}" } xml.author { xml.name "#{Setting.app_title}" }
xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; } xml.generator(:uri => OpenProject::Info.url) { xml.text! OpenProject::Info.app_name; }
@items.each do |item| @items.each do |item|
item_event = (not first_item.nil? and first_item.respond_to?(:data)) ? item.data : item item_event = (not first_item.nil? and first_item.respond_to?(:data)) ? item.data : item

@ -50,7 +50,7 @@ See doc/COPYRIGHT.rdoc for more details.
<td style="width:15%;"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :method => :put) %></td> <td style="width:15%;"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :method => :put) %></td>
<td class="buttons"> <td class="buttons">
<%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration },
:method => :post, :method => :delete,
:confirm => l(:text_are_you_sure), :confirm => l(:text_are_you_sure),
:class => 'icon icon-del' %> :class => 'icon icon-del' %>
</td> </td>

@ -67,3 +67,11 @@ See doc/COPYRIGHT.rdoc for more details.
<li><kbd>g</kbd> <kbd>t</kbd><%= l(:label_keyboard_shortcut_go_timelines) %></li> <li><kbd>g</kbd> <kbd>t</kbd><%= l(:label_keyboard_shortcut_go_timelines) %></li>
<li><kbd>n</kbd> <kbd>w</kbd> <kbd>p</kbd><%= l(:label_keyboard_shortcut_new_work_package) %></li> <li><kbd>n</kbd> <kbd>w</kbd> <kbd>p</kbd><%= l(:label_keyboard_shortcut_new_work_package) %></li>
</ul> </ul>
<h2><%= l(:label_keyboard_shortcut_some_pages_only) %></h2>
<ul>
<li><kbd>g</kbd> <kbd>e</kbd><%= l(:label_keyboard_shortcut_go_edit) %></li>
<li><kbd>m</kbd><%= l(:label_keyboard_shortcut_open_more_menu) %></li>
<li><kbd>g</kbd> <kbd>p</kbd><%= l(:label_keyboard_shortcut_go_preview) %></li>
<li><kbd>k</kbd><%= l(:label_keyboard_shortcut_focus_next_item) %></li>
<li><kbd>j</kbd><%= l(:label_keyboard_shortcut_focus_previous_item) %></li>
</ul>

@ -32,7 +32,10 @@ See doc/COPYRIGHT.rdoc for more details.
<%= content_for :action_menu_main %> <%= content_for :action_menu_main %>
<% if content_for?(:action_menu_more) %> <% if content_for?(:action_menu_more) %>
<li class="drop-down"> <li class="drop-down">
<a href="javascript:" class="icon icon-more"><%= l(:more_actions) %></a> <a href="javascript:" class="icon icon-more"
accesskey="<%= OpenProject::AccessKeys.key_for(:more_menu) %>">
<%= l(:more_actions) %>
</a>
<ul class="action_menu_more" style="display:none;"> <ul class="action_menu_more" style="display:none;">
<%= content_for :action_menu_more %> <%= content_for :action_menu_more %>
</ul> </ul>

@ -30,7 +30,7 @@ See doc/COPYRIGHT.rdoc for more details.
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title><%= html_title %></title> <title><%= html_title %></title>
<meta name="description" content="<%= Redmine::Info.app_name %>" /> <meta name="description" content="<%= OpenProject::Info.app_name %>" />
<meta name="keywords" content="issue,bug,type" /> <meta name="keywords" content="issue,bug,type" />
<meta name="current_menu_item" content="<%= current_menu_item %>" /> <meta name="current_menu_item" content="<%= current_menu_item %>" />
<%= csrf_meta_tags %> <%= csrf_meta_tags %>

@ -42,8 +42,10 @@ See doc/COPYRIGHT.rdoc for more details.
<p><label for="status_is_closed"><%=Status.human_attribute_name(:is_closed) %></label> <p><label for="status_is_closed"><%=Status.human_attribute_name(:is_closed) %></label>
<%= check_box 'status', 'is_closed' %></p> <%= check_box 'status', 'is_closed' %></p>
<p><label for="status_is_default"><%= Status.human_attribute_name(:is_default) %></label> <% unless @status.is_default? %>
<%= check_box 'status', 'is_default' %></p> <p><label for="status_is_default"><%= Status.human_attribute_name(:is_default) %></label>
<%= check_box 'status', 'is_default' %></p>
<% end %>
<%= call_hook(:view_statuses_form, :status => @status) %> <%= call_hook(:view_statuses_form, :status => @status) %>

@ -92,32 +92,6 @@ See doc/COPYRIGHT.rdoc for more details.
:date => absolute_text + absolute_calendar).html_safe %> :date => absolute_text + absolute_calendar).html_safe %>
</p> </p>
<p class="tl-form-overflow">
<%= label_tag 'timeline_options_comparison_historical',
l('timelines.filter.comparison.historical') %>
<%= radio_button("timeline[options]",
:comparison,
'historical',
:checked => (timeline.comparison == 'historical')) %>
<% historical_text1 = text_field_tag "timeline[options][compare_to_historical_one]",
timeline.options["compare_to_historical_one"],
:size => 10
historical_calendar1 = calendar_for('timeline_options_compare_to_historical_one')
historical_text2 = text_field_tag "timeline[options][compare_to_historical_two]",
timeline.options["compare_to_historical_two"],
:size => 10
historical_calendar2 = calendar_for('timeline_options_compare_to_historical_two')
%>
<%= l('timelines.filter.comparison.compare_historical',
:startdate => historical_text1 + historical_calendar1 ,
:enddate => historical_text2 + historical_calendar2).html_safe %>
</p>
</div> </div>
</div> </div>
</fieldset> </fieldset>

@ -49,6 +49,7 @@ See doc/COPYRIGHT.rdoc for more details.
'timelines.errors.not_implemented', 'timelines.errors.not_implemented',
'timelines.errors.report_comparison', 'timelines.errors.report_comparison',
'timelines.empty', 'timelines.empty',
'timelines.filter.noneSelection',
'timelines.filter.column.due_date', 'timelines.filter.column.due_date',
'timelines.filter.column.name', 'timelines.filter.column.name',
'timelines.filter.column.type', 'timelines.filter.column.type',

@ -46,10 +46,9 @@ See doc/COPYRIGHT.rdoc for more details.
<%= li_unless_nil(link_to_if_authorized l(:button_move), {:controller => '/work_packages/moves', :action => 'new', :work_package_id => controller.work_package}, :class => 'icon icon-move') %> <%= li_unless_nil(link_to_if_authorized l(:button_move), {:controller => '/work_packages/moves', :action => 'new', :work_package_id => controller.work_package}, :class => 'icon icon-move') %>
<%= li_unless_nil(link_to_if_authorized l(:button_delete), <%= li_unless_nil(link_to_if_authorized l(:button_delete),
{ :controller => '/work_packages', { :controller => '/work_packages/bulk',
:action => 'destroy', :action => 'destroy',
:id => controller.work_package, :ids => [work_package.id] },
:todo => 'destroy' },
:confirm => (controller.work_package.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :confirm => (controller.work_package.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)),
:method => :delete , :method => :delete ,
:class => 'icon icon-del' ) %> :class => 'icon icon-del' ) %>

@ -0,0 +1,45 @@
<h2><%= l(:label_confirmation) %></h2>
<%= error_messages_for work_packages.first %>
<%= form_tag work_packages_bulk_path, :method => :delete do %>
<% work_packages.each do |work_package| %>
<%= hidden_field_tag 'ids[]', work_package.id %>
<% end %>
<div class="box">
<%= work_package_associations_to_address(associated) %>
<p class="bold">
<%= l(:text_destroy_what_to_do) %>
</p>
<%= fields_for :to_do do |f| %>
<p>
<%= f.radio_button 'action', 'destroy' %>
<%= f.label 'action_destroy', l(:text_destroy) %>
</p>
<p>
<%= f.radio_button 'action' , 'nullify' %>
<%= f.label 'action_nullify', l(:text_assign_to_project) %>
</p>
<p>
<%= f.radio_button 'action', 'reassign', :onclick => 'if(jQuery("#to_do_action_reassign").prop("checked")) { jQuery("#to_do_reassign_to_id").focus(); }' %>
<%= f.label 'action_reassign', l(:text_reassign) %>
<%= f.label 'reassign_to_id', l(:text_reassign_to) %>
<%= f.text_field 'reassign_to_id', :value => params[:reassign_to_id], :size => 6, :onfocus => 'jQuery("#to_do_action_reassign").prop("checked", true);' %>
</p>
<% end %>
</div>
<%= submit_tag l(:button_apply) %>
<% end %>

@ -89,7 +89,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="splitcontentright"> <div class="splitcontentright">
<% if @project && User.current.allowed_to?(:manage_subtasks, @project) %> <% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
<p> <p>
<label for='work_package_parent_id'><%= WorkPackage.human_attribute_name(:parent_work_package) %></label> <label for='work_package_parent_id'><%= WorkPackage.human_attribute_name(:parent) %></label>
<%= text_field_tag 'work_package[parent_id]', '', :size => 10 %> <%= text_field_tag 'work_package[parent_id]', '', :size => 10 %>
</p> </p>
<div id="parent_work_package_candidates" class="autocomplete"></div> <div id="parent_work_package_candidates" class="autocomplete"></div>

@ -38,7 +38,7 @@ See doc/COPYRIGHT.rdoc for more details.
</li> </li>
<% else %> <% else %>
<li class="edit"> <li class="edit">
<%= context_menu_link l(:button_edit), work_package_bulk_edit_path(:ids => @work_packages.collect(&:id)), <%= context_menu_link l(:button_edit), edit_work_packages_bulk_path(:ids => @work_packages.collect(&:id)),
:class => 'icon-edit', :class => 'icon-edit',
:disabled => !@can[:edit] %> :disabled => !@can[:edit] %>
</li> </li>
@ -151,7 +151,7 @@ See doc/COPYRIGHT.rdoc for more details.
</li> </li>
<li class="delete"> <li class="delete">
<%= context_menu_link l(:button_delete), work_packages_path(:ids => @work_packages.collect(&:id)), <%= context_menu_link l(:button_delete), work_packages_bulk_path(:ids => @work_packages.collect(&:id)),
:method => :delete, :method => :delete,
:confirm => l(:text_work_packages_destroy_confirmation), :confirm => l(:text_work_packages_destroy_confirmation),
:class => 'context_item', :class => 'context_item',

@ -80,12 +80,6 @@ de:
value: "Wert" value: "Wert"
enumeration: enumeration:
active: "Aktiv" active: "Aktiv"
issue:
parent_issue: "Übergeordnete Aufgabe"
subproject: "Unterprojekt von"
time_entries: "Logzeit"
type: "Typ"
watcher: "Beobachter"
relation: relation:
delay: "Pufferzeit" delay: "Pufferzeit"
from: "Arbeitspaket" from: "Arbeitspaket"
@ -171,6 +165,11 @@ de:
progress: "% erledigt" progress: "% erledigt"
responsible: "Planungsverantwortlicher" responsible: "Planungsverantwortlicher"
spent_time: "Aufgewendete Zeit" spent_time: "Aufgewendete Zeit"
parent_issue: "Übergeordnete Aufgabe"
subproject: "Unterprojekt von"
time_entries: "Logzeit"
type: "Typ"
watcher: "Beobachter"
errors: errors:
messages: messages:
accepted: "muss akzeptiert werden" accepted: "muss akzeptiert werden"
@ -206,6 +205,7 @@ de:
before_or_equal_to: "muss vor oder gleich %{date} sein" before_or_equal_to: "muss vor oder gleich %{date} sein"
models: models:
work_package: work_package:
is_not_a_valid_target_for_time_entries: "Arbeitspaket #%{id} ist kein gültiges Ziel für die Zuordnug der Zeiterfassungseinträge."
attributes: attributes:
due_date: due_date:
not_start_date: "ist nicht am Startdatum, obwohl die bei Meilensteinen Pflicht ist" not_start_date: "ist nicht am Startdatum, obwohl die bei Meilensteinen Pflicht ist"
@ -481,7 +481,6 @@ de:
enumeration_activities: "Aktivitäten (Zeiterfassung)" enumeration_activities: "Aktivitäten (Zeiterfassung)"
enumeration_work_package_priorities: "Arbeitspaket-Prioritäten" enumeration_work_package_priorities: "Arbeitspaket-Prioritäten"
enumeration_planning_element_statuses: "Planungselement-Status"
enumeration_reported_project_statuses: "Gemeldeter Projekt-Status" enumeration_reported_project_statuses: "Gemeldeter Projekt-Status"
enumeration_system_activity: "System-Aktivität" enumeration_system_activity: "System-Aktivität"
@ -544,7 +543,8 @@ de:
due_date: "Abschlussdatum" due_date: "Abschlussdatum"
error: "Ein Fehler ist aufgetreten." error: "Ein Fehler ist aufgetreten."
filter: filter:
none: "(keines)" noneElement: "(keines)"
noneSelection: "(keine)"
name: "Name" name: "Name"
project_status: "Projekt-Status" project_status: "Projekt-Status"
project_type: "Projekt-Typ" project_type: "Projekt-Typ"
@ -866,6 +866,7 @@ de:
label_start_to_end: "Anfang bis Ende" label_start_to_end: "Anfang bis Ende"
label_start_to_start: "Anfang bis Anfang" label_start_to_start: "Anfang bis Anfang"
label_statistics: "Statistiken" label_statistics: "Statistiken"
label_status: "Status"
label_stay_logged_in: "Angemeldet bleiben" label_stay_logged_in: "Angemeldet bleiben"
label_string: "Text" label_string: "Text"
label_subproject_new: "Neues Unterprojekt" label_subproject_new: "Neues Unterprojekt"
@ -962,6 +963,7 @@ de:
label_keyboard_shortcut_help_heading: "Vorhandene Tastaturkürzel" label_keyboard_shortcut_help_heading: "Vorhandene Tastaturkürzel"
label_keyboard_shortcut_within_project: "Innerhalb von Projekten:" label_keyboard_shortcut_within_project: "Innerhalb von Projekten:"
label_keyboard_shortcut_global_shortcuts: "Globale Tastaturkürzel:" label_keyboard_shortcut_global_shortcuts: "Globale Tastaturkürzel:"
label_keyboard_shortcut_some_pages_only: " Tastenkürzel auf speziellen Seiten:"
label_keyboard_shortcut_search_global: "OpenProject weite Suche" label_keyboard_shortcut_search_global: "OpenProject weite Suche"
label_keyboard_shortcut_search_project: "Projektsuche" label_keyboard_shortcut_search_project: "Projektsuche"
label_keyboard_shortcut_go_my_page: "Gehe zu Meiner Seite'" label_keyboard_shortcut_go_my_page: "Gehe zu Meiner Seite'"
@ -974,6 +976,11 @@ de:
label_keyboard_shortcut_go_news: "Gehe zu den Projekt News" label_keyboard_shortcut_go_news: "Gehe zu den Projekt News"
label_keyboard_shortcut_go_timelines: "Gehe zur Zeitpläne Seite" label_keyboard_shortcut_go_timelines: "Gehe zur Zeitpläne Seite"
label_keyboard_shortcut_new_work_package: "Erstelle neues Arbeitspacket" label_keyboard_shortcut_new_work_package: "Erstelle neues Arbeitspacket"
label_keyboard_shortcut_go_edit: "Gehe zur Bearbeiten-Ansicht (nur auf Detailseiten)"
label_keyboard_shortcut_open_more_menu: "Öffne 'Weitere-Aktionen' Menü (nur auf Detailseiten)"
label_keyboard_shortcut_go_preview: "Gehe zur Vorschau (nur auf Editieren-Seiten)"
label_keyboard_shortcut_focus_previous_item: "Fokussiere vorheriges Listenelement (nur bei einigen Listen)"
label_keyboard_shortcut_focus_next_item: "Fokussiere nächstes Listenelement (nur bei einigen Listen)"
mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:" mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:"
mail_body_account_information: "Ihre Konto-Informationen" mail_body_account_information: "Ihre Konto-Informationen"
@ -1246,15 +1253,16 @@ de:
text_analyze: "Weiter analysieren: %{subject}" text_analyze: "Weiter analysieren: %{subject}"
text_are_you_sure: "Sind Sie sicher?" text_are_you_sure: "Sind Sie sicher?"
text_are_you_sure_with_children: "Lösche Aufgabe und alle Unteraufgaben?" text_are_you_sure_with_children: "Lösche Aufgabe und alle Unteraufgaben?"
text_assign_time_entries_to_project: "Gebuchte Aufwände dem Projekt zuweisen" text_assign_to_project: "Dem Projekt zuweisen"
text_caracters_maximum: "Max. %{count} Zeichen." text_caracters_maximum: "Max. %{count} Zeichen."
text_caracters_minimum: "Muss mindestens %{count} Zeichen lang sein." text_caracters_minimum: "Muss mindestens %{count} Zeichen lang sein."
text_comma_separated: "Mehrere Werte erlaubt (durch Komma getrennt)." text_comma_separated: "Mehrere Werte erlaubt (durch Komma getrennt)."
text_custom_field_possible_values_info: "Eine Zeile pro Wert" text_custom_field_possible_values_info: "Eine Zeile pro Wert"
text_default_administrator_account_changed: "Administrator-Kennwort geändert" text_default_administrator_account_changed: "Administrator-Kennwort geändert"
text_default_encoding: "Default: UTF-8" text_default_encoding: "Default: UTF-8"
text_destroy_time_entries: "Gebuchte Aufwände löschen" text_destroy: "Löschen"
text_destroy_time_entries_question: "Es wurden bereits %{hours} Stunden auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?" text_destroy_with_associated: "Es sind weitere Datenobjekte mit den bzw. dem zu löschenden Arbeitpaket(en) verbunden. Es handelt sich dabei um Objekte der folgenden Typen:"
text_destroy_what_to_do: "Was soll mit den Objekten geschehen?"
text_diff_truncated: "... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet." text_diff_truncated: "... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet."
text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert, und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/configuration.yml vor und starten Sie die Applikation neu." text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert, und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/configuration.yml vor und starten Sie die Applikation neu."
text_enumeration_category_reassign_to: "Die Objekte statt dessen diesem Wert zuordnen:" text_enumeration_category_reassign_to: "Die Objekte statt dessen diesem Wert zuordnen:"
@ -1289,7 +1297,8 @@ de:
text_powered_by: "Powered by %{link}" text_powered_by: "Powered by %{link}"
text_project_destroy_confirmation: "Sind Sie sicher, dass sie das Projekt löschen wollen?" text_project_destroy_confirmation: "Sind Sie sicher, dass sie das Projekt löschen wollen?"
text_project_identifier_info: "Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt, muss mit einem Kleinbuchstaben beginnen.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden." text_project_identifier_info: "Kleinbuchstaben (a-z), Ziffern, Binde- und Unterstriche erlaubt, muss mit einem Kleinbuchstaben beginnen.<br />Einmal gespeichert, kann die Kennung nicht mehr geändert werden."
text_reassign_time_entries: "Gebuchte Aufwände diesem Ticket zuweisen:" text_reassign: "Neu zuweisen an"
text_reassign_to: "Ziel-Arbeitspaket:"
text_regexp_info: "z. B. ^[A-Z0-9]+$" text_regexp_info: "z. B. ^[A-Z0-9]+$"
text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der OpenProject-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen OpenProject- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet." text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der OpenProject-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen OpenProject- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet."
text_rmagick_available: "RMagick verfügbar (optional)" text_rmagick_available: "RMagick verfügbar (optional)"
@ -1375,12 +1384,10 @@ de:
comparisons: "Planungsvergleiche" comparisons: "Planungsvergleiche"
comparison: comparison:
absolute: "Absolut" absolute: "Absolut"
historical: "Historisch"
none: "Kein Planungsvergleich" none: "Kein Planungsvergleich"
relative: "Relativ" relative: "Relativ"
compare_relative: "Vergleiche aktuelle Planung mit vor %{timespan}" compare_relative: "Vergleiche aktuelle Planung mit vor %{timespan}"
compare_absolute: "Vergleiche aktuelle Planung mit %{date}" compare_absolute: "Vergleiche aktuelle Planung mit %{date}"
compare_historical: "Vergleiche %{startdate} mit %{enddate}"
time_relative: time_relative:
days: "Tagen" days: "Tagen"
weeks: "Wochen" weeks: "Wochen"
@ -1396,7 +1403,8 @@ de:
grouping_two: "Zweites Gruppierungskriterium" grouping_two: "Zweites Gruppierungskriterium"
grouping_two_phrase: "Hat eine Abhängigkeit zu Projekttyp" grouping_two_phrase: "Hat eine Abhängigkeit zu Projekttyp"
hide_chart: "Diagramm ausblenden" hide_chart: "Diagramm ausblenden"
none: "(keines)" noneElement: "(keines)"
noneSelection: "(keine)"
outline: "Zu Beginn angezeigte Hierarchie" outline: "Zu Beginn angezeigte Hierarchie"
parent: "Zeige Unterprojekte von" parent: "Zeige Unterprojekte von"
planning_element_filters: "Planungselemente filtern" planning_element_filters: "Planungselemente filtern"

@ -79,12 +79,6 @@ en:
value: "Value" value: "Value"
enumeration: enumeration:
active: "Active" active: "Active"
issue:
parent_issue: "Parent"
subproject: "Subproject"
time_entries: "Log time"
type: "Type"
watcher: "Watcher"
relation: relation:
delay: "Delay" delay: "Delay"
from: "Work Package" from: "Work Package"
@ -173,6 +167,10 @@ en:
progress: "% done" progress: "% done"
responsible: "Responsible" responsible: "Responsible"
spent_time: "Spent time" spent_time: "Spent time"
subproject: "Subproject"
time_entries: "Log time"
type: "Type"
watcher: "Watcher"
errors: errors:
messages: messages:
accepted: "must be accepted" accepted: "must be accepted"
@ -210,6 +208,7 @@ en:
identical_projects: "can not be created from one project to itself" identical_projects: "can not be created from one project to itself"
project_association_not_allowed: "does not allow associations" project_association_not_allowed: "does not allow associations"
work_package: work_package:
is_not_a_valid_target_for_time_entries: "Work package #%{id} is not a valid target for reassigning the time entries."
attributes: attributes:
due_date: due_date:
not_start_date: "is not on start date, although this is required for milestones" not_start_date: "is not on start date, although this is required for milestones"
@ -478,7 +477,6 @@ en:
enumeration_activities: "Activities (time tracking)" enumeration_activities: "Activities (time tracking)"
enumeration_work_package_priorities: "Work package priorities" enumeration_work_package_priorities: "Work package priorities"
enumeration_system_activity: "System Activity" enumeration_system_activity: "System Activity"
enumeration_planning_element_statuses: "Planning element status"
enumeration_reported_project_statuses: "Reported project status" enumeration_reported_project_statuses: "Reported project status"
error_can_not_archive_project: "This project cannot be archived" error_can_not_archive_project: "This project cannot be archived"
@ -547,7 +545,8 @@ en:
create_planning_select_project: "Project:" create_planning_select_project: "Project:"
really_close_dialog: "Do you really want to close the dialog and loose the entered data?" really_close_dialog: "Do you really want to close the dialog and loose the entered data?"
filter: filter:
none: "(none)" noneElement: "(none)"
noneSelection: "(none)"
label_account: "Account" label_account: "Account"
label_activity: "Activity" label_activity: "Activity"
@ -862,6 +861,7 @@ en:
label_start_to_end: "start to end" label_start_to_end: "start to end"
label_start_to_start: "start to start" label_start_to_start: "start to start"
label_statistics: "Statistics" label_statistics: "Statistics"
label_status: "Status"
label_stay_logged_in: "Stay logged in" label_stay_logged_in: "Stay logged in"
label_string: "Text" label_string: "Text"
label_subproject_new: "New subproject" label_subproject_new: "New subproject"
@ -958,6 +958,7 @@ en:
label_keyboard_shortcut_help_heading: "Available Keyboard Shortcuts" label_keyboard_shortcut_help_heading: "Available Keyboard Shortcuts"
label_keyboard_shortcut_within_project: "Project related shortcuts:" label_keyboard_shortcut_within_project: "Project related shortcuts:"
label_keyboard_shortcut_global_shortcuts: "Global shortcuts:" label_keyboard_shortcut_global_shortcuts: "Global shortcuts:"
label_keyboard_shortcut_some_pages_only: "Special shortcuts:"
label_keyboard_shortcut_search_global: "Global search" label_keyboard_shortcut_search_global: "Global search"
label_keyboard_shortcut_search_project: "Find a project" label_keyboard_shortcut_search_project: "Find a project"
label_keyboard_shortcut_go_my_page: "Go to My Page" label_keyboard_shortcut_go_my_page: "Go to My Page"
@ -970,6 +971,11 @@ en:
label_keyboard_shortcut_go_news: "Go to project news" label_keyboard_shortcut_go_news: "Go to project news"
label_keyboard_shortcut_go_timelines: "Go to timelines" label_keyboard_shortcut_go_timelines: "Go to timelines"
label_keyboard_shortcut_new_work_package: "Create new work package" label_keyboard_shortcut_new_work_package: "Create new work package"
label_keyboard_shortcut_go_edit: "Go to edit the current item (on detail pages only)"
label_keyboard_shortcut_open_more_menu: "Open more-menu (on detail pages only)"
label_keyboard_shortcut_go_preview: "Go to preview the current edit (on edit pages only)"
label_keyboard_shortcut_focus_previous_item: "Focus previous list element (on some lists only)"
label_keyboard_shortcut_focus_next_item: "Focus next list element (on some lists only)"
mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:" mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
mail_body_account_information: "Your account information" mail_body_account_information: "Your account information"
@ -1232,15 +1238,16 @@ en:
text_analyze: "Further analyze: %{subject}" text_analyze: "Further analyze: %{subject}"
text_are_you_sure: "Are you sure?" text_are_you_sure: "Are you sure?"
text_are_you_sure_with_children: "Delete work package and all child work packages?" text_are_you_sure_with_children: "Delete work package and all child work packages?"
text_assign_time_entries_to_project: "Assign reported hours to the project" text_assign_to_project: "Assign to the project"
text_caracters_maximum: "%{count} characters maximum." text_caracters_maximum: "%{count} characters maximum."
text_caracters_minimum: "Must be at least %{count} characters long." text_caracters_minimum: "Must be at least %{count} characters long."
text_comma_separated: "Multiple values allowed (comma separated)." text_comma_separated: "Multiple values allowed (comma separated)."
text_custom_field_possible_values_info: "One line for each value" text_custom_field_possible_values_info: "One line for each value"
text_default_administrator_account_changed: "Default administrator account changed" text_default_administrator_account_changed: "Default administrator account changed"
text_default_encoding: "Default: UTF-8" text_default_encoding: "Default: UTF-8"
text_destroy_time_entries: "Delete reported hours" text_destroy: "Delete"
text_destroy_time_entries_question: "%{hours} hours were reported on the work package you are about to delete. What do you want to do?" text_destroy_with_associated: "There are additional objects assossociated with the work package(s) that are to be deleted. Those objects are of the following types:"
text_destroy_what_to_do: "What do you want to do?"
text_diff_truncated: "... This diff was truncated because it exceeds the maximum size that can be displayed." text_diff_truncated: "... This diff was truncated because it exceeds the maximum size that can be displayed."
text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them."
text_enumeration_category_reassign_to: "Reassign them to this value:" text_enumeration_category_reassign_to: "Reassign them to this value:"
@ -1272,7 +1279,8 @@ en:
text_powered_by: "Powered by %{link}" text_powered_by: "Powered by %{link}"
text_project_destroy_confirmation: "Are you sure you want to delete this project and related data?" text_project_destroy_confirmation: "Are you sure you want to delete this project and related data?"
text_project_identifier_info: "Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed." text_project_identifier_info: "Only lower case letters (a-z), numbers, dashes and underscores are allowed, must start with a lower case letter.<br />Once saved, the identifier cannot be changed."
text_reassign_time_entries: "Reassign reported hours to this work package:" text_reassign: "Reassign to"
text_reassign_to: "work package:"
text_regexp_info: "eg. ^[A-Z0-9]+$" text_regexp_info: "eg. ^[A-Z0-9]+$"
text_repository_usernames_mapping: "Select or update the OpenProject user mapped to each username found in the repository log.\nUsers with the same OpenProject and repository username or email are automatically mapped." text_repository_usernames_mapping: "Select or update the OpenProject user mapped to each username found in the repository log.\nUsers with the same OpenProject and repository username or email are automatically mapped."
text_rmagick_available: "RMagick available (optional)" text_rmagick_available: "RMagick available (optional)"
@ -1364,12 +1372,10 @@ en:
comparisons: "Comparisons" comparisons: "Comparisons"
comparison: comparison:
absolute: "Absolute" absolute: "Absolute"
historical: "Historical"
none: "None" none: "None"
relative: "Relative" relative: "Relative"
compare_relative: "Compare current planning to %{timespan} ago" compare_relative: "Compare current planning to %{timespan} ago"
compare_absolute: "Compare current planning to %{date}" compare_absolute: "Compare current planning to %{date}"
compare_historical: "Compare %{startdate} to %{enddate}"
time_relative: time_relative:
days: "days" days: "days"
weeks: "weeks" weeks: "weeks"
@ -1385,7 +1391,8 @@ en:
grouping_two: "Second grouping criterion" grouping_two: "Second grouping criterion"
grouping_two_phrase: "Has a dependency to project type" grouping_two_phrase: "Has a dependency to project type"
hide_chart: "Hide chart" hide_chart: "Hide chart"
none: "(none)" noneElement: "(none)"
noneSelection: "(none)"
outline: "Intitial outline expansion" outline: "Intitial outline expansion"
parent: "Show subprojects of" parent: "Show subprojects of"
planning_element_filters: "Filter planning elements" planning_element_filters: "Filter planning elements"
@ -1393,7 +1400,7 @@ en:
types: "Show types" types: "Show types"
status: "Show status" status: "Show status"
project_time_filter: "Projects with a Planning Element of a certain type in a certain timeframe" project_time_filter: "Projects with a planning element of a certain type in a certain timeframe"
project_time_filter_historical: "from %{startdate} to %{enddate}" project_time_filter_historical: "from %{startdate} to %{enddate}"
project_time_filter_relative: "from %{startspan}%{startspanunit} ago, to %{endspan}%{endspanunit} from now" project_time_filter_relative: "from %{startspan}%{startspanunit} ago, to %{endspan}%{endspanunit} from now"
project_filters: "Filter projects" project_filters: "Filter projects"

@ -271,11 +271,7 @@ OpenProject::Application.routes.draw do
match 'auto_complete' => 'auto_completes#index', :via => [:get, :post], :format => false match 'auto_complete' => 'auto_completes#index', :via => [:get, :post], :format => false
match 'context_menu' => 'context_menus#index', :via => [:get, :post], :format => false match 'context_menu' => 'context_menus#index', :via => [:get, :post], :format => false
resources :calendar, :controller => 'calendars', :only => [:index] resources :calendar, :controller => 'calendars', :only => [:index]
end resource :bulk, :controller => 'bulk', :only => [:edit, :update, :destroy]
namespace :work_package_bulk do
get :edit, :format => false
put :update, :format => false
end end
resources :work_packages, :only => [:show, :edit, :update, :index] do resources :work_packages, :only => [:show, :edit, :update, :index] do
@ -422,8 +418,6 @@ OpenProject::Application.routes.draw do
end end
end end
resources :planning_element_statuses, :controller => 'planning_element_statuses'
resources :project_types, :controller => 'project_types' do resources :project_types, :controller => 'project_types' do
member do member do
get :confirm_destroy get :confirm_destroy

@ -9,9 +9,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'yaml' require_relative 'migration_utils/timelines'
require_relative 'migration_utils/utils'
class MigrateTimelinesEndDatePropertyInOptions < ActiveRecord::Migration class MigrateTimelinesEndDatePropertyInOptions < ActiveRecord::Migration
include Migration::Utils include Migration::Utils
@ -26,7 +24,7 @@ class MigrateTimelinesEndDatePropertyInOptions < ActiveRecord::Migration
say_with_time_silently "Update timelines options" do say_with_time_silently "Update timelines options" do
update_column_values('timelines', update_column_values('timelines',
[COLUMN], [COLUMN],
update_options(OPTIONS), update_options(migrate_end_date_options(OPTIONS)),
options_filter(OPTIONS.keys)) options_filter(OPTIONS.keys))
end end
end end
@ -35,7 +33,7 @@ class MigrateTimelinesEndDatePropertyInOptions < ActiveRecord::Migration
say_with_time_silently "Restore timelines options" do say_with_time_silently "Restore timelines options" do
update_column_values('timelines', update_column_values('timelines',
[COLUMN], [COLUMN],
update_options(OPTIONS.invert), update_options(migrate_end_date_options(OPTIONS.invert)),
options_filter(OPTIONS.invert.keys)) options_filter(OPTIONS.invert.keys))
end end
end end
@ -46,28 +44,11 @@ class MigrateTimelinesEndDatePropertyInOptions < ActiveRecord::Migration
filter([COLUMN], options) filter([COLUMN], options)
end end
def update_options(options) def migrate_end_date_options(options)
Proc.new do |row| Proc.new do |timelines_opts|
timelines_opts = YAML.load(row[COLUMN]) opts = rename_columns(timelines_opts, options)
renamed_options = timelines_opts.each_with_object({}) do |(k, v), h|
new_key = (options.has_key? k) ? options[k] : k
h[new_key] = update_option_value(v)
end
row[COLUMN] = YAML.dump(renamed_options)
UpdateResult.new(row, true)
end
end
def update_option_value(value) opts
if value.kind_of? Array
value.map{|e| (OPTIONS.has_key? e) ? OPTIONS[e] : e}
elsif OPTIONS.has_key? value
OPTIONS[value]
else
value
end end
end end
end end

@ -0,0 +1,78 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
class MigrateRemainingCoreSettings < ActiveRecord::Migration
REPLACED = {
"tracker" => "type",
"issue_status_updated" => "status_updated",
"issue_status" => "status",
"issue_field" => "field"
}
def self.up
# Delete old plugin settings no longer needed
ActiveRecord::Base.connection.execute <<-SQL
DELETE FROM #{settings_table}
WHERE name = #{quote_value('plugin_redmine_favicon')}
OR name = #{quote_value('plugin_chiliproject_help_link')}
SQL
# Rename Tracker to Type
Setting['work_package_list_default_columns'] = replace(Setting['work_package_list_default_columns'], REPLACED)
# Rename IssueStatus in notified events
Setting['notified_events'] = replace(Setting['notified_events'], REPLACED)
# Rename IssueStatus and IssueField in work_package_done_ratio
Setting['work_package_done_ratio'] = replace(Setting['work_package_done_ratio'], REPLACED)
end
def self.down
# the above delete part is inherently not reversable
# Rename Type to Tracker
Setting['work_package_list_default_columns'] = replace(Setting['work_package_list_default_columns'], REPLACED.invert)
# Rename Status to IssueStatus in notified events
Setting['notified_events'] = replace(Setting['notified_events'], REPLACED.invert)
# Rename back to IssueStatus and IssueField in work_package_done_ratio
Setting['work_package_done_ratio'] = replace(Setting['work_package_done_ratio'], REPLACED.invert)
end
private
def replace(value,mapping)
if value.respond_to? :map
value.map { |s| mapping[s].nil? ? s : mapping[s] }
else
mapping[value].nil? ? value : mapping[value]
end
end
def settings_table
@settings_table ||= ActiveRecord::Base.connection.quote_table_name('settings')
end
def quote_value s
ActiveRecord::Base.connection.quote(s)
end
end

@ -0,0 +1,157 @@
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require_relative 'migration_utils/timelines'
class MigrateTimelinesOptions < ActiveRecord::Migration
include Migration::Utils
COLUMN = 'options'
OPTIONS = {
# already done in 20131015064141_migrate_timelines_end_date_property_in_options.rb
#'end_date' => 'due_date',
'planning_element_types' => 'type',
'project_type' => 'type',
'project_status' => 'status',
}
def up
say_with_time_silently "Check for historical comparisons" do
comparisons = timelines_with_historical_comparisons
unless comparisons.empty?
affected_ids = comparisons.collect(&:id)
raise "Error: Cannot migrate timelines options!"\
"\n\n"\
"Timelines exist that use historical comparison. This is not\n"\
"supported in future versions of timelines.\n\n"\
"The affected timelines ids are: #{affected_ids}\n\n"\
"You may use the rake task "\
"'migrations:timelines:remove_timelines_historical_comparison_from_options' "\
"to prepare the\n"\
"current schema for this migration."\
"\n\n\n"
end
end
say_with_time_silently "Update timelines options" do
update_column_values('timelines',
[COLUMN],
update_options(migrate_timelines_options(OPTIONS,
pe_id_map,
pe_type_id_map)),
nil)
end
end
def down
say_with_time_silently "Restore timelines options" do
update_column_values('timelines',
[COLUMN],
update_options(migrate_timelines_options(OPTIONS.invert,
pe_id_map.invert,
pe_type_id_map.invert)),
nil)
end
end
private
PE_TYPE_KEY = 'planning_element_types'
PE_TIME_TYPE_KEY = 'planning_element_time_types'
VERTICAL_PE_TYPES = 'vertical_planning_elements'
def migrate_timelines_options(options, pe_id_map, pe_type_id_map)
Proc.new do |timelines_opts|
timelines_opts = rename_columns timelines_opts, options
timelines_opts = migrate_planning_element_types timelines_opts, pe_type_id_map
timelines_opts = migrate_planning_element_time_types timelines_opts, pe_type_id_map
timelines_opts = migrate_vertical_planning_elements timelines_opts, pe_id_map
timelines_opts
end
end
def migrate_planning_element_types(timelines_opts, pe_type_id_map)
pe_types = []
pe_types = timelines_opts[PE_TYPE_KEY].delete_if { |t| t.nil? } if timelines_opts.has_key? PE_TYPE_KEY
pe_types = pe_types.empty? ? new_ids_of_former_pes
: pe_types.map { |p| pe_type_id_map[p] }
timelines_opts[PE_TYPE_KEY] = pe_types
timelines_opts
end
def migrate_planning_element_time_types(timelines_opts, pe_type_id_map)
return timelines_opts unless timelines_opts.has_key? PE_TIME_TYPE_KEY
pe_time_types = timelines_opts[PE_TIME_TYPE_KEY]
pe_time_types.map! { |p| pe_type_id_map[p] }
timelines_opts[PE_TIME_TYPE_KEY] = pe_time_types
timelines_opts
end
def migrate_vertical_planning_elements(timelines_opts, pe_id_map)
return timelines_opts unless timelines_opts.has_key? VERTICAL_PE_TYPES
vertical_pes = timelines_opts[VERTICAL_PE_TYPES].split(',')
.map { |p| p.strip }
unless vertical_pes.empty?
mapped_pes = vertical_pes.map { |v| pe_id_map[v] }
.compact
timelines_opts[VERTICAL_PE_TYPES] = mapped_pes.join(',')
end
timelines_opts
end
def new_ids_of_former_pes
@new_ids_of_former_pes ||= pe_types_ids_with_new_ids.each_with_object([]) do |i, l|
l << i['new_id']
end
end
def pe_type_id_map
@pe_type_id_map ||= pe_types_ids_with_new_ids.each_with_object({}) do |r, h|
h[r['id']] = r['new_id']
end
end
def pe_types_ids_with_new_ids
select_all <<-SQL
SELECT id, new_id
FROM legacy_planning_element_types
SQL
end
def pe_id_map
@pe_id_map ||= pe_ids_with_new_ids.each_with_object({}) do |r, h|
h[r['id']] = r['new_id']
end
end
def pe_ids_with_new_ids
select_all <<-SQL
SELECT id, new_id
FROM legacy_planning_elements
SQL
end
end

@ -64,12 +64,12 @@ module Migration
COLUMNS, COLUMNS,
find_work_packages_with_missing_initial_attachment(legacy_journal_type, find_work_packages_with_missing_initial_attachment(legacy_journal_type,
result), result),
filter(legacy_journal_type)) journal_filter(legacy_journal_type))
result.flatten result.flatten
end end
def filter(legacy_journal_type) def journal_filter(legacy_journal_type)
"type = '#{legacy_journal_type}' AND changed_data LIKE '%attachments%'" "type = '#{legacy_journal_type}' AND changed_data LIKE '%attachments%'"
end end

@ -0,0 +1,66 @@
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'yaml'
require_relative 'utils'
module Migration
module Utils
TimelineWithHistoricalComparison = Struct.new(:id, :from_date, :to_date)
OPTIONS_COLUMN = 'options'
HISTORICAL_DATE_FROM = 'compare_to_historical_one'
HISTORICAL_DATE_TO = 'compare_to_historical_two'
def timelines_with_historical_comparisons
timelines = select_all <<-SQL
SELECT id, options
FROM timelines
WHERE options LIKE '%comparison: historical%'
SQL
timelines.each_with_object([]) do |r, l|
options = YAML.load(r[OPTIONS_COLUMN])
from_date = options[HISTORICAL_DATE_FROM]
to_date = options[HISTORICAL_DATE_TO]
l << TimelineWithHistoricalComparison.new(r['id'], from_date, to_date)
end
end
def update_options(callback)
Proc.new do |row|
timelines_opts = YAML.load(row[OPTIONS_COLUMN])
migrated_options = callback.call(timelines_opts.clone) unless callback.nil?
row[OPTIONS_COLUMN] = YAML.dump(HashWithIndifferentAccess.new(migrated_options))
UpdateResult.new(row, true)
end
end
def rename_columns(timelines_opts, options)
return timelines_opts unless timelines_opts.has_key? 'columns'
columns = timelines_opts['columns']
columns.map! do |c|
options.has_key?(c) ? options[c] : c
end
timelines_opts['columns'] = columns.uniq
timelines_opts
end
end
end

@ -29,18 +29,28 @@ See doc/COPYRIGHT.rdoc for more details.
# Changelog # Changelog
* `#709` Added test for double custom field validation error messages
* `#902` Spelling Mistake: Timelines Report Configuration
* `#959` Too many available responsibles returned for filtering in Timelines * `#959` Too many available responsibles returned for filtering in Timelines
* `#1738` Forum problem when no description given. * `#1738` Forum problem when no description given.
* `#1916` Work package update screen is closed when attached file is deleted * `#1916` Work package update screen is closed when attached file is deleted
* `#1935` Fixed bug: Default submenu for wiki pages is wrong (Configure menu item) * `#1935` Fixed bug: Default submenu for wiki pages is wrong (Configure menu item)
* `#2009` No journal entry created for attachments if the attachment is added on container creation * `#2009` No journal entry created for attachments if the attachment is added on container creation
* `#2026` 404 error when letters are entered in category Work Package * `#2026` 404 error when letters are entered in category Work Package
* `#2221` [Accessibility] enhance keyboard shortcuts
* `#2371` Add support for IE10 to Timelines * `#2371` Add support for IE10 to Timelines
* `#2400` Cannot delete work package
* `#2423` [Issue Tracker] Several Internal Errors when there is no default work package status
* `#2426` [Core] Enumerations for planning elements
* `#2427` [Issue Tracker] Cannot delete work package priority
* `#2433` [Timelines] Empty timeline report not displayed initially
* `#2448` Accelerate work package updates * `#2448` Accelerate work package updates
* `#2464` No initial attachment journal for messages * `#2464` No initial attachment journal for messages
* `#2470` [Timelines] Vertical planning elements which are not displayed horizontally are not shown in timeline report * `#2470` [Timelines] Vertical planning elements which are not displayed horizontally are not shown in timeline report
* `#2479` Remove TinyMCE spike * `#2479` Remove TinyMCE spike
* `#2557` Highlight changes of any work package attribute available in the timelines table * `#2521` XSS: MyPage on unfiltered WorkPackage Subject
* `#2548` Migrated core settings
* `#2557` Highlight changes of any work package attribute available in the timelines table
* `#2559` Migrate existing IssueCustomFields to WorkPackageCustomFields * `#2559` Migrate existing IssueCustomFields to WorkPackageCustomFields
* Fix compatibility with old mail configuration * Fix compatibility with old mail configuration

@ -123,60 +123,6 @@ Given /^(?:the )?[pP]roject "([^\"]*)" does not use the following [mM]odules:$/
p.reload p.reload
end end
Given /^the [Uu]ser "([^\"]*)" is a "([^\"]*)" (?:in|of) the [Pp]roject "([^\"]*)"$/ do |user, role, project|
u = User.find_by_login(user)
r = Role.find_by_name(role)
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin do
Member.new.tap do |m|
m.user = u
m.privacy_unnecessary = true if plugin_loaded?("redmine_dtag_privacy")
m.roles << r
m.project = p
end.save!
end
end
Given /^there is a [rR]ole "([^\"]*)"$/ do |name|
Role.spawn.tap { |r| r.name = name }.save! unless Role.find_by_name(name)
end
Given /^there are the following roles:$/ do |table|
table.raw.flatten.each do |name|
FactoryGirl.create(:role, :name => name) unless Role.find_by_name(name)
end
end
Given /^the [rR]ole "([^\"]*)" may have the following [rR]ights:$/ do |role, table|
r = Role.find_by_name(role)
raise "No such role was defined: #{role}" unless r
as_admin do
available_perms = Redmine::AccessControl.permissions.collect(&:name)
r.permissions = []
table.raw.each do |_perm|
perm = _perm.first
unless perm.blank?
perm = perm.gsub(" ", "_").underscore.to_sym
if available_perms.include?(:"#{perm}")
r.permissions << perm
end
end
end
r.save!
end
end
Given /^the [rR]ole "(.+?)" has no (?:[Pp]ermissions|[Rr]ights)$/ do |role_name|
role = Role.find_by_name(role_name)
raise "No such role was defined: #{role_name}" unless role
as_admin do
role.permissions = []
role.save!
end
end
Given /^the [Uu]ser "([^\"]*)" has 1 time [eE]ntry$/ do |user| Given /^the [Uu]ser "([^\"]*)" has 1 time [eE]ntry$/ do |user|
u = User.find_by_login user u = User.find_by_login user
p = u.projects.last p = u.projects.last
@ -383,10 +329,6 @@ Given /^the [pP]roject uses the following modules:$/ do |table|
end end
Given /^the user "(.*?)" is a "([^\"]*?)"$/ do |user, role|
step %Q{the user "#{user}" is a "#{role}" in the project "#{get_project.name}"}
end
Given /^the [pP]roject(?: "([^\"]*)")? has the following types:$/ do |project_name, table| Given /^the [pP]roject(?: "([^\"]*)")? has the following types:$/ do |project_name, table|
p = get_project(project_name) p = get_project(project_name)
table.hashes.each_with_index do |t, i| table.hashes.each_with_index do |t, i|
@ -502,4 +444,3 @@ end
def plugin_loaded?(name) def plugin_loaded?(name)
Redmine::Plugin.all.detect {|x| x.id == name.to_sym}.present? Redmine::Plugin.all.detect {|x| x.id == name.to_sym}.present?
end end

@ -1,29 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
InstanceFinder.register(PlanningElementStatus, Proc.new { |name| PlanningElementStatus.find_by_name(name) })

@ -0,0 +1,92 @@
# encoding: utf-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
Given /^the [Uu]ser "([^\"]*)" is a "([^\"]*)" (?:in|of) the [Pp]roject "([^\"]*)"$/ do |user, role, project|
u = User.find_by_login(user)
r = Role.find_by_name(role)
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin do
Member.new.tap do |m|
m.user = u
m.privacy_unnecessary = true if plugin_loaded?("redmine_dtag_privacy")
m.roles << r
m.project = p
end.save!
end
end
Given /^there is a [rR]ole "([^\"]*)"$/ do |name, table = Cucumber::Ast::Table.new([])|
FactoryGirl.create(:role, :name => name) unless Role.find_by_name(name)
end
Given /^there is a [rR]ole "([^\"]*)" with the following permissions:?$/ do |name, table|
FactoryGirl.create(:role, :name => name, :permissions => table.raw.flatten) unless Role.find_by_name(name)
end
Given /^there are the following roles:$/ do |table|
table.raw.flatten.each do |name|
FactoryGirl.create(:role, :name => name) unless Role.find_by_name(name)
end
end
Given /^the [rR]ole "([^\"]*)" may have the following [rR]ights:$/ do |role, table|
r = Role.find_by_name(role)
raise "No such role was defined: #{role}" unless r
as_admin do
available_perms = Redmine::AccessControl.permissions.collect(&:name)
r.permissions = []
table.raw.each do |_perm|
perm = _perm.first
unless perm.blank?
perm = perm.gsub(" ", "_").underscore.to_sym
if available_perms.include?(:"#{perm}")
r.permissions << perm
end
end
end
r.save!
end
end
Given /^the [rR]ole "(.+?)" has no (?:[Pp]ermissions|[Rr]ights)$/ do |role_name|
role = Role.find_by_name(role_name)
raise "No such role was defined: #{role_name}" unless role
as_admin do
role.permissions = []
role.save!
end
end
Given /^the user "(.*?)" is a "([^\"]*?)"$/ do |user, role|
step %Q{the user "#{user}" is a "#{role}" in the project "#{get_project.name}"}
end

@ -47,14 +47,6 @@ Given /^I am working in the [tT]imeline "([^"]*)" of the project called "([^"]*)
@timeline_name = timeline_name @timeline_name = timeline_name
end end
Given /^there are the following planning element statuses:$/ do |table|
table.map_headers! { |header| header.underscore.gsub(' ', '_') }
table.hashes.each do |type_attributes|
FactoryGirl.create(:planning_element_status, type_attributes)
end
end
Given /^there are the following reported project statuses:$/ do |table| Given /^there are the following reported project statuses:$/ do |table|
table.map_headers! { |header| header.underscore.gsub(' ', '_') } table.map_headers! { |header| header.underscore.gsub(' ', '_') }

@ -135,6 +135,9 @@ module NavigationHelpers
when /^the new work_package page (?:for|of) the project called "([^\"]+)"$/ when /^the new work_package page (?:for|of) the project called "([^\"]+)"$/
"/projects/#{$1}/work_packages/new" "/projects/#{$1}/work_packages/new"
when /^the bulk destroy page of work packages$/
Rails.application.routes.url_helpers.work_packages_bulk_path
when /^the wiki index page(?: below the (.+) page)? (?:for|of) (?:the)? project(?: called)? (.+)$/ when /^the wiki index page(?: below the (.+) page)? (?:for|of) (?:the)? project(?: called)? (.+)$/
parent_page_title, project_identifier = $1, $2 parent_page_title, project_identifier = $1, $2
project_identifier.gsub!("\"", "") project_identifier.gsub!("\"", "")

@ -82,9 +82,6 @@ Feature: Timeline Wiki Macro
And the user "manager" is a "loser" And the user "manager" is a "loser"
And the user "mrtimeline" is a "god" And the user "mrtimeline" is a "god"
And there are the following planning element statuses:
| Name |
| closed |
And there are the following work packages: And there are the following work packages:
| Subject | Start date | Due date | description | status | responsible | | Subject | Start date | Due date | description | status | responsible |
| January | 2012-01-01 | 2012-01-31 | Avocado Grande | closed | manager | | January | 2012-01-01 | 2012-01-31 | Avocado Grande | closed | manager |

@ -0,0 +1,68 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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.
#++
Feature: Deleting work packages
Background:
Given there is 1 user with:
| login | manager |
And there are the following types:
| Name | Is milestone |
| Phase1 | false |
And there is a project named "ecookbook"
And there is a role "manager" with the following permissions:
| view_work_packages |
| delete_work_packages |
| view_time_entries |
| edit_time_entries |
And the user "manager" is a "manager" in the project "ecookbook"
And there are the following work packages in project "ecookbook":
| subject |
| wp1 |
| wp2 |
And there is a time entry for "wp1" with 10 hours
And I am already logged in as "manager"
@javascript
Scenario: Deleting a work package via the action menu
When I go to the page of the work package "wp1"
And I select "Delete" from the action menu
And I confirm popups
Then I should be on the bulk destroy page of work packages
When I choose "Reassign"
And I fill in the id of work package "wp2" into "work package"
And I submit the form by the "Apply" button
Then I should be on the work packages index page of the project called "ecookbook"
When I go to the page of the work package "wp2"
Then the work package should be shown with the following values:
| Spent time | 10.00 hours |

@ -27,13 +27,15 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
module Redmine module OpenProject
module AccessKeys module AccessKeys
ACCESSKEYS = {:edit => '3', ACCESSKEYS = {:preview => '1',
:preview => '1', :new_issue => '2',
:edit => '3',
:quick_search => '4', :quick_search => '4',
:project_search => '5',
:help => '6', :help => '6',
:new_issue => '2' :more_menu => '7'
}.freeze unless const_defined?(:ACCESSKEYS) }.freeze unless const_defined?(:ACCESSKEYS)
def self.key_for(action) def self.key_for(action)

@ -27,13 +27,13 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
module Redmine module OpenProject
module Info module Info
class << self class << self
def app_name; Setting.software_name end def app_name; Setting.software_name end
def url; Setting.software_url end def url; Setting.software_url end
def help_url def help_url
"https://www.openproject.org/projects/support" "https://www.openproject.org/projects/openproject/wiki/Support"
end end
def versioned_name; "#{app_name} #{Redmine::VERSION.to_semver}" end def versioned_name; "#{app_name} #{Redmine::VERSION.to_semver}" end

@ -142,7 +142,7 @@ module Pagination::Controller
controller.pagination.delete(last_action) controller.pagination.delete(last_action)
# remove old # remove old
controller.send(:remove_method, last_action) if controller.respond_to? :last_action controller.send(:remove_method, last_action) if controller.respond_to? last_action
end end
def define_action!(block = default_block) def define_action!(block = default_block)

@ -109,7 +109,7 @@ Redmine::AccessControl.map do |map|
:work_packages => [:new, :new_type, :preview, :create] } :work_packages => [:new, :new_type, :preview, :create] }
map.permission :move_work_packages, {:'work_packages/moves' => [:new, :create]}, :require => :loggedin map.permission :move_work_packages, {:'work_packages/moves' => [:new, :create]}, :require => :loggedin
map.permission :edit_work_packages, { :issues => [:edit, :update, :update_form], map.permission :edit_work_packages, { :issues => [:edit, :update, :update_form],
:work_package_bulk => [:edit, :update], :'work_packages/bulk' => [:edit, :update],
:work_packages => [:edit, :update, :new_type, :preview, :quoted], :work_packages => [:edit, :update, :new_type, :preview, :quoted],
:journals => :preview, :journals => :preview,
:planning_elements => [:new, :create, :edit, :update], :planning_elements => [:new, :create, :edit, :update],
@ -119,6 +119,7 @@ Redmine::AccessControl.map do |map|
map.permission :edit_own_work_package_notes, {:journals => [:edit, :update]}, :require => :loggedin map.permission :edit_own_work_package_notes, {:journals => [:edit, :update]}, :require => :loggedin
map.permission :delete_work_packages, {:issues => :destroy, map.permission :delete_work_packages, {:issues => :destroy,
:work_packages => :destroy, :work_packages => :destroy,
:'work_packages/bulk' => :destroy,
:planning_elements => [:confirm_destroy, :planning_elements => [:confirm_destroy,
:destroy, :destroy,
:destroy_all, :destroy_all,
@ -227,7 +228,7 @@ Redmine::MenuManager.map :top_menu do |menu|
menu.push :my_page, { :controller => '/my', :action => 'page' }, :if => Proc.new { User.current.logged? } menu.push :my_page, { :controller => '/my', :action => 'page' }, :if => Proc.new { User.current.logged? }
# projects menu will be added by Redmine::MenuManager::TopMenuHelper#render_projects_top_menu_node # projects menu will be added by Redmine::MenuManager::TopMenuHelper#render_projects_top_menu_node
menu.push :administration, { :controller => '/admin', :action => 'projects' }, :if => Proc.new { User.current.admin? }, :last => true menu.push :administration, { :controller => '/admin', :action => 'projects' }, :if => Proc.new { User.current.admin? }, :last => true
menu.push :help, Redmine::Info.help_url, :last => true, :caption => "?", :html => { :accesskey => Redmine::AccessKeys.key_for(:help) } menu.push :help, OpenProject::Info.help_url, :last => true, :caption => "?", :html => { :accesskey => OpenProject::AccessKeys.key_for(:help) }
end end
Redmine::MenuManager.map :account_menu do |menu| Redmine::MenuManager.map :account_menu do |menu|
@ -282,7 +283,7 @@ Redmine::MenuManager.map :project_menu do |menu|
menu.push :work_packages, { :controller => '/work_packages', :action => 'index' }, :param => :project_id, :caption => :label_work_package_plural menu.push :work_packages, { :controller => '/work_packages', :action => 'index' }, :param => :project_id, :caption => :label_work_package_plural
menu.push :new_work_package, { :controller => '/work_packages', :action => 'new'}, :param => :project_id, :caption => :label_work_package_new, :parent => :work_packages, menu.push :new_work_package, { :controller => '/work_packages', :action => 'new'}, :param => :project_id, :caption => :label_work_package_new, :parent => :work_packages,
:html => { :accesskey => Redmine::AccessKeys.key_for(:new_work_package) } :html => { :accesskey => OpenProject::AccessKeys.key_for(:new_work_package) }
menu.push :summary_field, {:controller => '/issues/reports', :action => 'report'}, :param => :project_id, :caption => :label_workflow_summary, :parent => :work_packages menu.push :summary_field, {:controller => '/issues/reports', :action => 'report'}, :param => :project_id, :caption => :label_workflow_summary, :parent => :work_packages
menu.push :calendar, { :controller => '/work_packages/calendars', :action => 'index' }, :param => :project_id, :caption => :label_calendar menu.push :calendar, { :controller => '/work_packages/calendars', :action => 'index' }, :param => :project_id, :caption => :label_calendar
menu.push :news, { :controller => '/news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural menu.push :news, { :controller => '/news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural

@ -45,9 +45,11 @@ module Redmine::MenuManager::TopMenuHelper
return "" if User.current.number_of_known_projects.zero? return "" if User.current.number_of_known_projects.zero?
heading = link_to l(:label_project_plural), { :controller => '/projects', heading = link_to l(:label_project_plural),
:action => 'index' }, { :controller => '/projects',
:title => l(:label_project_plural) :action => 'index' },
:title => l(:label_project_plural),
:access_key => OpenProject::AccessKeys.key_for(:project_search)
if User.current.impaired? if User.current.impaired?
content_tag :li do content_tag :li do

@ -0,0 +1,55 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require_relative '../../db/migrate/migration_utils/timelines'
namespace :migrations do
namespace :timelines do
desc "Sets all timelines with historical comparison from 'historical' to 'none'"
task :remove_timelines_historical_comparison_from_options => :environment do |task|
setter = TimelinesHistoricalComparisonSetter.new
setter.remove_timelines_historical_comparison_from_options
end
private
class TimelinesHistoricalComparisonSetter < ActiveRecord::Migration
include Migration::Utils
def remove_timelines_historical_comparison_from_options
say_with_time_silently "Set historical comparison to none for all timelines" do
update_column_values('timelines',
['options'],
update_options(set_historical_comparison_to_none),
historical_comparison_filter)
end
end
private
def set_historical_comparison_to_none
Proc.new do |timelines_opts|
timelines_opts['comparison'] = 'none'
timelines_opts
end
end
def historical_comparison_filter
"options LIKE '%comparison: historical%'"
end
end
end
end

@ -3,4 +3,4 @@
ENV["RAILS_ENV"] ||= "production" ENV["RAILS_ENV"] ||= "production"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
puts puts
puts Redmine::Info.environment puts OpenProject::Info.environment

@ -1,122 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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 Api::V2::PlanningElementStatusesController do
let(:current_user) { FactoryGirl.create(:admin) }
before do
User.stub(:current).and_return current_user
end
describe 'index.xml' do
def fetch
get 'index', :format => 'xml'
end
it_should_behave_like "a controller action with unrestricted access"
describe 'with no planning element statuses available' do
it 'assigns an empty planning_element_statuses array' do
get 'index', :format => 'xml'
assigns(:planning_element_statuses).should == []
end
it 'renders the index builder template' do
get 'index', :format => 'xml'
response.should render_template('planning_element_statuses/index', :formats => ["api"])
end
end
describe 'with 3 planning element statuses available' do
before do
@created_planning_element_statuses = [
FactoryGirl.create(:planning_element_status),
FactoryGirl.create(:planning_element_status),
FactoryGirl.create(:planning_element_status)
]
end
it 'assigns an array with all planning element statuses' do
get 'index', :format => 'xml'
assigns(:planning_element_statuses).should == @created_planning_element_statuses
end
it 'renders the index template' do
get 'index', :format => 'xml'
response.should render_template('planning_element_statuses/index', :formats => ["api"])
end
end
end
describe 'show.xml' do
describe 'with unknown planning element status' do
if false # would like to write it this way
it 'returns status code 404' do
get 'show', :id => '1337', :format => 'xml'
response.status.should == '404 Not Found'
end
it 'returns an empty body' do
get 'show', :id => '1337', :format => 'xml'
response.body.should be_empty
end
else # but have to write it that way
it 'raises ActiveRecord::RecordNotFound errors' do
lambda do
get 'show', :id => '1337', :format => 'xml'
end.should raise_error(ActiveRecord::RecordNotFound)
end
end
end
describe 'with an available planning element status' do
before do
@available_planning_element_status = FactoryGirl.create(:planning_element_status, :id => '1337')
end
def fetch
get 'show', :id => '1337', :format => 'xml'
end
it_should_behave_like "a controller action with unrestricted access"
it 'assigns the available planning element status' do
get 'show', :id => '1337', :format => 'xml'
assigns(:planning_element_status).should == @available_planning_element_status
end
it 'renders the show template' do
get 'show', :id => '1337', :format => 'xml'
response.should render_template('planning_element_statuses/show', :formats => ["api"])
end
end
end
end

@ -85,13 +85,43 @@ describe StatusesController do
describe :edit do describe :edit do
let(:template) { 'edit' } let(:template) { 'edit' }
before do context :default do
status let!(:status_default) { FactoryGirl.create(:status,
is_default: true) }
before { get :edit, id: status_default.id }
it_behaves_like :response
describe :view do
render_views
get :edit, id: status.id it do
assert_no_tag tag: 'p',
content: Status.human_attribute_name(:is_default)
end
end
end
context 'no_default' do
before do
status
get :edit, id: status.id
end
it_behaves_like :response
describe :view do
render_views
it do
assert_tag tag: 'p',
content: Status.human_attribute_name(:is_default)
end
end
end end
it_behaves_like :response
end end
describe :update do describe :update do

@ -28,7 +28,7 @@
require 'spec_helper' require 'spec_helper'
describe WorkPackageBulkController do describe WorkPackages::BulkController do
let(:user) { FactoryGirl.create(:user) } let(:user) { FactoryGirl.create(:user) }
let(:custom_field_value) { '125' } let(:custom_field_value) { '125' }
let(:custom_field_1) { FactoryGirl.create(:work_package_custom_field, let(:custom_field_1) { FactoryGirl.create(:work_package_custom_field,
@ -76,6 +76,8 @@ describe WorkPackageBulkController do
custom_field_values: { custom_field_1.id => custom_field_value }, custom_field_values: { custom_field_1.id => custom_field_value },
project: project_2) } project: project_2) }
let(:stub_work_package) { FactoryGirl.build_stubbed(:work_package) }
before do before do
custom_field_1 custom_field_1
member_1 member_1
@ -177,12 +179,10 @@ describe WorkPackageBulkController do
it { should be_redirect } it { should be_redirect }
it { should redirect_to(controller: 'work_packages', it { should redirect_to(project_work_packages_path(project_1)) }
action: :index,
project_id: project_1.identifier) }
end end
end end
shared_context :update_request do shared_context :update_request do
before do before do
put :update, put :update,
@ -409,4 +409,47 @@ describe WorkPackageBulkController do
end end
end end
end end
describe :destroy do
let(:params) { { "ids" => "1", "to_do" => "blubs" } }
before do
controller.should_receive(:find_work_packages) do
controller.instance_variable_set(:@work_packages, [stub_work_package])
end
controller.should_receive(:authorize)
end
describe 'w/ the cleanup beeing successful' do
before do
stub_work_package.should_receive(:reload).and_return(stub_work_package)
stub_work_package.should_receive(:destroy)
WorkPackage.should_receive(:cleanup_associated_before_destructing_if_required).with([stub_work_package], user, params["to_do"]).and_return true
as_logged_in_user(user) do
delete :destroy, params
end
end
it 'should redirect to the project' do
response.should redirect_to(project_work_packages_path(stub_work_package.project))
end
end
describe 'w/o the cleanup beeing successful' do
before do
WorkPackage.should_receive(:cleanup_associated_before_destructing_if_required).with([stub_work_package], user, params["to_do"]).and_return false
as_logged_in_user(user) do
delete :destroy, params
end
end
it 'should redirect to the project' do
response.should render_template('destroy')
end
end
end
end end

@ -89,7 +89,7 @@ describe WorkPackages::ContextMenusController do
end end
shared_examples_for :bulk_edit do shared_examples_for :bulk_edit do
let(:edit_link) { "/work_package_bulk/edit?#{ids_link}" } let(:edit_link) { "/work_packages/bulk/edit?#{ids_link}" }
it_behaves_like :edit_impl it_behaves_like :edit_impl
end end
@ -126,7 +126,7 @@ describe WorkPackages::ContextMenusController do
get :index, ids: ids get :index, ids: ids
end end
let(:status_link) { "/work_package_bulk/update?#{ids_link}"\ let(:status_link) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bstatus_id%5D=#{status_2.id}" } "&amp;work_package%5Bstatus_id%5D=#{status_2.id}" }
it do it do
@ -139,7 +139,7 @@ describe WorkPackages::ContextMenusController do
shared_examples_for :priority do shared_examples_for :priority do
let(:priority_immediate) { FactoryGirl.create(:priority_immediate) } let(:priority_immediate) { FactoryGirl.create(:priority_immediate) }
let(:priority_link) { "/work_package_bulk/update?#{ids_link}"\ let(:priority_link) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bpriority_id%5D=#{priority_immediate.id}" } "&amp;work_package%5Bpriority_id%5D=#{priority_immediate.id}" }
before do before do
@ -161,9 +161,9 @@ describe WorkPackages::ContextMenusController do
project: project_1) } project: project_1) }
let(:version_2) { FactoryGirl.create(:version, let(:version_2) { FactoryGirl.create(:version,
project: project_1) } project: project_1) }
let(:version_link_1) { "/work_package_bulk/update?#{ids_link}"\ let(:version_link_1) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bfixed_version_id%5D=#{version_1.id}" } "&amp;work_package%5Bfixed_version_id%5D=#{version_1.id}" }
let(:version_link_2) { "/work_package_bulk/update?#{ids_link}"\ let(:version_link_2) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bfixed_version_id%5D=#{version_2.id}" } "&amp;work_package%5Bfixed_version_id%5D=#{version_2.id}" }
before do before do
@ -182,7 +182,7 @@ describe WorkPackages::ContextMenusController do
end end
shared_examples_for :assigned_to do shared_examples_for :assigned_to do
let(:assigned_to_link) { "/work_package_bulk/update?#{ids_link}"\ let(:assigned_to_link) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bassigned_to_id%5D=#{user.id}" } "&amp;work_package%5Bassigned_to_id%5D=#{user.id}" }
before { get :index, ids: ids } before { get :index, ids: ids }
@ -212,7 +212,7 @@ describe WorkPackages::ContextMenusController do
shared_examples_for :copy do shared_examples_for :copy do
let(:copy_link) { "/work_packages/move/new?copy_options%5Bcopy%5D=t&amp;"\ let(:copy_link) { "/work_packages/move/new?copy_options%5Bcopy%5D=t&amp;"\
"#{ids_link}" } "#{ids_link}" }
before { get :index, ids: ids } before { get :index, ids: ids }
it do it do
@ -235,7 +235,7 @@ describe WorkPackages::ContextMenusController do
end end
shared_examples_for :delete do shared_examples_for :delete do
let(:delete_link) { "/work_packages?#{ids_link}" } let(:delete_link) { "/work_packages/bulk?#{ids_link}" }
before { get :index, ids: ids } before { get :index, ids: ids }

@ -39,7 +39,7 @@ FactoryGirl.define do
visible true visible true
field_format "bool" field_format "bool"
factory :user_custom_field do factory :user_custom_field, :class => UserCustomField do
sequence(:name) { |n| "User Custom Field #{n}" } sequence(:name) { |n| "User Custom Field #{n}" }
type "UserCustomField" type "UserCustomField"
@ -80,7 +80,7 @@ FactoryGirl.define do
end end
end end
factory :issue_custom_field do factory :issue_custom_field, :class => WorkPackageCustomField do
sequence(:name) { |n| "Issue Custom Field #{n}" } sequence(:name) { |n| "Issue Custom Field #{n}" }
type "WorkPackageCustomField" type "WorkPackageCustomField"

@ -57,7 +57,7 @@ describe ApplicationHelper do
OpenProject::Footer.content = nil OpenProject::Footer.content = nil
end end
it { footer_content.should == I18n.t(:text_powered_by, :link => link_to(Redmine::Info.app_name, Redmine::Info.url)) } it { footer_content.should == I18n.t(:text_powered_by, :link => link_to(OpenProject::Info.app_name, OpenProject::Info.url)) }
end end
context "string as additional footer content" do context "string as additional footer content" do
@ -66,7 +66,7 @@ describe ApplicationHelper do
OpenProject::Footer.add_content("openproject","footer") OpenProject::Footer.add_content("openproject","footer")
end end
it { footer_content.include?(I18n.t(:text_powered_by, :link => link_to(Redmine::Info.app_name, Redmine::Info.url))).should be_true } it { footer_content.include?(I18n.t(:text_powered_by, :link => link_to(OpenProject::Info.app_name, OpenProject::Info.url))).should be_true }
it { footer_content.include?("<span class=\"footer_openproject\">footer</span>").should be_true } it { footer_content.include?("<span class=\"footer_openproject\">footer</span>").should be_true }
end end

@ -0,0 +1,289 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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 'spec_helper'
describe WorkPackage do
let(:work_package) { FactoryGirl.create(:work_package, :project => project,
:status => status) }
let(:work_package2) { FactoryGirl.create(:work_package, :project => project2,
:status => status) }
let(:user) { FactoryGirl.create(:user) }
let(:type) { FactoryGirl.create(:type_standard) }
let(:project) { FactoryGirl.create(:project, types: [type]) }
let(:project2) { FactoryGirl.create(:project, types: [type]) }
let(:role) { FactoryGirl.create(:role) }
let(:role2) { FactoryGirl.create(:role) }
let(:member) { FactoryGirl.create(:member, :principal => user,
:roles => [role]) }
let(:member2) { FactoryGirl.create(:member, :principal => user,
:roles => [role2],
:project => work_package2.project) }
let(:status) { FactoryGirl.create(:status) }
let(:priority) { FactoryGirl.create(:priority) }
let(:time_entry) { FactoryGirl.build(:time_entry, :work_package => work_package,
:project => work_package.project) }
let(:time_entry2) { FactoryGirl.build(:time_entry, :work_package => work_package2,
:project => work_package2.project) }
describe :cleanup_action_required_before_destructing? do
describe 'w/ the work package having a time entry' do
before do
work_package
time_entry.save!
end
it "should be true" do
WorkPackage.cleanup_action_required_before_destructing?(work_package).should be_true
end
end
describe 'w/ two work packages having a time entry' do
before do
work_package
time_entry.save!
time_entry2.save!
end
it "should be true" do
WorkPackage.cleanup_action_required_before_destructing?([work_package, work_package2]).should be_true
end
end
describe 'w/o the work package having a time entry' do
before do
work_package
end
it "should be false" do
WorkPackage.cleanup_action_required_before_destructing?(work_package).should be_false
end
end
end
describe :associated_classes_to_address_before_destructing? do
describe 'w/ the work package having a time entry' do
before do
work_package
time_entry.save!
end
it "should be have 'TimeEntry' as class to address" do
WorkPackage.associated_classes_to_address_before_destruction_of(work_package).should == [TimeEntry]
end
end
describe 'w/o the work package having a time entry' do
before do
work_package
end
it "should be empty" do
WorkPackage.associated_classes_to_address_before_destruction_of(work_package).should be_empty
end
end
end
describe :cleanup_associated_before_destructing_if_required do
before do
work_package.save!
time_entry.hours = 10
time_entry.save!
end
describe 'w/o a cleanup beeing necessary' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'reassign') }
before do
time_entry.destroy
end
it 'should return true' do
action.should be_true
end
end
describe 'w/ "destroy" as action' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'destroy') }
it 'should return true' do
action.should be_true
end
it 'should not touch the time_entry' do
action
time_entry.reload
time_entry.work_package_id.should == work_package.id
end
end
describe 'w/o an action' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user) }
it 'should return true' do
action.should be_true
end
it 'should not touch the time_entry' do
action
time_entry.reload
time_entry.work_package_id.should == work_package.id
end
end
describe 'w/ "nullify" as action' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'nullify') }
it 'should return true' do
action.should be_true
end
it 'should set the work_package_id of all time entries to nil' do
action
time_entry.reload
time_entry.work_package_id.should be_nil
end
end
describe 'w/ "reassign" as action
w/ reassigning to a valid work_package' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'reassign', :reassign_to_id => work_package2.id) }
before do
work_package2.save!
role2.permissions << :edit_time_entries
role2.save!
member2.save!
end
it 'should return true' do
action.should be_true
end
it 'should set the work_package_id of all time entries to the new work package' do
action
time_entry.reload
time_entry.work_package_id.should == work_package2.id
end
it "should set the project_id of all time entries to the new work package's project" do
action
time_entry.reload
time_entry.project_id.should == work_package2.project_id
end
end
describe 'w/ "reassign" as action
w/ reassigning to a work_package the user is not allowed to see' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'reassign', :reassign_to_id => work_package2.id) }
before do
work_package2.save!
end
it 'should return true' do
action.should be_false
end
it 'should not alter the work_package_id of all time entries' do
action
time_entry.reload
time_entry.work_package_id.should == work_package.id
end
end
describe 'w/ "reassign" as action
w/ reassigning to a non existing work package' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'reassign', :reassign_to_id => 0) }
it 'should return true' do
action.should be_false
end
it 'should not alter the work_package_id of all time entries' do
action
time_entry.reload
time_entry.work_package_id.should == work_package.id
end
it 'should set an error on work packages' do
action
work_package.errors.get(:base).should == [I18n.t(:'activerecord.errors.models.work_package.is_not_a_valid_target_for_time_entries', id: 0)]
end
end
describe 'w/ "reassign" as action
w/o providing a reassignment id' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'reassign') }
it 'should return true' do
action.should be_false
end
it 'should not alter the work_package_id of all time entries' do
action
time_entry.reload
time_entry.work_package_id.should == work_package.id
end
it 'should set an error on work packages' do
action
work_package.errors.get(:base).should == [I18n.t(:'activerecord.errors.models.work_package.is_not_a_valid_target_for_time_entries', id: nil)]
end
end
describe 'w/ an invalid option' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, :action => 'bogus') }
it 'should return false' do
action.should be_false
end
end
describe 'w/ nil as invalid option' do
let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, nil) }
it 'should return false' do
action.should be_false
end
end
end
end

@ -1312,4 +1312,35 @@ describe WorkPackage do
end end
end end
end end
describe 'custom fields' do
it 'should not duplicate error messages when invalid' do
cf1 = FactoryGirl.create(:work_package_custom_field, :is_required => true)
cf2 = FactoryGirl.create(:work_package_custom_field, :is_required => true)
# create work_package with one required custom field
work_package = FactoryGirl.create :work_package
work_package.project.work_package_custom_fields << cf1
work_package.type.custom_fields << cf1
# set that custom field with a value, should be fine
work_package.custom_field_values = {cf1.id => 'test'}
work_package.save!; work_package.reload
# is it fine?
expect(work_package).to be_valid
# now give the work_package another required custom field, but don't assign a value
work_package.project.work_package_custom_fields << cf2
work_package.type.custom_fields << cf2
work_package.custom_field_values # #custom_field_values needs to be touched
# that should not be valid
expect(work_package).to_not be_valid
# assert that there is only one error
expect(work_package.errors.size).to eq 1
expect(work_package.errors_on(:custom_values).size).to eq 1
end
end
end end

@ -26,10 +26,11 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
FactoryGirl.define do require 'spec_helper'
factory(:planning_element_status, :class => PlanningElementStatus) do require_relative '../support/permission_specs'
sequence(:name) { |n| "Planning Element Status No. #{n}" }
sequence(:position) { |n| n } describe WorkPackages::BulkController, "delete_work_packages permission", type: :controller do
end include PermissionSpecs
check_permission_required_for('work_packages/bulk#destroy', :delete_work_packages)
end end

@ -29,9 +29,9 @@
require 'spec_helper' require 'spec_helper'
require_relative '../support/permission_specs' require_relative '../support/permission_specs'
describe WorkPackageBulkController, "edit_work_packages permission", type: :controller do describe WorkPackages::BulkController, "edit_work_packages permission", type: :controller do
include PermissionSpecs include PermissionSpecs
check_permission_required_for('work_package_bulk#edit', :edit_work_packages) check_permission_required_for('work_packages/bulk#edit', :edit_work_packages)
check_permission_required_for('work_package_bulk#update', :edit_work_packages) check_permission_required_for('work_packages/bulk#update', :edit_work_packages)
end end

@ -28,15 +28,20 @@
require 'spec_helper' require 'spec_helper'
describe WorkPackageBulkController do describe WorkPackages::BulkController do
it "should connect GET /work_package_bulk/edit to work_package_bulk/edit" do it "should connect GET /work_packages/bulk/edit to work_package_bulk/edit" do
get("/work_package_bulk/edit").should route_to(controller: 'work_package_bulk', get("/work_packages/bulk/edit").should route_to(controller: 'work_packages/bulk',
action: 'edit') action: 'edit')
end end
it "should connect PUT /work_package_bulk/update to work_package_bulk#update" do it "should connect PUT /work_packages/bulk/update to work_package_bulk#update" do
put("/work_package_bulk/update").should route_to(controller: 'work_package_bulk', put("/work_packages/bulk").should route_to(controller: 'work_packages/bulk',
action: 'update') action: 'update')
end
it "should connect DELETE /work_packages/bulk to work_package_bulk#destroy" do
delete("/work_packages/bulk").should route_to(controller: 'work_packages/bulk',
action: 'destroy')
end end
end end

@ -1,70 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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 'api/v2/planning_element_statuses/_planning_element_status.api' do
before do
view.extend TimelinesHelper
end
# added to pass in locals
def render
params[:format] = 'xml'
super(:partial => 'api/v2/planning_element_statuses/planning_element_status.api', :object => planning_element_status)
end
describe 'with an assigned planning_element_status' do
let(:planning_element_status) { FactoryGirl.build(:planning_element_status,
:id => 1,
:name => 'Awesometastic Planning Element Status',
:position => 100) }
it 'renders a planning_element_status node' do
render
response.should have_selector('planning_element_status', :count => 1)
end
describe 'planning_element_status node' do
it 'contains an id element containing the planning element status id' do
render
response.should have_selector('planning_element_status id', :text => '1')
end
it 'contains a name element containing the planning element status name' do
render
response.should have_selector('planning_element_status name', :text => 'Awesometastic Planning Element Status')
end
it 'contains an position element containing the planning element status position' do
render
response.should have_selector('planning_element_status position', :text => '100')
end
end
end
end

@ -1,104 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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 'api/v2/planning_element_statuses/index.api.rsb' do
before do
view.extend TimelinesHelper
end
before do
params[:format] = 'xml'
end
describe 'with no planning element statuses available' do
it 'renders an empty planning_element_statuses document' do
assign(:planning_element_statuses, [])
render
response.should have_selector('planning_element_statuses', :count => 1)
response.should have_selector('planning_element_statuses[type=array][size="0"]') do
without_tag 'planning_element_status'
end
end
end
describe 'with 3 planning element statuses available' do
let(:planning_element_statuses) do
[
FactoryGirl.build(:planning_element_status),
FactoryGirl.build(:planning_element_status),
FactoryGirl.build(:planning_element_status)
]
end
it 'renders a planning_element_statuses document with the size 3 of type array' do
assign(:planning_element_statuses, planning_element_statuses)
render
response.should have_selector('planning_element_statuses', :count => 1)
response.should have_selector('planning_element_statuses[type=array][size="3"]')
end
it 'renders a planning_element_status for each assigned planning element' do
assign(:planning_element_statuses, planning_element_statuses)
render
response.should have_selector('planning_element_statuses planning_element_status', :count => 3)
end
it 'renders the _planning_element_status template for each assigned planning element status' do
assign(:planning_element_statuses, planning_element_statuses)
view.should_receive(:render).exactly(3).times.with(hash_including(:partial => '/api/v2/planning_element_statuses/planning_element_status.api')).and_return('')
# just to be able to call render despite the should_receive expectations above
view.should_receive(:render).once.with({:template=>"api/v2/planning_element_statuses/index", :handlers=>["rsb"], :formats=>["api"]}, {}).and_call_original
render
end
it 'passes the planning element statuses as local var to the partial' do
assign(:planning_element_statuses, planning_element_statuses)
view.should_receive(:render).once.with(hash_including(:object => planning_element_statuses.first)).and_return('')
view.should_receive(:render).once.with(hash_including(:object => planning_element_statuses.second)).and_return('')
view.should_receive(:render).once.with(hash_including(:object => planning_element_statuses.third)).and_return('')
# just to be able to call render despite the should_receive expectations above
view.should_receive(:render).once.with({:template=>"api/v2/planning_element_statuses/index", :handlers=>["rsb"], :formats=>["api"]}, {}).and_call_original
render
end
end
end

@ -1,74 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 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 'api/v2/planning_element_statuses/show.api.rsb' do
before do
view.extend TimelinesHelper
end
before do
params[:format] = 'xml'
end
describe 'with an assigned planning element status' do
let(:planning_element_status) { FactoryGirl.build(:planning_element_status) }
before do
assign(:planning_element_status, planning_element_status)
end
it 'renders a planning_element_status document' do
render
response.should have_selector('planning_element_status', :count => 1)
end
it 'renders the _planning_element_status template once' do
view.should_receive(:render).once.with(hash_including(:partial => '/api/v2/planning_element_statuses/planning_element_status.api')).and_return('')
# just to render the speced template despite the should receive expectations above
view.should_receive(:render).once.with({:template=>"api/v2/planning_element_statuses/show", :handlers=>["rsb"], :formats=>["api"]}, {}).and_call_original
render
end
it 'passes the planning element status as local var to the partial' do
view.should_receive(:render).once.with(hash_including(:object => planning_element_status)).and_return('')
# just to render the speced template despite the should receive expectations above
view.should_receive(:render).once.with({:template=>"api/v2/planning_element_statuses/show", :handlers=>["rsb"], :formats=>["api"]}, {}).and_call_original
render
end
end
end

@ -178,9 +178,9 @@ class Redmine::MenuManager::MapperTest < ActiveSupport::TestCase
test 'deleting all items' do test 'deleting all items' do
# Exposed by deleting :last items # Exposed by deleting :last items
Redmine::MenuManager.map :test_menu do |menu| Redmine::MenuManager.map :test_menu do |menu|
menu.push :not_last, Redmine::Info.help_url menu.push :not_last, OpenProject::Info.help_url
menu.push :administration, { :controller => 'projects', :action => 'show'}, {:last => true} menu.push :administration, { :controller => 'projects', :action => 'show'}, {:last => true}
menu.push :help, Redmine::Info.help_url, :last => true menu.push :help, OpenProject::Info.help_url, :last => true
end end
assert_nothing_raised do assert_nothing_raised do

Loading…
Cancel
Save