Merge remote-tracking branch 'opf/feature/rails3' into migration/prepare_schema_migration_table

Conflicts:
	doc/CHANGELOG.md
pull/457/head
Martin Czuchra 11 years ago
commit 7ed41c47d5
  1. 3
      Gemfile
  2. 6
      Gemfile.lock
  3. 15
      app/assets/javascripts/application.js
  4. 2
      app/assets/javascripts/context_menu.js
  5. 17
      app/assets/javascripts/modal.js
  6. 218
      app/assets/javascripts/timelines.js
  7. 2
      app/assets/javascripts/timelines_modal.js
  8. 10
      app/assets/javascripts/timelines_select_boxes.js
  9. 37
      app/controllers/api/v2/planning_elements_controller.rb
  10. 81
      app/controllers/api/v2/statuses_controller.rb
  11. 22
      app/controllers/work_packages/context_menus_controller.rb
  12. 12
      app/models/issue_status.rb
  13. 3
      app/models/journal/attachable_journal.rb
  14. 20
      app/models/journal/attachment_journal.rb
  15. 57
      app/models/journal/base_journal.rb
  16. 18
      app/models/journal/changeset_journal.rb
  17. 3
      app/models/journal/customizable_journal.rb
  18. 18
      app/models/journal/message_journal.rb
  19. 16
      app/models/journal/news_journal.rb
  20. 20
      app/models/journal/time_entry_journal.rb
  21. 13
      app/models/journal/wiki_content_journal.rb
  22. 29
      app/models/journal/work_package_journal.rb
  23. 16
      app/models/query.rb
  24. 26
      app/models/timeline.rb
  25. 22
      app/models/type.rb
  26. 118
      app/models/work_package.rb
  27. 19
      app/views/api/v2/statuses/_status.api.rsb
  28. 34
      app/views/api/v2/statuses/index.api.rsb
  29. 30
      app/views/api/v2/statuses/show.api.rsb
  30. 3
      app/views/timelines/_timeline.html.erb
  31. 27
      app/views/timelines/filter/_planning_elements.html.erb
  32. 2
      app/views/versions/index.html.erb
  33. 46
      app/views/work_packages/context_menus/index.html.erb
  34. 6
      app/views/work_packages/index.html.erb
  35. 2
      app/views/work_packages/show.html.erb
  36. 4
      config/locales/de.yml
  37. 3
      config/locales/en.yml
  38. 9
      config/routes.rb
  39. 8
      db/migrate/20130916094339_legacy_issues_to_work_packages.rb
  40. 63
      db/migrate/20130920081135_legacy_attachment_journal_data.rb
  41. 47
      db/migrate/20130920085055_legacy_changeset_journal_data.rb
  42. 47
      db/migrate/20130920090201_legacy_news_journal_data.rb
  43. 56
      db/migrate/20130920090641_legacy_message_journal_data.rb
  44. 56
      db/migrate/20130920092800_legacy_time_entry_journal_data.rb
  45. 90
      db/migrate/20130920093823_legacy_wiki_content_journal_data.rb
  46. 60
      db/migrate/20130920094524_legacy_issue_journal_data.rb
  47. 141
      db/migrate/20130920095747_legacy_planning_element_journal_data.rb
  48. 50
      db/migrate/20130920142714_update_attachment_container.rb
  49. 82
      db/migrate/20130920150143_journal_activities_data.rb
  50. 60
      db/migrate/migration_utils/db_worker.rb
  51. 146
      db/migrate/migration_utils/journal_migrator_concerns.rb
  52. 407
      db/migrate/migration_utils/legacy_journal_migrator.rb
  53. 39
      db/migrate/migration_utils/legacy_table_checker.rb
  54. 2
      db/migrate/migration_utils/utils.rb
  55. 31
      db/seeds/production.rb
  56. 78
      doc/CHANGELOG.md
  57. 209
      features/planning_elements/filter.feature
  58. 150
      features/step_definitions/api_steps.rb
  59. 6
      features/step_definitions/general_steps.rb
  60. 45
      features/step_definitions/timelines_then_steps.rb
  61. 62
      features/step_definitions/timelines_when_steps.rb
  62. 29
      features/step_definitions/web_steps.rb
  63. 7
      features/support/env.rb
  64. 4
      features/timelines/show.feature
  65. 19
      features/timelines/timeline_modal_views.feature
  66. 1
      features/timelines/timeline_view.feature
  67. 135
      features/timelines/timeline_view_with_filters.feature
  68. 48
      features/timelines/timeline_view_with_reporters.feature
  69. 2
      lib/open_project/version.rb
  70. 131
      spec/controllers/api/v2/statuses_controller_spec.rb
  71. 2
      spec/controllers/versions_controller_spec.rb
  72. 347
      spec/controllers/work_packages/context_menus_controller_spec.rb
  73. 2
      spec/factories/time_entry_factory.rb
  74. 5
      spec/models/query_spec.rb
  75. 42
      spec/routing/work_package/auto_completes_routing_spec.rb
  76. 43
      spec/routing/work_package/calendars_routing_spec.rb
  77. 37
      spec/routing/work_package/context_menus_routing_spec.rb
  78. 44
      spec/routing/work_package/previews_routing_spec.rb
  79. 33
      spec/routing/work_packages_spec.rb
  80. 2
      test/exemplars/journal_exemplar.rb
  81. 138
      test/functional/issues/context_menus_controller_test.rb
  82. 5
      test/integration/routing_test.rb
  83. 2
      test/unit/type_test.rb

@ -48,6 +48,8 @@ gem 'awesome_nested_set'
gem 'color-tools', '~> 1.3.0', :require => 'color' gem 'color-tools', '~> 1.3.0', :require => 'color'
gem "ruby-progressbar"
gem 'tinymce-rails' gem 'tinymce-rails'
gem 'tinymce-rails-langs' gem 'tinymce-rails-langs'
@ -114,6 +116,7 @@ group :test do
gem 'ruby-prof' gem 'ruby-prof'
gem 'simplecov', ">= 0.8.pre" gem 'simplecov', ">= 0.8.pre"
gem "shoulda-matchers" gem "shoulda-matchers"
gem "json_spec"
end end
group :ldap do group :ldap do

@ -165,6 +165,9 @@ GEM
jquery-rails jquery-rails
railties (>= 3.1.0) railties (>= 3.1.0)
json (1.8.0) json (1.8.0)
json_spec (1.1.1)
multi_json (~> 1.0)
rspec (~> 2.0)
launchy (2.3.0) launchy (2.3.0)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.0.0) letter_opener (1.0.0)
@ -278,6 +281,7 @@ GEM
rspec-mocks (~> 2.13.0) rspec-mocks (~> 2.13.0)
ruby-openid (2.2.3) ruby-openid (2.2.3)
ruby-prof (0.13.0) ruby-prof (0.13.0)
ruby-progressbar (1.2.0)
rubytree (0.8.3) rubytree (0.8.3)
json (>= 1.7.5) json (>= 1.7.5)
structured_warnings (>= 0.1.3) structured_warnings (>= 0.1.3)
@ -379,6 +383,7 @@ DEPENDENCIES
jquery-rails (~> 2.0.3) jquery-rails (~> 2.0.3)
jquery-ui-rails jquery-ui-rails
jruby-openssl jruby-openssl
json_spec
launchy (~> 2.3.0) launchy (~> 2.3.0)
letter_opener (~> 1.0.0) letter_opener (~> 1.0.0)
loofah loofah
@ -409,6 +414,7 @@ DEPENDENCIES
rspec-rails (~> 2.0) rspec-rails (~> 2.0)
ruby-openid (~> 2.2.3) ruby-openid (~> 2.2.3)
ruby-prof ruby-prof
ruby-progressbar
rubytree (~> 0.8.3) rubytree (~> 0.8.3)
sass-rails (~> 3.2.3) sass-rails (~> 3.2.3)
select2-rails (~> 3.3.2) select2-rails (~> 3.3.2)

@ -61,6 +61,7 @@
//= require issues //= require issues
//= require work_packages //= require work_packages
//= require settings //= require settings
//= require modal
//source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse //source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse
if (typeof []._reverse == 'undefined') { if (typeof []._reverse == 'undefined') {
@ -70,6 +71,20 @@ if (typeof []._reverse == 'undefined') {
} }
jQuery(document).ready(function ($) { jQuery(document).ready(function ($) {
//remove modal layout=false if we ctrl-click!
$(document.body).on("click", "a", function (e) {
if (top != self && e.ctrlKey) {
if (e.target && e.target.href) {
var url = e.target.href;
if (url.match(/(&)?layout=false/)) {
url = url.replace(/(&)?layout=false/g, "").replace(/\?$/, "");
window.open(url);
e.preventDefault();
}
}
}
});
if (typeof CS !== "undefined") { if (typeof CS !== "undefined") {
var regions = $.datepicker.regional; var regions = $.datepicker.regional;
var regional = regions[CS.lang] || regions[""]; var regional = regions[CS.lang] || regions[""];

@ -144,7 +144,7 @@ ContextMenu.prototype = {
{asynchronous:true, {asynchronous:true,
method: 'get', method: 'get',
evalScripts:true, evalScripts:true,
parameters:Form.serialize(Event.findElement(e, 'form')), parameters:jQuery(e.srcElement).closest("form").serialize(),
onComplete:function(request){ onComplete:function(request){
dims = $('context-menu').getDimensions(); dims = $('context-menu').getDimensions();
menu_width = dims.width; menu_width = dims.width;

@ -58,7 +58,6 @@ var ModalHelper = (function() {
/** replace all data-modal links and all inside modal links */ /** replace all data-modal links and all inside modal links */
body.on("click", "[data-modal]", modalFunction); body.on("click", "[data-modal]", modalFunction);
modalDiv.on("click", "a", modalFunction);
// 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));
@ -99,8 +98,18 @@ var ModalHelper = (function() {
modalDiv.data('changed', false); modalDiv.data('changed', false);
var document_host = document.location.href.split("/")[2];
body.on("click", "a", function (e) { body.on("click", "a", function (e) {
var url = jQuery(e.target).attr("href"); var url = jQuery(e.target).attr("href");
var data = this.href.split("/");
var link_host = data[2];
if (link_host && link_host != document_host) {
window.open(this.href);
return false;
}
if (url) { if (url) {
jQuery(e.target).attr("href", modalHelper.tweakLink(url)); jQuery(e.target).attr("href", modalHelper.tweakLink(url));
} }
@ -195,6 +204,11 @@ var ModalHelper = (function() {
* @param callback called when done. called with modal div. * @param callback called when done. called with modal div.
*/ */
ModalHelper.prototype.createModal = function(url, callback) { ModalHelper.prototype.createModal = function(url, callback) {
if (top != self) {
window.open(url.replace(/(&)?layout=false/g, ""));
return;
}
var modalHelper = this, modalDiv = this.modalDiv, counter = 0; var modalHelper = this, modalDiv = this.modalDiv, counter = 0;
url = this.tweakLink(url); url = this.tweakLink(url);
@ -238,3 +252,4 @@ var ModalHelper = (function() {
return ModalHelper; return ModalHelper;
})(); })();
var modalHelperInstance = new ModalHelper();

@ -290,8 +290,7 @@ Timeline = {
// prerequisites (3rd party libs) // prerequisites (3rd party libs)
this.checkPrerequisites(); this.checkPrerequisites();
this.modalHelper = modalHelperInstance;
this.modalHelper = new ModalHelper();
this.modalHelper.setupTimeline( this.modalHelper.setupTimeline(
this, this,
{ {
@ -305,26 +304,7 @@ Timeline = {
timeline.reload(); timeline.reload();
}); });
timelineLoader = new Timeline.TimelineLoader( timelineLoader = this.provideTimelineLoader()
this,
{
api_prefix : this.options.api_prefix,
url_prefix : this.options.url_prefix,
project_prefix : this.options.project_prefix,
planning_element_prefix : this.options.planning_element_prefix,
project_id : this.options.project_id,
project_types : this.options.project_types,
project_statuses : this.options.project_status,
project_responsibles : this.options.project_responsibles,
project_parents : this.options.parents,
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined),
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined),
ajax_defaults : this.ajax_defaults,
current_time : this.comparisonCurrentTime(),
target_time : this.comparisonTarget(),
include_planning_elements : this.verticalPlanningElementIds()
}
);
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);
@ -348,26 +328,8 @@ Timeline = {
}, },
reload: function() { reload: function() {
delete this.lefthandTree; delete this.lefthandTree;
var timelineLoader = new Timeline.TimelineLoader(
this, var timelineLoader = this.provideTimelineLoader()
{
api_prefix : this.options.api_prefix,
url_prefix : this.options.url_prefix,
project_prefix : this.options.project_prefix,
planning_element_prefix : this.options.planning_element_prefix,
project_id : this.options.project_id,
project_types : this.options.project_types,
project_statuses : this.options.project_status,
project_responsibles : this.options.project_responsibles,
project_parents : this.options.parents,
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined),
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined),
ajax_defaults : this.ajax_defaults,
current_time : this.comparisonCurrentTime(),
target_time : this.comparisonTarget(),
include_planning_elements : this.verticalPlanningElementIds()
}
);
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) { jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) {
@ -386,6 +348,31 @@ Timeline = {
timelineLoader.load(); timelineLoader.load();
}, },
provideTimelineLoader: function() {
return new Timeline.TimelineLoader(
this,
{
api_prefix : this.options.api_prefix,
url_prefix : this.options.url_prefix,
project_prefix : this.options.project_prefix,
planning_element_prefix : this.options.planning_element_prefix,
project_id : this.options.project_id,
project_types : this.options.project_types,
project_statuses : this.options.project_status,
project_responsibles : this.options.project_responsibles,
project_parents : this.options.parents,
planning_element_types : this.options.planning_element_types,
planning_element_responsibles : this.options.planning_element_responsibles,
planning_element_status : this.options.planning_element_status,
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined),
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined),
ajax_defaults : this.ajax_defaults,
current_time : this.comparisonCurrentTime(),
target_time : this.comparisonTarget(),
include_planning_elements : this.verticalPlanningElementIds()
}
);
},
defer: function(action, delay) { defer: function(action, delay) {
var timeline = this; var timeline = this;
var result; var result;
@ -432,6 +419,85 @@ Timeline = {
// ╭───────────────────────────────────────────────────────────────────╮ // ╭───────────────────────────────────────────────────────────────────╮
// │ Loading │ // │ Loading │
// ╰───────────────────────────────────────────────────────────────────╯ // ╰───────────────────────────────────────────────────────────────────╯
FilterQueryStringBuilder: (function() {
/**
* FilterQueryStringBuilder
*
* Simple serializer of query strings that satisfies OpenProject's filter
* API. Transforms hashes of desired filterings into the proper query strings.
*
* Example:
* fqsb = (new FilterQueryStringBuilder({
* 'type_id': [4, 5]
* })).build(
* '/api/v2/projects/sample_project/planning_elements.json'
* );
*
* => /api/v2/projects/sample_project/planning_elements.json?f[]=type_id&op[type_id]==&v[type_id][]=4&v[type_id][]=5
*
*/
var FilterQueryStringBuilder = function (filterHash) {
this.filterHash = filterHash;
};
FilterQueryStringBuilder.prototype.buildMetaDataForKey = function(key) {
this.queryStringParts.push(
{name: 'f[]', value: key},
{name: 'op[' + key + ']', value: '='}
);
};
FilterQueryStringBuilder.prototype.buildFilterDataForKeyAndValue = function(key, value) {
this.queryStringParts.push(
{name: 'v[' + key + '][]', value: value}
);
};
FilterQueryStringBuilder.prototype.buildFilterDataForKeyAndArrayOfValues = function(key, value) {
jQuery.each(value, jQuery.proxy( function(i, e) {
this.buildFilterDataForKeyAndValue(key, e);
}, this));
};
FilterQueryStringBuilder.prototype.buildFilterDataForValue = function(key, value) {
return value instanceof Array ?
this.buildFilterDataForKeyAndArrayOfValues(key, value) :
this.buildFilterDataForKeyAndValue(key, value);
};
FilterQueryStringBuilder.prototype.registerKeyAndValue = function(key, value) {
this.buildMetaDataForKey(key);
this.buildFilterDataForValue(key, value);
};
FilterQueryStringBuilder.prototype.buildQueryStringParts = function() {
this.queryStringParts = [];
jQuery.each(this.filterHash, jQuery.proxy(this.registerKeyAndValue, this));
};
FilterQueryStringBuilder.prototype.buildQueryStringFromQueryStringParts = function(url) {
return jQuery.map(this.queryStringParts, function(e, i) {
return e.name + "=" + encodeURIComponent(e.value);
}).join('&');
};
FilterQueryStringBuilder.prototype.buildUrlFromQueryStringParts = function(url) {
var resultUrl = url;
resultUrl += "?";
resultUrl += this.buildQueryStringFromQueryStringParts();
return resultUrl;
};
FilterQueryStringBuilder.prototype.build = function(url) {
this.buildQueryStringParts();
return this.buildUrlFromQueryStringParts(url);
};
return FilterQueryStringBuilder;
})(),
TimelineLoader : (function () { TimelineLoader : (function () {
/** /**
@ -1021,6 +1087,32 @@ Timeline = {
}); });
}; };
TimelineLoader.prototype.provideServerSideFilterHashTypes = function (hash) {
if (this.options.planning_element_types !== undefined) {
hash.type_id = this.options.planning_element_types;
}
};
TimelineLoader.prototype.provideServerSideFilterHashStatus = function (hash) {
if (this.options.planning_element_status !== undefined) {
hash.status_id = this.options.planning_element_status;
}
};
TimelineLoader.prototype.provideServerSideFilterHashResponsibles = function (hash) {
if (this.options.planning_element_responsibles !== undefined) {
hash.responsible_id = this.options.planning_element_responsibles;
}
};
TimelineLoader.prototype.provideServerSideFilterHash = function() {
var result = {};
this.provideServerSideFilterHashTypes(result);
this.provideServerSideFilterHashResponsibles(result);
this.provideServerSideFilterHashStatus(result);
return result;
};
TimelineLoader.prototype.registerPlanningElements = function (ids) { TimelineLoader.prototype.registerPlanningElements = function (ids) {
this.inChunks(ids, function (projectIdsOfPacket, i) { this.inChunks(ids, function (projectIdsOfPacket, i) {
@ -1030,26 +1122,31 @@ Timeline = {
"/" + "/" +
projectIdsOfPacket.join(','); projectIdsOfPacket.join(',');
var qsb = new Timeline.FilterQueryStringBuilder(
this.provideServerSideFilterHash());
var url = qsb.build(projectPrefix + '/planning_elements.json');
// load current planning elements. // load current planning elements.
this.loader.register( this.loader.register(
Timeline.PlanningElement.identifier + '_' + i, Timeline.PlanningElement.identifier + '_' + i,
{ url : projectPrefix + { url : url },
'/planning_elements.json?exclude=scenarios' +
this.comparisonCurrentUrlSuffix()},
{ storeIn: Timeline.PlanningElement.identifier } { storeIn: Timeline.PlanningElement.identifier }
); );
/* TODO!
// load historical planning elements. // load historical planning elements.
if (this.options.target_time) { if (this.options.target_time) {
this.loader.register( this.loader.register(
Timeline.HistoricalPlanningElement.identifier + '_' + i, Timeline.HistoricalPlanningElement.identifier + '_' + i,
{ url : projectPrefix + { url : projectPrefix +
'/planning_elements.json?exclude=scenarios' + '/planning_elements.json' +
this.comparisonTargetUrlSuffix() }, this.comparisonTargetUrlSuffix() },
{ storeIn: Timeline.HistoricalPlanningElement.identifier, { storeIn: Timeline.HistoricalPlanningElement.identifier,
readFrom: Timeline.PlanningElement.identifier } readFrom: Timeline.PlanningElement.identifier }
); );
} }
*/
}); });
}; };
@ -2085,12 +2182,8 @@ Timeline = {
return false; return false;
}, },
filteredOut: function() { filteredOut: function() {
var filtered = this.filteredOutForProjectFilter() || var filtered = this.filteredOutForProjectFilter();
this.filteredOutForPlanningElementTypes() ||
this.filteredOutForResponsibles();
this.filteredOut = function() { return filtered; }; this.filteredOut = function() { return filtered; };
return filtered; return filtered;
}, },
inTimeFrame: function () { inTimeFrame: function () {
@ -2099,18 +2192,6 @@ Timeline = {
filteredOutForProjectFilter: function() { filteredOutForProjectFilter: function() {
return this.project.filteredOut(); return this.project.filteredOut();
}, },
filteredOutForResponsibles: function() {
return Timeline.filterOutBasedOnArray(
this.timeline.options.planning_element_responsibles,
this.getResponsible()
);
},
filteredOutForPlanningElementTypes: function() {
return Timeline.filterOutBasedOnArray(
this.timeline.options.planning_element_types,
this.getPlanningElementType()
);
},
all: function(timeline) { all: function(timeline) {
// collect all planning elements // collect all planning elements
var r = timeline.planning_elements; var r = timeline.planning_elements;
@ -3605,7 +3686,7 @@ Timeline = {
getAvailableRows: function() { getAvailableRows: function() {
var timeline = this; var timeline = this;
return { return {
all: ['end_date', 'planning_element_types', 'project_status', 'project_type', 'responsible', 'start_date'], all: ['end_date', 'type', 'status', 'responsible', 'start_date'],
type: function (data, pet, pt) { type: function (data, pet, pt) {
var ptName, petName; var ptName, petName;
if (pt !== undefined) { if (pt !== undefined) {
@ -3622,8 +3703,13 @@ Timeline = {
return jQuery('<span class="tl-column">' + (ptName || petName || "-") + '</span>'); return jQuery('<span class="tl-column">' + (ptName || petName || "-") + '</span>');
}, },
project_status: function(data) { status: function(data) {
var status; var status;
if (data.planning_element_status) {
status = data.planning_element_status;
}
if (data.getProjectStatus instanceof Function) { if (data.getProjectStatus instanceof Function) {
status = data.getProjectStatus(); status = data.getProjectStatus();
} }
@ -4194,7 +4280,7 @@ Timeline = {
text = timeline.escape(data.name); text = timeline.escape(data.name);
if (data.getUrl instanceof Function) { if (data.getUrl instanceof Function) {
text = jQuery('<a href="' + data.getUrl() + '" class="tl-discreet-link" target="_blank" data-modal/>').append(text).attr("title", text); text = jQuery('<a href="' + data.getUrl() + '" class="tl-discreet-link" data-modal/>').append(text).attr("title", text);
} }
if (data.is(Timeline.Project)) { if (data.is(Timeline.Project)) {

@ -57,4 +57,4 @@ ModalHelper.prototype.setupTimeline = function(timeline, options) {
} }
} }
}); });
}; };

@ -53,11 +53,17 @@ jQuery(document).ready(function($) {
$("#timeline_options_project_status"), $("#timeline_options_project_status"),
$("#timeline_options_project_types"), $("#timeline_options_project_types"),
$("#timeline_options_planning_element_responsibles"), $("#timeline_options_planning_element_responsibles"),
$("#timeline_options_grouping_two_selection")
].each(function (item) {
$(item).timelinesAutocomplete({ ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} });
});
[
$("#timeline_options_planning_element_types"), $("#timeline_options_planning_element_types"),
$("#timeline_options_planning_element_time_types"), $("#timeline_options_planning_element_time_types"),
$("#timeline_options_grouping_two_selection") $("#timeline_options_planning_element_status")
].each(function (item) { ].each(function (item) {
$(item).timelinesAutocomplete({ ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} }) $(item).timelinesAutocomplete({});
}); });
var item = $("#timeline_options_columns_"); var item = $("#timeline_options_columns_");

@ -51,6 +51,7 @@ module Api
def index def index
optimize_planning_elements_for_less_db_queries optimize_planning_elements_for_less_db_queries
rewire_ancestors
respond_to do |format| respond_to do |format|
format.api format.api
@ -169,6 +170,12 @@ module Api
# is called as a before filter and as a method # is called as a before filter and as a method
def assign_planning_elements(projects = (@projects || [@project])) def assign_planning_elements(projects = (@projects || [@project]))
@planning_elements = WorkPackage.for_projects(projects).without_deleted @planning_elements = WorkPackage.for_projects(projects).without_deleted
query = Query.new
query.add_filters(params[:f], params[:op], params[:v]) if params[:f]
@planning_elements = @planning_elements.with_query query
end end
# remove this and replace by calls it with calls # remove this and replace by calls it with calls
@ -203,7 +210,7 @@ module Api
return if @planning_elements.class == Array return if @planning_elements.class == Array
# triggering full load to avoid separate queries for count or related models # triggering full load to avoid separate queries for count or related models
@planning_elements = @planning_elements.all(:include => [:type, :project]) @planning_elements = @planning_elements.all(:include => [:type, :status, :project])
# Replacing association proxies with already loaded instances to avoid # Replacing association proxies with already loaded instances to avoid
# further db calls. # further db calls.
@ -242,6 +249,34 @@ module Api
pe.send(:association_instance_set, :children, children) pe.send(:association_instance_set, :children, children)
end end
end end
# Filtering work_packages can destroy the parent-child-relationships
# of work_packages. If parents are removed, the relationships need
# to be rewired to the first ancestor in the ancestor-chain.
#
# Before Filtering:
# A -> B -> C
# After Filtering:
# A -> C
#
# to see the respective cases that need to be handled properly by this rewiring,
# @see features/planning_elements/filter.feature
def rewire_ancestors
filtered_ids = @planning_elements.map(&:id)
@planning_elements.each do |pe|
# remove all children, that are not present in the filtered set
pe.children = pe.children.select {|child| filtered_ids.include? child.id} unless pe.children.empty?
# re-wire the parent of this pe to the first ancestor found in the filtered set
# re-wiring is only needed, when there is actually a parent, and the parent has been filtered out
if pe.parent_id && !filtered_ids.include?(pe.parent_id)
ancestors = @planning_elements.select{|candidate| candidate.lft < pe.lft && candidate.rgt > pe.rgt }
# the greatest lower boundary is the first ancestor not filtered
pe.parent = ancestors.sort_by{|ancestor| ancestor.lft }.last
end
end
end
end end
end end
end end

@ -0,0 +1,81 @@
#-- 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.
#++
# resolves either a given status (show) or returns a list of available statuses
# if the controller is called nested inside a project, it returns only the
# statuses that can be reached by the workflows of the project
module Api
module V2
class StatusesController < ApplicationController
include PaginationHelper
include ::Api::V2::ApiController
rescue_from ActiveRecord::RecordNotFound, with: lambda{render_404}
extend Pagination::Controller
paginate_model IssueStatus
unloadable
before_filter :require_login
before_filter :resolve_project
accept_key_auth :index, :show
def index
if @project
@statuses = Type.issue_statuses(@project.types.map(&:id))
else
visible_type_ids = Project.visible
.includes(:types)
.map(&:types).flatten
.map(&:id)
@statuses = Type.issue_statuses(visible_type_ids)
end
respond_to do |format|
format.api
end
end
def show
@status = IssueStatus.find(params[:id])
respond_to do |format|
format.api
end
end
protected
def resolve_project
@project = Project.find(params[:project_id]) if params[:project_id]
end
end
end
end

@ -27,34 +27,36 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Issues::ContextMenusController < ApplicationController class WorkPackages::ContextMenusController < ApplicationController
def issues def index
@issues = WorkPackage.visible.all(:conditions => {:id => params[:ids]}, :include => :project) @work_packages = WorkPackage.visible.all(order: "#{WorkPackage.table_name}.id",
conditions: {id: params[:ids]},
include: :project)
if (@issues.size == 1) if (@work_packages.size == 1)
@issue = @issues.first @work_package = @work_packages.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @allowed_statuses = @work_package.new_statuses_allowed_to(User.current)
else else
@allowed_statuses = @issues.map do |i| @allowed_statuses = @work_packages.map do |i|
i.new_statuses_allowed_to(User.current) i.new_statuses_allowed_to(User.current)
end.inject do |memo,s| end.inject do |memo,s|
memo & s memo & s
end end
end end
@projects = @issues.collect(&:project).compact.uniq @projects = @work_packages.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1 @project = @projects.first if @projects.size == 1
@can = {:edit => User.current.allowed_to?(:edit_work_packages, @projects), @can = {:edit => User.current.allowed_to?(:edit_work_packages, @projects),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)), :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (User.current.allowed_to?(:edit_work_packages, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)), :update => (User.current.allowed_to?(:edit_work_packages, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
:move => (@project && User.current.allowed_to?(:move_work_packages, @project)), :move => (@project && User.current.allowed_to?(:move_work_packages, @project)),
:copy => (@issue && @project.types.include?(@issue.type) && User.current.allowed_to?(:add_work_packages, @project)), :copy => (@work_package && @project.types.include?(@work_package.type) && User.current.allowed_to?(:add_work_packages, @project)),
:delete => User.current.allowed_to?(:delete_work_packages, @projects) :delete => User.current.allowed_to?(:delete_work_packages, @projects)
} }
if @project if @project
@assignables = @project.assignable_users @assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) @assignables << @work_package.assigned_to if @work_package && @work_package.assigned_to && !@assignables.include?(@work_package.assigned_to)
@types = @project.types @types = @project.types
else else
#when multiple projects, we only keep the intersection of each set #when multiple projects, we only keep the intersection of each set

@ -28,6 +28,8 @@
#++ #++
class IssueStatus < ActiveRecord::Base class IssueStatus < ActiveRecord::Base
extend Pagination::Model
before_destroy :check_integrity before_destroy :check_integrity
has_many :workflows, :foreign_key => "old_status_id" has_many :workflows, :foreign_key => "old_status_id"
acts_as_list acts_as_list
@ -41,6 +43,12 @@ class IssueStatus < ActiveRecord::Base
after_save :unmark_old_default_value, :if => :is_default? after_save :unmark_old_default_value, :if => :is_default?
scope :like, lambda { |q|
s = "%#{q.to_s.strip.downcase}%"
{ :conditions => ["LOWER(name) LIKE :s", {:s => s}],
:order => "name" }
}
def unmark_old_default_value def unmark_old_default_value
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id])
end end
@ -95,6 +103,10 @@ class IssueStatus < ActiveRecord::Base
end end
end end
def self.search_scope(query)
like(query)
end
def <=>(status) def <=>(status)
position <=> status.position position <=> status.position
end end

@ -27,9 +27,8 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::AttachableJournal < ActiveRecord::Base class Journal::AttachableJournal < Journal::BaseJournal
self.table_name = "attachable_journals" self.table_name = "attachable_journals"
belongs_to :journal
belongs_to :attachment belongs_to :attachment
end end

@ -27,24 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::AttachmentJournal < ActiveRecord::Base class Journal::AttachmentJournal < Journal::BaseJournal
self.table_name = "attachment_journals" self.table_name = "attachment_journals"
belongs_to :journal
@@journaled_attributes = [:container_id,
:container_type,
:filename,
:disk_filename,
:filesize,
:content_type,
:digest,
:downloads,
:author_id,
:description]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -0,0 +1,57 @@
#-- 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.
#++
class Journal::BaseJournal < ActiveRecord::Base
self.abstract_class = true
belongs_to :journal
def journaled_attributes
attributes.symbolize_keys.select{|k,_| self.class.journaled_attributes.include? k}
end
private
def self.journaled_attributes
@journaled_attributes ||= column_names.map{ |n| n.to_sym} - excluded_attributes
end
def self.column_names
db_columns(table_name).map(&:name)
end
def self.excluded_attributes
[primary_key.to_sym, inheritance_column.to_sym, :journal_id, :lock_version, :created_at, :root_id, :lft, :rgt]
end
def self.db_columns(table_name)
ActiveRecord::Base.connection.columns table_name
end
end

@ -27,22 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::ChangesetJournal < ActiveRecord::Base class Journal::ChangesetJournal < Journal::BaseJournal
self.table_name = "changeset_journals" self.table_name = "changeset_journals"
belongs_to :journal
@@journaled_attributes = [:repository_id,
:revision,
:commiter,
:commited_on,
:comments,
:commit_data,
:scmid,
:user_id]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -27,9 +27,8 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::CustomizableJournal < ActiveRecord::Base class Journal::CustomizableJournal < Journal::BaseJournal
self.table_name = "customizable_journals" self.table_name = "customizable_journals"
belongs_to :journal
belongs_to :custom_field, foreign_key: :custom_field_id belongs_to :custom_field, foreign_key: :custom_field_id
end end

@ -27,22 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::MessageJournal < ActiveRecord::Base class Journal::MessageJournal < Journal::BaseJournal
self.table_name = "message_journals" self.table_name = "message_journals"
belongs_to :journal
@@journaled_attributes = [:board_id,
:parent_id,
:subject,
:content,
:author_id,
:replies_count,
:last_reply_id,
:sticky]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -27,20 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::NewsJournal < ActiveRecord::Base class Journal::NewsJournal < Journal::BaseJournal
self.table_name = "news_journals" self.table_name = "news_journals"
belongs_to :journal
@@journaled_attributes = [:project_id,
:title,
:summary,
:description,
:author_id,
:comments_count]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -27,24 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::TimeEntryJournal < ActiveRecord::Base class Journal::TimeEntryJournal < Journal::BaseJournal
self.table_name = "time_entry_journals" self.table_name = "time_entry_journals"
belongs_to :journal
@@journaled_attributes = [:project_id,
:user_id,
:work_package_id,
:hours,
:comments,
:activity_id,
:spent_on,
:tyear,
:tmonth,
:tweek]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -27,17 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::WikiContentJournal < ActiveRecord::Base class Journal::WikiContentJournal < Journal::BaseJournal
self.table_name = "wiki_content_journals" self.table_name = "wiki_content_journals"
belongs_to :journal
@@journaled_attributes = [:page_id,
:author_id,
:text]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -27,33 +27,6 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
class Journal::WorkPackageJournal < ActiveRecord::Base class Journal::WorkPackageJournal < Journal::BaseJournal
self.table_name = "work_package_journals" self.table_name = "work_package_journals"
belongs_to :journal
@@journaled_attributes = [:type_id,
:project_id,
:subject,
:description,
:start_date,
:due_date,
:category_id,
:status_id,
:assigned_to_id,
:priority_id,
:fixed_version_id,
:author_id,
:done_ratio,
:estimated_hours,
:planning_element_status_comment,
:deleted_at,
:parent_id,
:responsible_id,
:planning_element_status_id]
def journaled_attributes
attributes.symbolize_keys.select{|k,_| @@journaled_attributes.include? k}
end
end end

@ -29,6 +29,8 @@
class Query < ActiveRecord::Base class Query < ActiveRecord::Base
@@user_filters = %w{assigned_to_id author_id watcher_id responsible_id}.freeze
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
serialize :filters serialize :filters
@ -86,6 +88,7 @@ class Query < ActiveRecord::Base
QueryColumn.new(:subject, :sortable => "#{WorkPackage.table_name}.subject"), QueryColumn.new(:subject, :sortable => "#{WorkPackage.table_name}.subject"),
QueryColumn.new(:author), QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true), QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
QueryColumn.new(:responsible, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
QueryColumn.new(:updated_at, :sortable => "#{WorkPackage.table_name}.updated_at", :default_order => 'desc'), QueryColumn.new(:updated_at, :sortable => "#{WorkPackage.table_name}.updated_at", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
@ -181,12 +184,15 @@ class Query < ActiveRecord::Base
author_values += user_values author_values += user_values
@available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty? @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
group_values = Group.all.collect {|g| [g.name, g.id.to_s] } group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
@available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values, :name => I18n.t('query_fields.member_of_group') } unless group_values.empty? @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values, :name => I18n.t('query_fields.member_of_group') } unless group_values.empty?
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
@available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values, :name => I18n.t('query_fields.assigned_to_role') } unless role_values.empty? @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values, :name => I18n.t('query_fields.assigned_to_role') } unless role_values.empty?
@available_filters["responsible_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
if User.current.logged? if User.current.logged?
# populate the watcher list with the same user list as other user filters if the user has the :view_work_package_watchers permission in at least one project # populate the watcher list with the same user list as other user filters if the user has the :view_work_package_watchers permission in at least one project
# TODO: this could be differentiated more, e.g. all users could watch issues in public projects, but won't necessarily be shown here # TODO: this could be differentiated more, e.g. all users could watch issues in public projects, but won't necessarily be shown here
@ -424,7 +430,7 @@ class Query < ActiveRecord::Base
operator = operator_for(field) operator = operator_for(field)
# "me" value subsitution # "me" value subsitution
if %w(assigned_to_id author_id watcher_id).include?(field) if @@user_filters.include? field
if v.delete("me") if v.delete("me")
if User.current.logged? if User.current.logged?
v.push(User.current.id.to_s) v.push(User.current.id.to_s)
@ -548,8 +554,12 @@ class Query < ActiveRecord::Base
sql = '' sql = ''
case operator case operator
when "=" when "="
if value.present? if value.present? then
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" if (value.include?("-1")) then
sql = "#{db_table}.#{db_field} IS NULL OR "
end
sql += "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
else else
# empty set of allowed values produces no result # empty set of allowed values produces no result
sql = "0=1" sql = "0=1"

@ -66,9 +66,9 @@ class Timeline < ActiveRecord::Base
"compare_to_relative", "compare_to_relative",
"compare_to_relative_unit", "compare_to_relative_unit",
"comparison", "comparison",
"exclude_empty",
"exclude_own_planning_elements", "exclude_own_planning_elements",
"exclude_reporters", "exclude_reporters",
"exclude_empty",
"exist", "exist",
"grouping_one_enabled", "grouping_one_enabled",
"grouping_one_selection", "grouping_one_selection",
@ -81,19 +81,20 @@ class Timeline < ActiveRecord::Base
"initial_outline_expansion", "initial_outline_expansion",
"parents", "parents",
"planning_element_responsibles", "planning_element_responsibles",
"planning_element_types", "planning_element_status",
"planning_element_time_types", "planning_element_time",
"planning_element_time_absolute_one", "planning_element_time_absolute_one",
"planning_element_time_absolute_two", "planning_element_time_absolute_two",
"planning_element_time_relative_one", "planning_element_time_relative_one",
"planning_element_time_relative_two",
"planning_element_time_relative_one_unit", "planning_element_time_relative_one_unit",
"planning_element_time_relative_two",
"planning_element_time_relative_two_unit", "planning_element_time_relative_two_unit",
"planning_element_time", "planning_element_time_types",
"planning_element_types",
"project_responsibles", "project_responsibles",
"project_sort",
"project_status", "project_status",
"project_types", "project_types",
"project_sort",
"timeframe_end", "timeframe_end",
"timeframe_start", "timeframe_start",
"vertical_planning_elements", "vertical_planning_elements",
@ -105,7 +106,7 @@ class Timeline < ActiveRecord::Base
"start_date", "start_date",
"end_date", "end_date",
"responsible", "responsible",
"project_status" "status"
] ]
@@available_zoom_factors = [ @@available_zoom_factors = [
@ -187,6 +188,17 @@ class Timeline < ActiveRecord::Base
Type.find(:all, :order => :name) Type.find(:all, :order => :name)
end end
def available_planning_element_status
types = Project.visible.includes(:types).map(&:types).flatten.uniq
types.map(&:issue_statuses).flatten.uniq
end
def selected_planning_element_status
resolve_with_none_element(:planning_element_status) do |ary|
IssueStatus.find(ary)
end
end
def selected_planning_element_types def selected_planning_element_types
resolve_with_none_element(:planning_element_types) do |ary| resolve_with_none_element(:planning_element_types) do |ary|
Type.find(ary) Type.find(ary)

@ -83,21 +83,17 @@ class Type < ActiveRecord::Base
find(:all, :order => 'position') find(:all, :order => 'position')
end end
# Returns an array of IssueStatus that are used def self.issue_statuses(types)
# in the type's workflows workflow_table, status_table = [Workflow, IssueStatus].map(&:arel_table)
def issue_statuses old_id_subselect, new_id_subselect = [:old_status_id, :new_status_id].map do |foreign_key|
if @issue_statuses workflow_table.project(workflow_table[foreign_key]).where(workflow_table[:type_id].in(types))
return @issue_statuses
elsif new_record?
return []
end end
IssueStatus.where(status_table[:id].in(old_id_subselect).or(status_table[:id].in(new_id_subselect)))
end
ids = Workflow. def issue_statuses
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE type_id = #{id}"). return [] if new_record?
flatten. @issue_statuses ||= Type.issue_statuses([id])
uniq
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
end end
def self.search_scope(query) def self.search_scope(query)

@ -626,6 +626,65 @@ class WorkPackage < ActiveRecord::Base
return done_date <= Date.today return done_date <= Date.today
end end
def move_to_project_without_transaction(new_project, new_type = nil, options = {})
options ||= {}
work_package = options[:copy] ? self.class.new.copy_from(self) : self
if new_project && work_package.project_id != new_project.id
delete_relations(work_package)
# work_package is moved to another project
# reassign to the category with same name if any
new_category = work_package.category.nil? ? nil : new_project.issue_categories.find_by_name(work_package.category.name)
work_package.category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(work_package.fixed_version)
work_package.fixed_version = nil
end
work_package.project = new_project
if !Setting.cross_project_work_package_relations? &&
parent && parent.project_id != project_id
self.parent_id = nil
end
end
if new_type
work_package.type = new_type
work_package.reset_custom_values!
end
# Allow bulk setting of attributes on the work_package
if options[:attributes]
# before setting the attributes, we need to remove the move-related fields
work_package.attributes = options[:attributes].except(:copy,:new_project_id, :new_type_id, :follow, :ids)
.reject { |key, value| value.blank? }
end # FIXME this eliminates the case, where values shall be bulk-assigned to null, but this needs to work together with the permit
if options[:copy]
work_package.author = User.current
work_package.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
work_package.status = if options[:attributes] && options[:attributes][:status_id]
IssueStatus.find_by_id(options[:attributes][:status_id])
else
self.status
end
end
if work_package.save
unless options[:copy]
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:work_package_id => id})
work_package.children.each do |child|
unless child.move_to_project_without_transaction(new_project)
# Move failed and transaction was rollback'd
return false
end
end
end
else
return false
end
work_package
end
protected protected
def recalculate_attributes_for(work_package_id) def recalculate_attributes_for(work_package_id)
@ -737,65 +796,6 @@ class WorkPackage < ActiveRecord::Base
projects projects
end end
def move_to_project_without_transaction(new_project, new_type = nil, options = {})
options ||= {}
work_package = options[:copy] ? self.class.new.copy_from(self) : self
if new_project && work_package.project_id != new_project.id
delete_relations(work_package)
# work_package is moved to another project
# reassign to the category with same name if any
new_category = work_package.category.nil? ? nil : new_project.issue_categories.find_by_name(work_package.category.name)
work_package.category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(work_package.fixed_version)
work_package.fixed_version = nil
end
work_package.project = new_project
if !Setting.cross_project_work_package_relations? &&
parent && parent.project_id != project_id
self.parent_id = nil
end
end
if new_type
work_package.type = new_type
work_package.reset_custom_values!
end
# Allow bulk setting of attributes on the work_package
if options[:attributes]
# before setting the attributes, we need to remove the move-related fields
work_package.attributes = options[:attributes].except(:copy,:new_project_id, :new_type_id, :follow, :ids)
.reject { |key, value| value.blank? }
end # FIXME this eliminates the case, where values shall be bulk-assigned to null, but this needs to work together with the permit
if options[:copy]
work_package.author = User.current
work_package.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
work_package.status = if options[:attributes] && options[:attributes][:status_id]
IssueStatus.find_by_id(options[:attributes][:status_id])
else
self.status
end
end
if work_package.save
unless options[:copy]
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:work_package_id => id})
work_package.children.each do |child|
unless child.move_to_project_without_transaction(new_project)
# Move failed and transaction was rollback'd
return false
end
end
end
else
return false
end
work_package
end
# Do not redefine alias chain on reload (see #4838) # Do not redefine alias chain on reload (see #4838)
alias_method_chain(:attributes=, :type_first) unless method_defined?(:attributes_without_type_first=) alias_method_chain(:attributes=, :type_first) unless method_defined?(:attributes_without_type_first=)

@ -1,4 +1,3 @@
#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)
@ -27,15 +26,13 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
# Merge those two once Issue == PlanningElement == WorkPackage api.status do
Then(/^I should (not )?see the planning element "(.*?)" in the timeline$/) do |negate, planning_element_name| api.id(status.id)
steps %Q{
Then I should #{negate}see "#{planning_element_name}" within ".timeline .tl-left-main" api.name(status.name)
}
end
Then(/^I should (not )?see the issue "(.*?)" in the timeline$/) do |negate, issue_name| api.position(status.position)
steps %Q{ api.is_default(status.is_default)
Then I should #{negate}see "#{issue_name}" within ".timeline .tl-left-main" api.is_closed(status.is_closed)
} api.default_done_ratio(status.default_done_ratio)
end end

@ -0,0 +1,34 @@
#-- 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.
#++
api.array :statuses, :size => @statuses.size do
@statuses.each do |status|
render(:partial => '/api/v2/statuses/status.api',
:object => status)
end
end

@ -0,0 +1,30 @@
#-- 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.
#++
render(:partial => '/api/v2/reported_project_statuses/status.api',
:object => @status)

@ -51,7 +51,7 @@ See doc/COPYRIGHT.rdoc for more details.
'timelines.filter.column.end_date', 'timelines.filter.column.end_date',
'timelines.filter.column.name', 'timelines.filter.column.name',
'timelines.filter.column.type', 'timelines.filter.column.type',
'timelines.filter.column.project_status', 'timelines.filter.column.status',
'timelines.filter.column.responsible', 'timelines.filter.column.responsible',
'timelines.filter.column.start_date', 'timelines.filter.column.start_date',
'timelines.filter.grouping_other', 'timelines.filter.grouping_other',
@ -104,7 +104,6 @@ See doc/COPYRIGHT.rdoc for more details.
<%= javascript_include_tag 'raphael-min.js', :plugin => 'chiliproject_timelines' %> <%= javascript_include_tag 'raphael-min.js', :plugin => 'chiliproject_timelines' %>
<%= javascript_include_tag 'Bitstream_Vera_Sans_400.font.js', :plugin => 'chiliproject_timelines' %> <%= javascript_include_tag 'Bitstream_Vera_Sans_400.font.js', :plugin => 'chiliproject_timelines' %>
<%= javascript_include_tag 'timelines.js', :plugin => 'chiliproject_timelines' %> <%= javascript_include_tag 'timelines.js', :plugin => 'chiliproject_timelines' %>
<%= javascript_include_tag 'modal.js', :plugin => 'chiliproject_timelines' %>
<%= javascript_include_tag 'timelines_modal.js', :plugin => 'chiliproject_timelines' %> <%= javascript_include_tag 'timelines_modal.js', :plugin => 'chiliproject_timelines' %>
<% @timeline_header_included = true %> <% @timeline_header_included = true %>
<% end %> <% end %>

@ -44,6 +44,33 @@ See doc/COPYRIGHT.rdoc for more details.
</p> </p>
<p class="tl-form-overflow">
<%= label_tag 'timeline_options_planning_element_status',
l("timelines.filter.status") %>
<% if User.current.impaired? %>
<%= select("timeline[options]",
:planning_element_status,
filter_select_with_none(
@timeline.available_planning_element_status,
:name, :id),
{:selected => @timeline.selected_planning_element_status.map(&:id)},
{:multiple => true,
:size => 12}) %>
<% else %>
<%= select("timeline[options]", :planning_element_status,
options_for_select([]),
{},
{ :'data-ajaxURL' => url_for({:controller => "/api/v2/statuses",
:action => "paginate_issue_statuses"}),
:multiple => true,
:'data-selected' => filter_select(
timeline.selected_planning_element_status,
:name, :id).to_json
}) %>
<% end %>
</p>
<p class="tl-form-overflow"> <p class="tl-form-overflow">
<%= label_tag 'timeline_options_planning_element_types', <%= label_tag 'timeline_options_planning_element_types',
l("timelines.filter.types") %> l("timelines.filter.types") %>

@ -84,4 +84,4 @@ See doc/COPYRIGHT.rdoc for more details.
<% html_title(l(:label_roadmap)) %> <% html_title(l(:label_roadmap)) %>
<%= context_menu issues_context_menu_path %> <%= context_menu work_packages_context_menu_path %>

@ -28,23 +28,23 @@ See doc/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<ul class="menu"> <ul class="menu">
<%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %> <%= call_hook(:view_issues_context_menu_start, {:issues => @work_packages, :can => @can, :back => @back }) %>
<% if !@issue.nil? -%> <% if !@work_package.nil? -%>
<li class="edit"> <li class="edit">
<%= context_menu_link l(:button_edit), edit_work_package_path(@issue), <%= context_menu_link l(:button_edit), edit_work_package_path(@work_package),
:class => 'icon-edit', :class => 'icon-edit',
:disabled => !@can[:edit] %> :disabled => !@can[:edit] %>
</li> </li>
<% else %> <% else %>
<li class="edit"> <li class="edit">
<%= context_menu_link l(:button_edit), bulk_edit_issues_path(:ids => @issues.collect(&:id)), <%= context_menu_link l(:button_edit), bulk_edit_issues_path(:ids => @work_packages.collect(&:id)),
:class => 'icon-edit', :class => 'icon-edit',
:disabled => !@can[:edit] %> :disabled => !@can[:edit] %>
</li> </li>
<% end %> <% end %>
<% updated_issues = @issues.collect(&:id) %> <% updated_issues = @work_packages.collect(&:id) %>
<% default_params = { :back_url => @back, <% default_params = { :back_url => @back,
:updated_object_ids => updated_issues } %> :updated_object_ids => updated_issues } %>
@ -52,7 +52,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% params = default_params.merge(:collection => @statuses, <% params = default_params.merge(:collection => @statuses,
:attribute => 'status', :attribute => 'status',
:title => WorkPackage.human_attribute_name(:status), :title => WorkPackage.human_attribute_name(:status),
:selected => lambda { |status| (@issue && status == @issue.status) }, :selected => lambda { |status| (@work_package && status == @work_package.status) },
:disabled => lambda { |status| !(@can[:update] && @allowed_statuses.include?(status)) }) %> :disabled => lambda { |status| !(@can[:update] && @allowed_statuses.include?(status)) }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end %> <% end %>
@ -61,7 +61,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% params = default_params.merge(:collection => @types, <% params = default_params.merge(:collection => @types,
:attribute => 'type', :attribute => 'type',
:title => WorkPackage.human_attribute_name(:type), :title => WorkPackage.human_attribute_name(:type),
:selected => lambda { |type| (@issue && type == @issue.type) }, :selected => lambda { |type| (@work_package && type == @work_package.type) },
:disabled => lambda { |type| !@can[:edit] }) %> :disabled => lambda { |type| !@can[:edit] }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end %> <% end %>
@ -69,8 +69,8 @@ See doc/COPYRIGHT.rdoc for more details.
<% params = default_params.merge(:collection => @priorities, <% params = default_params.merge(:collection => @priorities,
:attribute => 'priority', :attribute => 'priority',
:title => WorkPackage.human_attribute_name(:priority), :title => WorkPackage.human_attribute_name(:priority),
:selected => lambda { |priority| (@issue && priority == @issue.priority) }, :selected => lambda { |priority| (@work_package && priority == @work_package.priority) },
:disabled => lambda { |priority| !@can[:edit] || @issues.detect { |i| !i.leaf? } }) %> :disabled => lambda { |priority| !@can[:edit] || @work_packages.detect { |i| !i.leaf? } }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% #TODO: allow editing versions when multiple projects %> <% #TODO: allow editing versions when multiple projects %>
@ -78,7 +78,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% versions = versions.map{ |v| [v, format_version_name(v)] } << [nil, l(:label_none)]%> <% versions = versions.map{ |v| [v, format_version_name(v)] } << [nil, l(:label_none)]%>
<% params = default_params.merge(:collection => versions, <% params = default_params.merge(:collection => versions,
:attribute => 'fixed_version', :attribute => 'fixed_version',
:selected => lambda { |version| (@issue && version == @issue.fixed_version) }, :selected => lambda { |version| (@work_package && version == @work_package.fixed_version) },
:disabled => lambda { |version| !@can[:update] }) %> :disabled => lambda { |version| !@can[:update] }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end %> <% end %>
@ -88,7 +88,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% params = default_params.merge(:collection => assignables, <% params = default_params.merge(:collection => assignables,
:attribute => 'assigned_to', :attribute => 'assigned_to',
:title => WorkPackage.human_attribute_name(:assigned_to), :title => WorkPackage.human_attribute_name(:assigned_to),
:selected => lambda { |user| @issue && user == @issue.assigned_to }, :selected => lambda { |user| @work_package && user == @work_package.assigned_to },
:disabled => lambda { |user| !@can[:update] }) %> :disabled => lambda { |user| !@can[:update] }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end %> <% end %>
@ -98,7 +98,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% params = default_params.merge(:collection => categories, <% params = default_params.merge(:collection => categories,
:attribute => 'category', :attribute => 'category',
:title => WorkPackage.human_attribute_name(:category), :title => WorkPackage.human_attribute_name(:category),
:selected => lambda { |category| @issue && category == @issue.category }, :selected => lambda { |category| @work_package && category == @work_package.category },
:disabled => lambda { |category| !@can[:update] }) %> :disabled => lambda { |category| !@can[:update] }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end -%> <% end -%>
@ -108,55 +108,55 @@ See doc/COPYRIGHT.rdoc for more details.
:attribute => 'done_ratio', :attribute => 'done_ratio',
:title => WorkPackage.human_attribute_name(:done_ratio), :title => WorkPackage.human_attribute_name(:done_ratio),
:db_attribute => 'done_ratio', :db_attribute => 'done_ratio',
:selected => lambda { |ratio| @issue && ratio == @issue.done_ratio }, :selected => lambda { |ratio| @work_package && ratio == @work_package.done_ratio },
:disabled => lambda { |ratio| !@can[:edit] || @issues.detect { |i| !i.leaf? } }) %> :disabled => lambda { |ratio| !@can[:edit] || @work_packages.detect { |i| !i.leaf? } }) %>
<%= context_menu_entry(params) %> <%= context_menu_entry(params) %>
<% end -%> <% end -%>
<% if !@issue.nil? %> <% if !@work_package.nil? %>
<% if @can[:log_time] -%> <% if @can[:log_time] -%>
<li class="log_time"> <li class="log_time">
<%= context_menu_link l(:button_log_time), <%= context_menu_link l(:button_log_time),
new_work_package_time_entry_path(@issue), new_work_package_time_entry_path(@work_package),
:class => 'context_item' %> :class => 'context_item' %>
</li> </li>
<% end %> <% end %>
<% if User.current.logged? %> <% if User.current.logged? %>
<li class="watch"> <li class="watch">
<%= watcher_link(@issue, User.current) %> <%= watcher_link(@work_package, User.current) %>
</li> </li>
<% end %> <% end %>
<% end %> <% end %>
<% if @issue.present? %> <% if @work_package.present? %>
<li> <li>
<%= context_menu_link l(:button_duplicate), new_project_work_package_path({ :project_id => @project, <%= context_menu_link l(:button_duplicate), new_project_work_package_path({ :project_id => @project,
:copy_from => @issue }), :copy_from => @work_package }),
:class => 'icon-duplicate', :class => 'icon-duplicate',
:disabled => !@can[:copy] %> :disabled => !@can[:copy] %>
</li> </li>
<% end %> <% end %>
<li class="move"> <li class="move">
<%= context_menu_link l(:button_move), new_move_work_packages_path(:ids => @issues.collect(&:id)), <%= context_menu_link l(:button_move), new_move_work_packages_path(:ids => @work_packages.collect(&:id)),
:class => 'context_item', :class => 'context_item',
:disabled => !@can[:move] %> :disabled => !@can[:move] %>
</li> </li>
<li class="copy"> <li class="copy">
<%= context_menu_link l(:button_copy), new_move_work_packages_path(:ids => @issues.collect(&:id), <%= context_menu_link l(:button_copy), new_move_work_packages_path(:ids => @work_packages.collect(&:id),
:copy_options => { :copy => 't' }), :copy_options => { :copy => 't' }),
:class => 'context_item' %> :class => 'context_item' %>
</li> </li>
<li class="delete"> <li class="delete">
<%= context_menu_link l(:button_delete), work_packages_path(:ids => @issues.collect(&:id)), <%= context_menu_link l(:button_delete), work_packages_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',
:disabled => !@can[:delete] %> :disabled => !@can[:delete] %>
</li> </li>
<%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %> <%= call_hook(:view_issues_context_menu_end, {:issues => @work_packages, :can => @can, :back => @back }) %>
</ul> </ul>

@ -81,7 +81,7 @@ See doc/COPYRIGHT.rdoc for more details.
:update => "content", :update => "content",
:method => :get, :method => :get,
:complete => "apply_filters_observer()", :complete => "apply_filters_observer()",
:with => "Form.serialize('query_form')" :with => "jQuery('#query_form').serialize()"
}, :class => 'icon icon-checked' %> }, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear), <%= link_to_remote l(:button_clear),
@ -91,7 +91,7 @@ See doc/COPYRIGHT.rdoc for more details.
}, :class => 'icon icon-reload' %> }, :class => 'icon icon-reload' %>
<% if query.new_record? && User.current.allowed_to?(:save_queries, project, :global => true) %> <% if query.new_record? && User.current.allowed_to?(:save_queries, project, :global => true) %>
<%= link_to l(:button_save), {}, :onclick => "selectAllOptions('selected_columns'); $('query_form').submit(); return false;", :class => 'icon icon-save' %> <%= link_to l(:button_save), {}, :onclick => "selectAllOptions('selected_columns'); jQuery('#query_form').submit(); return false;", :class => 'icon icon-save' %>
<% end %> <% end %>
</p> </p>
<% end %> <% end %>
@ -134,4 +134,4 @@ See doc/COPYRIGHT.rdoc for more details.
<%= auto_discovery_link_tag(:atom, {:controller => '/journals', :action => 'index', :query_id => query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> <%= auto_discovery_link_tag(:atom, {:controller => '/journals', :action => 'index', :query_id => query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
<% end %> <% end %>
<%= context_menu issues_context_menu_path %> <%= context_menu work_packages_context_menu_path %>

@ -154,7 +154,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= stylesheet_link_tag 'context_menu_rtl' if l(:direction) == 'rtl' %> <%= stylesheet_link_tag 'context_menu_rtl' if l(:direction) == 'rtl' %>
<% end %> <% end %>
<div id="context-menu" style="display: none;"></div> <div id="context-menu" style="display: none;"></div>
<%= javascript_tag "new ContextMenu('#{issues_context_menu_path}')" %> <%= javascript_tag "new ContextMenu('#{work_packages_context_menu_path}')" %>
<% #include calendar js files in case they are needed for edit <% #include calendar js files in case they are needed for edit
include_calendar_headers_tags -%> include_calendar_headers_tags -%>

@ -1361,7 +1361,7 @@ de:
type: "Typ" type: "Typ"
end_date: "Abschlussdatum" end_date: "Abschlussdatum"
name: "Name" name: "Name"
project_status: "Projekt-Status" status: "Status"
responsible: "Planungsverantwortlicher" responsible: "Planungsverantwortlicher"
start_date: "Startdatum" start_date: "Startdatum"
columns: "Spalten" columns: "Spalten"
@ -1402,6 +1402,8 @@ de:
project_responsible: "Projekte von diesem Planungsverantwortlichen anzeigen" project_responsible: "Projekte von diesem Planungsverantwortlichen anzeigen"
project_status: "Projekt-Status anzeigen" project_status: "Projekt-Status anzeigen"
project_types: "Projekt-Typ anzeigen" project_types: "Projekt-Typ anzeigen"
types: "Typ anzeigen"
status: "Status anzeigen"
timeframe: "Zeitausschnitt bestimmen" timeframe: "Zeitausschnitt bestimmen"
timeframe_end: "bis" timeframe_end: "bis"
timeframe_start: "von" timeframe_start: "von"

@ -1340,7 +1340,7 @@ en:
type: "Type" type: "Type"
end_date: "End date" end_date: "End date"
name: "Name" name: "Name"
project_status: "Project status" status: "Status"
responsible: "Responsible" responsible: "Responsible"
start_date: "Start date" start_date: "Start date"
columns: "Columns" columns: "Columns"
@ -1374,6 +1374,7 @@ en:
planning_element_filters: "Filter planning elements" planning_element_filters: "Filter planning elements"
planning_element_responsible: "Show planning elements with responsible" planning_element_responsible: "Show planning elements with responsible"
types: "Show types" types: "Show types"
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}"

@ -58,7 +58,11 @@ OpenProject::Application.routes.draw do
resources :authentication resources :authentication
resources :planning_element_journals resources :planning_element_journals
resources :planning_element_statuses resources :statuses do
collection do
get :paginate_issue_statuses
end
end
resources :colors, :controller => 'planning_element_type_colors' resources :colors, :controller => 'planning_element_type_colors'
resources :planning_element_types do resources :planning_element_types do
collection do collection do
@ -76,6 +80,7 @@ OpenProject::Application.routes.draw do
get :paginate_reported_project_statuses get :paginate_reported_project_statuses
end end
end end
resources :statuses, :only => [:index, :show]
resources :timelines resources :timelines
resources :projects do resources :projects do
@ -86,6 +91,7 @@ OpenProject::Application.routes.draw do
resources :project_associations do resources :project_associations do
get :available_projects, :on => :collection get :available_projects, :on => :collection
end end
resources :statuses, :only => [:index, :show]
end end
end end
@ -294,6 +300,7 @@ OpenProject::Application.routes.draw do
namespace :work_packages do namespace :work_packages 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
resources :calendar, :controller => 'calendars', :only => [:index] resources :calendar, :controller => 'calendars', :only => [:index]
end end

@ -19,13 +19,12 @@ class LegacyIssuesToWorkPackages < ActiveRecord::Migration
def up def up
raise_on_existing_work_package_entries raise_on_existing_work_package_entries
copy_legacy_issues_to_work_packages copy_legacy_issues_to_work_packages
reset_public_key_sequence_in_postgres
end end
def down def down
raise_on_existing_legacy_issue_entries raise_on_existing_legacy_issue_entries
copy_work_packages_to_legacy_issues copy_work_packages_to_legacy_issues
end end
@ -45,6 +44,11 @@ class LegacyIssuesToWorkPackages < ActiveRecord::Migration
end end
end end
def reset_public_key_sequence_in_postgres
return unless ActiveRecord::Base.connection.instance_values["config"][:adapter] == "postgres"
ActiveRecord::Base.connection.reset_pk_sequence!('work_packages')
end
def copy_legacy_issues_to_work_packages def copy_legacy_issues_to_work_packages
execute <<-SQL execute <<-SQL
INSERT INTO work_packages INSERT INTO work_packages

@ -0,0 +1,63 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
class LegacyAttachmentJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals
end
private
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("AttachmentJournal", "attachment_journals") do
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
rewrite_issue_container_to_work_package(to_insert)
end
def rewrite_issue_container_to_work_package(to_insert)
if to_insert['container_type'].last == 'Issue'
to_insert['container_type'][-1] = 'WorkPackage'
end
end
end
end
end

@ -0,0 +1,47 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
class LegacyChangesetJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals
end
private
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("ChangesetJournal", "changeset_journals")
end
end

@ -0,0 +1,47 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
class LegacyNewsJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals
end
private
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("NewsJournal", "news_journals")
end
end

@ -0,0 +1,56 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
require_relative 'migration_utils/journal_migrator_concerns'
class LegacyMessageJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals 'attachable_journals'
end
private
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("MessageJournal", "message_journals") do
extend Migration::JournalMigratorConcerns::Attachable
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
migrate_attachments(to_insert, legacy_journal, journal_id)
end
end
end
end

@ -0,0 +1,56 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
require_relative 'migration_utils/journal_migrator_concerns'
class LegacyTimeEntryJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals 'customizable_journals'
end
private
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("TimeEntryJournal", "time_entry_journals") do
extend Migration::JournalMigratorConcerns::Customizable
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
migrate_custom_values(to_insert, legacy_journal, journal_id)
end
end
end
end

@ -0,0 +1,90 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
class LegacyWikiContentJournalData < ActiveRecord::Migration
class UnsupportedWikiContentJournalCompressionError < ::StandardError
end
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals
end
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new("WikiContentJournal", "wiki_content_journals") do
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
# remove once lock_version is no longer a column in the wiki_content_journales table
if !to_insert.has_key?("lock_version")
if !legacy_journal.has_key?("version")
raise WikiContentJournalVersionError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
There is a wiki content without a version.
The DB requires a version to be set
#{legacy_journal},
#{to_insert}
MESSAGE
end
# as the old journals used the format [old_value, new_value] we have to fake it here
to_insert["lock_version"] = [nil,legacy_journal["version"]]
end
if to_insert.has_key?("data")
# Why is that checked but than the compression is not used in any way to read the data
if !to_insert.has_key?("compression")
raise UnsupportedWikiContentJournalCompressionError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
There is a WikiContent journal that contains data in an
unsupported compression: #{compression}
MESSAGE
end
# as the old journals used the format [old_value, new_value] we have to fake it here
to_insert["text"] = [nil, to_insert.delete("data")]
# fix non null constraint violation on page_id.
to_insert["page_id"] = [nil, journal_id]
end
end
end
end
end

@ -0,0 +1,60 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
require_relative 'migration_utils/journal_migrator_concerns'
class LegacyIssueJournalData < ActiveRecord::Migration
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals 'customizable_journals',
'attachable_journals'
end
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new "IssueJournal", "work_package_journals" do
extend Migration::JournalMigratorConcerns::Attachable
extend Migration::JournalMigratorConcerns::Customizable
self.journable_class = "WorkPackage"
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
migrate_attachments(to_insert, legacy_journal, journal_id)
migrate_custom_values(to_insert, legacy_journal, journal_id)
end
end
end
end

@ -0,0 +1,141 @@
#-- 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.
#++
#
require_relative 'migration_utils/legacy_journal_migrator'
require_relative 'migration_utils/journal_migrator_concerns'
class LegacyPlanningElementJournalData < ActiveRecord::Migration
class UnknownJournaledError < ::StandardError
end
class UnknownTypeError < ::StandardError
end
def up
migrator.run
end
def down
migrator.remove_journals_derived_from_legacy_journals 'customizable_journals',
'attachable_journals'
end
def migrator
@migrator ||= Migration::LegacyJournalMigrator.new "Timelines_PlanningElementJournal", "work_package_journals" do
extend Migration::JournalMigratorConcerns::Attachable
extend Migration::JournalMigratorConcerns::Customizable
self.journable_class = "WorkPackage"
def migrate(legacy_journal)
update_journaled_id(legacy_journal)
super
end
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id)
update_type_id(to_insert)
migrate_attachments(to_insert, legacy_journal, journal_id)
migrate_custom_values(to_insert, legacy_journal, journal_id)
end
def update_journaled_id(legacy_journal)
new_journaled_id = new_journaled_id_for_old(legacy_journal["journaled_id"])
if new_journaled_id.nil?
raise UnknownJournaledError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
No new journaled_id could be found to replace the journaled_id value of
#{legacy_journal["journaled_id"]} for the legacy journal with the id
#{legacy_journal["id"]}
MESSAGE
end
legacy_journal["journaled_id"] = new_journaled_id
end
def update_type_id(to_insert)
return if to_insert["planning_element_type_id"].nil? ||
to_insert["planning_element_type_id"].last.nil?
new_type_id = new_type_id_for_old(to_insert["planning_element_type_id"].last)
if new_type_id.nil?
raise UnknownTypeError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
No new type_id could be found to replace the type_id value of
#{to_insert["planning_element_type_id"].last}
MESSAGE
end
to_insert["type_id"] = [nil, new_type_id]
end
def new_journaled_id_for_old(old_journaled_id)
@new_journaled_ids ||= begin
old_new = db_select_all <<-SQL
SELECT journaled_id AS old_id, new_id
FROM legacy_journals
LEFT JOIN legacy_planning_elements
ON legacy_journals.journaled_id = legacy_planning_elements.id
WHERE type = 'Timelines_PlanningElementJournal'
SQL
old_new.inject({}) do |mem, entry|
mem[entry['old_id']] = entry['new_id']
mem
end
end
@new_journaled_ids[old_journaled_id]
end
def new_type_id_for_old(old_type_id)
@new_type_ids ||= begin
old_new = db_select_all <<-SQL
SELECT id AS old_id, new_id
FROM legacy_planning_element_types
SQL
old_new.inject({}) do |mem, entry|
# the old_type_id was casted to a fixnum
# cheaper to change this here
mem[entry['old_id'].to_i] = entry['new_id'].to_i
mem
end
end
@new_type_ids[old_type_id]
end
end
end
end

@ -0,0 +1,50 @@
#-- 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/utils'
class UpdateAttachmentContainer < ActiveRecord::Migration
include Migration::Utils
def up
say_with_time_silently "Changing container type from 'Issue' to 'WorkPackage'" do
update <<-SQL
UPDATE #{attachments_table}
SET container_type = #{work_package_type}
WHERE container_type = #{issue_type}
SQL
end
end
def down
say_with_time_silently "Changing container type from 'WorkPackage' to 'Issue'" do
update <<-SQL
UPDATE #{attachments_table}
SET container_type = #{issue_type}
WHERE container_type = #{work_package_type}
SQL
end
end
private
def attachments_table
ActiveRecord::Base.connection.quote_table_name('attachments')
end
def issue_type
ActiveRecord::Base.connection.quote('Issue')
end
def work_package_type
ActiveRecord::Base.connection.quote('WorkPackage')
end
end

@ -0,0 +1,82 @@
#-- 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/utils'
class JournalActivitiesData < ActiveRecord::Migration
include Migration::Utils
def up
say_with_time_silently "Changing activity type from 'issues' to 'work_packages'" do
update <<-SQL
UPDATE #{journals_table}
SET activity_type = #{work_package_activity}
WHERE journable_type = #{work_package_type}
SQL
end
end
def down
if legacy_planning_elements_table_exists?
say_with_time_silently "Changing activity type from 'work_packages' to 'planning_elements'" do
update <<-SQL
UPDATE #{journals_table}
SET activity_type = #{planning_element_activity}
WHERE #{journals_table}.journable_id IN (SELECT new_id
FROM #{legacy_planning_elements_table})
SQL
end
else
say "Can not distinguish between former planning_elements and issues. Assuming all to be former issues."
end
say_with_time_silently "Changing activity type from 'work_packages' to 'issues'" do
update <<-SQL
UPDATE #{journals_table}
SET activity_type = #{issue_activity}
WHERE activity_type = #{work_package_activity}
SQL
end
end
private
def legacy_planning_elements_table_exists?
suppress_messages do
table_exists? legacy_planning_elements_table
end
end
def journals_table
ActiveRecord::Base.connection.quote_table_name('journals')
end
def legacy_planning_elements_table
ActiveRecord::Base.connection.quote_table_name('legacy_planning_elements')
end
def work_package_type
ActiveRecord::Base.connection.quote('WorkPackage')
end
def work_package_activity
ActiveRecord::Base.connection.quote('work_packages')
end
def planning_element_activity
ActiveRecord::Base.connection.quote('timelines_planning_elements')
end
def issue_activity
ActiveRecord::Base.connection.quote('issues')
end
end

@ -0,0 +1,60 @@
#-- 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 Migration
module DbWorker
def quote_value(name)
ActiveRecord::Base.connection.quote name
end
def quoted_table_name(name)
ActiveRecord::Base.connection.quote_table_name name
end
def db_table_exists?(name)
ActiveRecord::Base.connection.table_exists? name
end
def db_columns(table_name)
ActiveRecord::Base.connection.columns table_name
end
def db_select_all(statement)
ActiveRecord::Base.connection.select_all statement
end
def db_execute(statement)
ActiveRecord::Base.connection.execute statement
end
def db_delete(statement)
ActiveRecord::Base.connection.delete statement
end
end
end

@ -0,0 +1,146 @@
#-- 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 Migration
module JournalMigratorConcerns
module Attachable
def migrate_attachments(to_insert, legacy_journal, journal_id)
attachments = to_insert.keys.select { |d| d =~ attachment_key_regexp }
attachments.each do |key|
attachment_id = attachment_key_regexp.match(key)[1]
# if an attachment was added the value contains something like:
# [nil, "blubs.png"]
# if it was removed the value is something like
# ["blubs.png", nil]
removed_filename, added_filename = *to_insert[key]
if added_filename && !removed_filename
# The attachment was added
attachable = ActiveRecord::Base.connection.select_all <<-SQL
SELECT *
FROM #{attachable_table_name} AS a
WHERE a.journal_id = #{quote_value(journal_id)} AND a.attachment_id = #{attachment_id};
SQL
if attachable.size > 1
raise AmbiguousAttachableJournalError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous attachable journal data.
Please make sure attachable journal data are consistent and
that the unique constraint on journal_id and attachment_id
is met.
MESSAGE
elsif attachable.size == 0
db_execute <<-SQL
INSERT INTO #{attachable_table_name}(journal_id, attachment_id, filename)
VALUES (#{quote_value(journal_id)}, #{quote_value(attachment_id)}, #{quote_value(added_filename)});
SQL
end
elsif removed_filename && !added_filename
# The attachment was removed
# we need to make certain that no subsequent journal adds an attachable_journal
# for this attachment
to_insert.delete_if { |k, v| k =~ /attachments_?#{attachment_id}/ }
else
raise InvalidAttachableJournalError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
There is a journal entry for an attachment but neither the old nor the new value contains anything:
#{to_insert}
#{legacy_journal}
MESSAGE
end
end
end
def attachable_table_name
quoted_table_name("attachable_journals")
end
def attachment_key_regexp
# Attachment journal entries can be written in two ways:
# attachments123 if the attachment was added
# attachments_123 if the attachment was removed
#
@attachment_key_regexp ||= /attachments_?(\d+)$/
end
end
module Customizable
def migrate_custom_values(to_insert, legacy_journal, journal_id)
keys = to_insert.keys
values = to_insert.values
custom_values = keys.select { |d| d =~ /custom_values.*/ }
custom_values.each do |k|
custom_field_id = k.split("_values").last.to_i
value = values[keys.index k].last
customizable = db_select_all <<-SQL
SELECT *
FROM #{customizable_table_name} AS a
WHERE a.journal_id = #{quote_value(journal_id)} AND a.custom_field_id = #{custom_field_id};
SQL
if customizable.size > 1
raise AmbiguousCustomizableJournalError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous customizable journal
data. Please make sure customizable journal data are
consistent and that the unique constraint on journal_id and
custom_field_id is met.
MESSAGE
elsif customizable.size == 0
db_execute <<-SQL
INSERT INTO #{customizable_table_name}(journal_id, custom_field_id, value)
VALUES (#{quote_value(journal_id)}, #{quote_value(custom_field_id)}, #{quote_value(value)});
SQL
end
end
end
def customizable_table_name
quoted_table_name("customizable_journals")
end
end
end
end

@ -0,0 +1,407 @@
#-- 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.
#++
require_relative 'db_worker'
require_relative 'legacy_table_checker'
module Migration
class IncompleteJournalsError < ::StandardError
end
class AmbiguousJournalsError < ::StandardError
end
class LegacyJournalMigrator
include DbWorker
include LegacyTableChecker
attr_accessor :table_name,
:type,
:journable_class
def initialize(type, table_name, &block)
self.table_name = table_name
self.type = type
instance_eval &block if block_given?
if table_name.nil? || type.nil?
raise ArgumentError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
table_name and type have to be provided. Either as parameters
or set within the block.
MESSAGE
end
self.journable_class ||= self.type.gsub(/Journal$/, "")
end
def run
unless preconditions_met?
puts <<-MESSAGE
There is no legacy_journals table from which to derive the new
journals. Doing nothing ...
MESSAGE
return
end
legacy_journals = fetch_legacy_journals
total_count = legacy_journals.count
if total_count > 1
progress_bar = ProgressBar.create(format: '%a <%B> %P%% %e',
total: total_count,
throttle_rate: 1,
smoothing: 0.5)
progress_bar.log "Migrating #{total_count} legacy journals."
legacy_journals.each_with_index do |legacy_journal, count|
migrate(legacy_journal)
progress_bar.increment
end
end
end
def remove_journals_derived_from_legacy_journals(*table_names)
table_names << table_name
if legacy_table_exists?
table_names.each do |table_name|
db_delete <<-SQL
DELETE
FROM #{quoted_table_name(table_name)}
WHERE journal_id in (SELECT id
FROM #{quoted_legacy_journals_table_name}
WHERE type=#{quote_value(type)})
SQL
end
db_delete <<-SQL
DELETE
FROM journals
WHERE id in (SELECT id
FROM #{quoted_legacy_journals_table_name}
WHERE type=#{quote_value(type)})
SQL
else
puts "No legacy table exists. Doing nothing"
end
end
protected
def migrate(legacy_journal)
journal = set_journal(legacy_journal)
journal_id = journal["id"]
set_journal_data(journal_id, legacy_journal)
end
def combine_journal(journaled_id, legacy_journal)
# compute the combined journal from current and all previous changesets.
combined_journal = legacy_journal["changed_data"]
if previous.journaled_id == journaled_id
combined_journal = previous.journal.merge(combined_journal)
end
# remember the combined journal as the previous one for the next iteration.
previous.set(combined_journal, journaled_id)
combined_journal
end
def previous
@previous ||= PreviousState.new({}, 0)
end
# here to be overwritten by instances
def migrate_key_value_pairs!(to_insert, legacy_journal, journal_id) end
# fetches specific journal data row. might be empty.
def fetch_existing_data_journal(journal_id)
db_select_all <<-SQL
SELECT *
FROM #{journal_table_name} AS d
WHERE d.journal_id = #{quote_value(journal_id)};
SQL
end
# gets a journal row, and makes sure it has a valid id in the database.
# if the journal does not exist, it creates it
def set_journal(legacy_journal)
journal = fetch_journal(legacy_journal)
if journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journals. Please make sure
journals are consistent and that the unique constraint on id,
type and version is met.
MESSAGE
elsif journal.size == 0
journal = create_journal(legacy_journal)
end
journal.first
end
# fetches specific journal row. might be empty.
def fetch_journal(legacy_journal)
id, version = legacy_journal["journaled_id"], legacy_journal["version"]
db_select_all <<-SQL
SELECT *
FROM #{quoted_journals_table_name} AS j
WHERE j.journable_id = #{quote_value(id)}
AND j.journable_type = #{quote_value(journable_class)}
AND j.version = #{quote_value(version)};
SQL
end
# creates a valid journal.
# But might be not what is desired as an end result, yet. It is e.g.
# created with created_at set to now. This will need to be set to an actual
# date
def create_journal(legacy_journal)
db_execute <<-SQL
INSERT INTO #{quoted_journals_table_name} (
id,
journable_id,
version,
user_id,
notes,
activity_type,
created_at,
journable_type
)
VALUES (
#{quote_value(legacy_journal["id"])},
#{quote_value(legacy_journal["journaled_id"])},
#{quote_value(legacy_journal["version"])},
#{quote_value(legacy_journal["user_id"])},
#{quote_value(legacy_journal["notes"])},
#{quote_value(legacy_journal["activity_type"])},
#{quote_value(legacy_journal["created_at"])},
#{quote_value(journable_class)}
);
SQL
fetch_journal(legacy_journal)
end
def set_journal_data(journal_id, legacy_journal)
deserialize_journal(legacy_journal)
journaled_id = legacy_journal["journaled_id"]
combined_journal = combine_journal(journaled_id, legacy_journal)
migrate_key_value_pairs!(combined_journal, legacy_journal, journal_id)
to_insert = insertable_data_journal(combined_journal)
existing_data_journal = fetch_existing_data_journal(journal_id)
if existing_data_journal.size > 1
raise AmbiguousJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are ambiguous journal data. Please make sure
journal data are consistent and that the unique constraint on
journal_id is met.
MESSAGE
elsif existing_data_journal.size == 0
existing_data_journal = create_data_journal(journal_id, to_insert)
end
existing_data_journal = existing_data_journal.first
update_data_journal(existing_data_journal["id"], to_insert)
end
def create_data_journal(journal_id, to_insert)
keys = to_insert.keys
values = to_insert.values
db_execute <<-SQL
INSERT INTO #{journal_table_name} (journal_id#{", " + keys.join(", ") unless keys.empty? })
VALUES (#{quote_value(journal_id)}#{", " + values.map{|d| quote_value(d)}.join(", ") unless values.empty?});
SQL
fetch_existing_data_journal(journal_id)
end
def update_data_journal(id, to_insert)
db_execute <<-SQL unless to_insert.empty?
UPDATE #{journal_table_name}
SET #{(to_insert.each.map { |key,value| "#{key} = #{quote_value(value)}"}).join(", ") }
WHERE id = #{id};
SQL
end
def deserialize_changed_data(journal)
changed_data = journal["changed_data"]
return Hash.new if changed_data.nil?
YAML.load(changed_data)
end
def deserialize_journal(journal)
integerize_ids(journal)
journal["changed_data"] = deserialize_changed_data(journal)
end
def insertable_data_journal(journal)
journal.inject({}) do |mem, (key, value)|
current_key = map_key(key)
if column_names.include?(current_key)
# The old journal's values attribute was structured like
# [old_value, new_value]
# We only need the new_value
mem[current_key] = value.last
end
mem
end
end
def map_key(key)
case key
when "issue_id"
"work_package_id"
when "tracker_id"
"type_id"
when "end_date"
"due_date"
else
key
end
end
def integerize_ids(journal)
# turn id fields into integers.
["id", "journaled_id", "user_id", "version"].each do |f|
journal[f] = journal[f].to_i
end
end
# fetches legacy journals. might me empty.
def fetch_legacy_journals
db_select_all <<-SQL
SELECT *
FROM #{quoted_legacy_journals_table_name} AS j
WHERE (j.type = #{quote_value(type)})
ORDER BY j.journaled_id, j.type, j.version;
SQL
end
def preconditions_met?
legacy_table_exists? && check_legacy_journal_completeness
end
def check_legacy_journal_completeness
# SQL finds all those journals whose has more or less predecessors than
# it's version would require. Ignores the first journal.
# e.g. a journal with version 5 would have to have 5 predecessors
invalid_journals = db_select_all <<-SQL
SELECT DISTINCT tmp.id
FROM (
SELECT
a.id AS id,
a.journaled_id,
a.type,
a.version AS version,
count(b.id) AS count
FROM
#{quoted_legacy_journals_table_name} AS a
LEFT JOIN
#{quoted_legacy_journals_table_name} AS b
ON a.version >= b.version
AND a.journaled_id = b.journaled_id
AND a.type = b.type
WHERE a.version > 1
AND (a.type = #{quote_value(type)})
GROUP BY
a.id,
a.journaled_id,
a.type,
a.version
) AS tmp
WHERE
NOT (tmp.version = tmp.count);
SQL
unless invalid_journals.empty?
raise IncompleteJournalsError, <<-MESSAGE.split("\n").map(&:strip!).join(" ") + "\n"
It appears there are incomplete journals. Please make sure
journals are consistent and that for every journal, there is an
initial journal containing all attribute values at the time of
creation. The offending journal ids are: #{invalid_journals}
MESSAGE
end
true
end
def journal_table_name
@journal_table_name ||= quoted_table_name(table_name)
end
def quoted_legacy_journals_table_name
@quoted_legacy_journals_table_name ||= quoted_table_name 'legacy_journals'
end
def quoted_journals_table_name
@quoted_journals_table_name ||= quoted_table_name 'journals'
end
def column_names
@column_names ||= db_columns(table_name).map(&:name)
end
end
class PreviousState < Struct.new(:journal, :journaled_id)
def set(journal, journaled_id)
self.journal = journal
self.journaled_id = journaled_id
end
end
end

@ -0,0 +1,39 @@
#-- 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 Migration
module LegacyTableChecker
include DbWorker
def legacy_table_exists?
db_table_exists? 'legacy_journals'
end
end
end

@ -21,7 +21,7 @@ module Migration
def update_column_values(table, column_list, updater, conditions) def update_column_values(table, column_list, updater, conditions)
updated_rows = [] updated_rows = []
select_rows_from_database(table, column_list, conditions).each do |row| select_rows_from_database(table, column_list, conditions).each do |row|
updated_rows << updater.call(row) updated_rows << updater.call(row)
end end

@ -26,4 +26,33 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
# add seeds specific for the production-environment here # add seeds specific for the production-environment here
standard_type = Type.find_by_is_standard(true)
# Adds the standard type to all existing projects
#
# As this seed might be executed on an existing database, there might be projects
# that do not have the default type yet.
projects_without_standard_type = Project.where("NOT EXISTS (SELECT * from projects_types WHERE projects.id = projects_types.project_id AND projects_types.type_id = #{standard_type.id})")
.all
projects_without_standard_type.each do |project|
project.types << standard_type
end
# Fixes work packages that do not have a type yet. They receive the standard type.
#
# This can happen when an existing database, having timelines planning elements,
# gets migrated. During the migration, the existing planning elements are converted
# to work_packages. Because the existance of a standard type cannot be guaranteed
# during the migration, such work packages receive a type_id of 0.
#
# Because all work packages that do not a type yet should always have had one
# (from todays standpoint) the assignment is done covertedly.
[WorkPackage, Journal::WorkPackageJournal].each do |klass|
klass.update_all({ :type_id => standard_type.id }, { :type_id => [0, nil] })
end

@ -29,11 +29,35 @@ See doc/COPYRIGHT.rdoc for more details.
# Changelog # Changelog
* `#1946` Modal shown within in Modal
* `#1949` External links within modals do not work
* `#1992` Prepare schema migrations table
## 3.0.0pre19
* `#2203` Use server-side responsible filter
* `#2204` Implement server-side status filter
* `#2218` Migrate context menus controller tests
* `#2204` Implement server-side status filter.
* `#2055` More dynamic attribute determination for journals for extending journals by plugins
## 3.0.0pre18
* `#1715` Group assigned work packages * `#1715` Group assigned work packages
* `#1770` New Comment Section layout errors * `#1770` New Comment Section layout errors
* `#1790` Fix activity view bug coming up during the meeting adaptions to acts_as_journalized * `#1790` Fix activity view bug coming up during the meeting adaptions to acts_as_journalized
* `#1793` Data Migration Journals
* `#1977` Set default type for planning elements
* `#1990` Migrate issue relation * `#1990` Migrate issue relation
* `#1992` Prepare schema migrations table * `#1997` Migrate journal activities
* `#2008` Migrate attachments
* `#2083` Extend APIv2 to evaluate filter arguments
* `#2087` Write tests for server-side type filter
* `#2088` Implement server-side filter for type
* `#2101` 500 on filtering multiple values
* `#2104` Handle incomplete trees on server-side
* `#2105` Call PE-API with type filters
* `#2138` Add responsible to workpackage-search
## 3.0.0pre17 ## 3.0.0pre17
@ -63,15 +87,16 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1850` Disable atom feeds via setting * `#1850` Disable atom feeds via setting
* `#1874` Move Scopes from Issue into Workpackage * `#1874` Move Scopes from Issue into Workpackage
* `#1898` Separate action for changing wiki parent page (was same as rename before) * `#1898` Separate action for changing wiki parent page (was same as rename before)
* `#1921` Allow disabling done ratio for work packages
* `#1923` Add permission that allows hiding repository statistics on commits per author * `#1923` Add permission that allows hiding repository statistics on commits per author
* `#1950` Grey line near the lower end of the modal, cuts off a bit of the content * `#1950` Grey line near the lower end of the modal, cuts off a bit of the content
* `#1921` Allow disabling done ratio for work packages
## 3.0.0pre15 ## 3.0.0pre15
* `#1557` Timeline Report Selection Not Visible
* `#1911` Change mouse icon when hovering over drag&drop-enabled select2 entries
* `#1301` Ajax call when logged out should open a popup window * `#1301` Ajax call when logged out should open a popup window
* `#1351` Generalize Modal Creation * `#1351` Generalize Modal Creation
# `#1557` Timeline Report Selection Not Visible
* `#1755` Migrate helper-tests for issues into specs for work package * `#1755` Migrate helper-tests for issues into specs for work package
* `#1766` Fixed bug: Viewing diff of Work Package description results in error 500 * `#1766` Fixed bug: Viewing diff of Work Package description results in error 500
* `#1767` Fixed bug: Viewing changesets results in "page not found" * `#1767` Fixed bug: Viewing changesets results in "page not found"
@ -81,15 +106,13 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1875` Added test steps to reuse steps for my page, my project page, and documents, no my page block lookup at class load time * `#1875` Added test steps to reuse steps for my page, my project page, and documents, no my page block lookup at class load time
* `#1876` Timelines do not show work packages when there is no status reporting * `#1876` Timelines do not show work packages when there is no status reporting
* `#1896` Moved visibility-tests for issues into specs for workpackages * `#1896` Moved visibility-tests for issues into specs for workpackages
# `#1911` Change mouse icon when hovering over drag&drop-enabled select2 entries
* `#1912` Merge column project type with column planning element type * `#1912` Merge column project type with column planning element type
* `#1918` Custom fields are not displayed when issue is created * `#1918` Custom fields are not displayed when issue is created
## 3.0.0pre14 ## 3.0.0pre14
* `#1873` Move Validations from Issue into Workpackage * `#825` Migrate Duration
* `#825` Migrate Duration * `#828` Remove Alternate Dates
* `#828` Remove Alternate Dates
* `#1421` Adapt issue created/updated wording to apply to work packages * `#1421` Adapt issue created/updated wording to apply to work packages
* `#1610` Move Planning Element Controller to API V2 * `#1610` Move Planning Element Controller to API V2
* `#1686` Issues not accessible in public projects when not a member * `#1686` Issues not accessible in public projects when not a member
@ -97,6 +120,7 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1787` Remove Scenarios * `#1787` Remove Scenarios
* `#1813` Run Data Generator on old AAJ schema * `#1813` Run Data Generator on old AAJ schema
* `#1859` Fix 20130814130142 down-migration (remove_documents) * `#1859` Fix 20130814130142 down-migration (remove_documents)
* `#1873` Move Validations from Issue into Workpackage
## 3.0.0pre13 ## 3.0.0pre13
@ -118,16 +142,14 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1418` Change links to issues/planning elements to use work_packages controller * `#1418` Change links to issues/planning elements to use work_packages controller
* `#1541` Use Rails 3.2.14 instead of Git Branch * `#1541` Use Rails 3.2.14 instead of Git Branch
* `#1595` Cleanup action menu for work packages * `#1595` Cleanup action menu for work packages
* `#1598` Switching type of work package looses inserted data
* `#1596` Copy/Move work packages between projects * `#1596` Copy/Move work packages between projects
* `#1598` Switching type of work package looses inserted data
* `#1618` Deactivate modal dialogs and respective cukes * `#1618` Deactivate modal dialogs and respective cukes
* `#1637` Removed files module * `#1637` Removed files module
* `#1648` Arbitrarily failing cuke: Navigating to the timeline page * `#1648` Arbitrarily failing cuke: Navigating to the timeline page
## 3.0.0pre10 ## 3.0.0pre10
* `#1536` Fixed bug: Reposman.rb receives xml response for json request
* `#1520` PlanningElements are created without the root_id attribute being set
* `#1246` Implement uniform "edit" action/view for pe & issues * `#1246` Implement uniform "edit" action/view for pe & issues
* `#1247` Implement uniform "update" action for pe & issues * `#1247` Implement uniform "update" action for pe & issues
* `#1411` Migrate database tables into the new model * `#1411` Migrate database tables into the new model
@ -140,43 +162,45 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1437` Update seed data * `#1437` Update seed data
* `#1512` Merge PlanningElementTypes model with Types model * `#1512` Merge PlanningElementTypes model with Types model
* `#1520` PlanningElements are created without the root_id attribute being set * `#1520` PlanningElements are created without the root_id attribute being set
* `#1520` PlanningElements are created without the root_id attribute being set
* `#1536` Fixed bug: Reposman.rb receives xml response for json request
* `#1577` Searching for project member candidates is only possible when using "firstname lastname" (or parts of it) * `#1577` Searching for project member candidates is only possible when using "firstname lastname" (or parts of it)
## 3.0.0pre9 ## 3.0.0pre9
* `#1517` Journal changed_data cannot contain the changes of a wiki_content content
* `#779` Integrate password expiration * `#779` Integrate password expiration
* `#1461` Integration Activity Plugin * `#1314` Always set last activity timestamp and check session expiry if ttl-setting is enabled
* `#1505` Removing all roles from a membership removes the project membership * `#1371` Changing pagination per_page_param does not change page
* `#1405` Incorrect message when trying to login with a permanently blocked account * `#1405` Incorrect message when trying to login with a permanently blocked account
* `#1488` Fixes multiple and missing error messages on project settings' member tab (now with support for success messages)
* `#1409` Changing pagination limit on members view looses members tab * `#1409` Changing pagination limit on members view looses members tab
* `#1371` Changing pagination per_page_param does not change page
* `#1314` Always set last activity timestamp and check session expiry if ttl-setting is enabled
* `#1414` Remove start & due date requirement from planning elements * `#1414` Remove start & due date requirement from planning elements
* `#1461` Integration Activity Plugin
* `#1488` Fixes multiple and missing error messages on project settings' member tab (now with support for success messages)
* `#1493` Exporting work packages to pdf returns 406 * `#1493` Exporting work packages to pdf returns 406
* `#1505` Removing all roles from a membership removes the project membership
* `#1517` Journal changed_data cannot contain the changes of a wiki_content content
## 3.0.0pre8 ## 3.0.0pre8
* `#1420` Allow for seeing work package description changes inside of the page
* `#1488` Fixes multiple and missing error messages on project settings' member tab
* `#377` Some usability fixes for members selection with select2 * `#377` Some usability fixes for members selection with select2
* `#1406` Creating a work package w/o responsible or assignee results in 500
* `#1391` Opening the new issue form in a project with an issue category defined produces 500 response
* `#1063` Added helper to format the time as a date in the current user or the system time zone
* `#1024` Add 'assign random password' option to user settings * `#1024` Add 'assign random password' option to user settings
* `#1063` Added helper to format the time as a date in the current user or the system time zone
* `#1391` Opening the new issue form in a project with an issue category defined produces 500 response
* `#1406` Creating a work package w/o responsible or assignee results in 500
* `#1420` Allow for seeing work package description changes inside of the page
* `#1488` Fixes multiple and missing error messages on project settings' member tab
## 3.0.0pre7 ## 3.0.0pre7
* `#778` Integrate ban of former passwords
* `#780` Add password brute force prevention
* `#820` Implement awesome nested set on work packages * `#820` Implement awesome nested set on work packages
* `#1034` Create changelog and document format
* `#1119` Creates a unified view for work_package show, new and create * `#1119` Creates a unified view for work_package show, new and create
* `#780` Add password brute force prevention * `#1209` Fix adding watcher to issue
* `#1214` Fix pagination label and 'entries_per_page' setting * `#1214` Fix pagination label and 'entries_per_page' setting
* `#1303` Watcherlist contains unescaped HTML
* `#1315` Correct spelling mistakes in German translation
* `#1299` Refactor user status * `#1299` Refactor user status
* `#1301` Ajax call when logged out should open a popup window * `#1301` Ajax call when logged out should open a popup window
* `#778` Integrate ban of former passwords * `#1303` Watcherlist contains unescaped HTML
* `#1209` Fix adding watcher to issue * `#1315` Correct spelling mistakes in German translation
* `#1034` Create changelog and document format

@ -0,0 +1,209 @@
#-- 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: Filtering work packages via the api
Background:
Given there is 1 project with the following:
| identifier | sample_project |
| name | sample_project |
And I am working in project "sample_project"
And the project "sample_project" has the following types:
| name | position |
| Bug | 1 |
| Task | 2 |
| Story | 3 |
| Epic | 4 |
And there is a default issuepriority with:
| name | Normal |
And there is a issuepriority with:
| name | High |
And there is a issuepriority with:
| name | Immediate |
And there are the following issue status:
| name | is_closed | is_default |
| New | false | true |
| In Progress | false | true |
| Closed | false | true |
And the project uses the following modules:
| timelines |
And there is a role "member"
And the role "member" may have the following rights:
| edit_work_packages |
| view_projects |
| view_reportings |
| view_timelines |
| view_work_packages |
And there is 1 user with the following:
| login | bob |
And there is 1 user with the following:
| login | peter |
And there is 1 user with the following:
| login | pamela |
And the user "bob" is a "member" in the project "sample_project"
And the user "peter" is a "member" in the project "sample_project"
And the user "pamela" is a "member" in the project "sample_project"
And I am already logged in as "bob"
Scenario: Call the endpoint of the api without filters
Given there are the following work packages in project "sample_project":
| subject | type |
| work_package#1 | Bug |
| work_package#2 | Story |
When I call the work_package-api on project "sample_project" requesting format "json" without any filters
Then the json-response should include 2 work packages
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"
Scenario: Call the api filtering for type
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Bug | |
| work_package#1.1 | Bug | work_package#1 |
| work_package#2 | Story | |
| work_package#2.1 | Story | work_package#2 |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Bug"
Then the json-response should include 2 work packages
Then the json-response should not contain a work_package "work_package#2"
And the json-response should contain a work_package "work_package#1"
Scenario: Call the api filtering for status
Given there are the following work packages in project "sample_project":
| subject | type | status |
| work_package#1 | Bug | New |
| work_package#2 | Story | In Progress |
| work_package#3 | Epic | Closed |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for status "In Progress"
Then the json-response should include 1 work package
Then the json-response should contain a work_package "work_package#2"
And the json-response should not contain a work_package "work_package#1"
Scenario: Filtering multiple types
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Bug | |
| work_package#1.1 | Bug | work_package#1 |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Bug,Epic"
Then the json-response should include 3 work packages
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#3"
And the json-response should not contain a work_package "work_package#3.1"
Scenario: Filter out children of work packages, if they don't have the right type
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic"
Then the json-response should include 1 work package
And the json-response should contain a work_package "work_package#3"
And the json-response should not contain a work_package "work_package#3.1"
Scenario: Filter out parents of work packages, if they don't have the right type
Given there are the following work packages in project "sample_project":
| subject | type |
| work_package#1 | Bug |
| work_package#2 | Story |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Story"
Then the json-response should include 1 work package
And the json-response should not contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"
Scenario: correctly export parent-child-relations
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#2 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" without any filters
Then the json-response should include 3 work packages
And the json-response should say that "work_package#1" is parent of "work_package#1.1"
Scenario: Move parent-relations up the ancestor-chain, when intermediate packages are fitered
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#1.1.1 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Task"
Then the json-response should include 2 work packages
And the json-response should not contain a work_package "work_package#1.1"
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#1.1.1"
And the json-response should say that "work_package#1" is parent of "work_package#1.1.1"
Scenario: The parent should be rewired to the first ancestor present in the filtered set
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Task | work_package#1 |
| work_package#1.1.1 | Bug | work_package#1.1 |
| work_package#1.1.1.1 | Task | work_package#1.1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Task"
Then the json-response should include 3 work packages
And the json-response should say that "work_package#1.1" is parent of "work_package#1.1.1.1"
Scenario: When all ancestors are filtered, the work_package should have no parent
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#1.1.1 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Task"
Then the json-response should include 1 work packages
And the json-response should say that "work_package#1.1.1" has no parent
Scenario: Children are filtered out
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Task | work_package#1 |
| work_package#1.2 | Story | work_package#1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Story"
And the json-response should say that "work_package#1" has 1 child
Scenario: Filtering for responsibles
Given there are the following work packages in project "sample_project":
| subject | type | responsible |
| work_package#1 | Task | bob |
| work_package#2 | Task | peter |
| work_package#3 | Task | pamela |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for responsible "peter"
Then the json-response should include 1 work package
And the json-response should not contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"

@ -0,0 +1,150 @@
#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.
#++
require "benchmark"
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" without any filters$/) do |project_name, format|
@project = Project.find(project_name)
@unfiltered_benchmark = Benchmark.measure("Unfiltered Results") do
visit api_v2_project_planning_elements_path(project_id: project_name, format: format)
end
end
Then(/^the json\-response should include (\d+) work package(s?)$/) do |number_of_wps, plural|
expect(work_package_names.size).to eql number_of_wps.to_i
end
Then(/^the json\-response should( not)? contain a work_package "(.*?)"$/) do |negation, work_package_name|
if negation
expect(work_package_names).not_to include work_package_name
else
expect(work_package_names).to include work_package_name
end
end
And(/^the json\-response should say that "(.*?)" is parent of "(.*?)"$/) do |parent_name, child_name|
child = decoded_json["planning_elements"].select {|wp| wp["name"] == child_name}.first
expect(child["parent"]["name"]).to eql parent_name
end
And(/^the json\-response should say that "(.*?)" has no parent$/) do |child_name|
child = decoded_json["planning_elements"].select {|wp| wp["name"] == child_name}.first
expect(child["parent"]).to be_nil
end
And(/^the json\-response should say that "(.*?)" has (\d+) child(ren)?$/) do |parent_name, nr_of_children,plural|
parent = decoded_json["planning_elements"].select {|wp| wp["name"] == parent_name}.first
expect(parent["children"].size).to eql nr_of_children.to_i
end
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for status "(.*?)"$/) do |project_name, format, status_names|
statuses = IssueStatus.where(name: status_names.split(','))
get_filtered_json(project_name: project_name,
format: format,
filters: [:status_id],
operators: {status_id: "="},
values: {status_id: statuses.map(&:id)} )
end
Then(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for type "(.*?)"$/) do |project_name, format, type_names|
types = Project.find_by_identifier(project_name).types.where(name: type_names.split(","))
get_filtered_json(project_name: project_name,
format: format,
filters: [:type_id],
operators: {type_id: "="},
values: {type_id: types.map(&:id)} )
end
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for responsible "(.*?)"$/) do |project_name, format, responsible_names|
responsibles = User.where(login: responsible_names.split(','))
get_filtered_json(project_name: project_name,
format: format,
filters: [:responsible_id],
operators: {responsible_id: "="},
values: {responsible_id: responsibles.map(&:id)} )
end
And(/^there are (\d+) work packages of type "(.*?)" in project "(.*?)"$/) do |nr_of_wps, type_name, project_name|
project = Project.find_by_identifier(project_name)
type = project.types.find_by_name(type_name)
FactoryGirl.create_list(:work_package, nr_of_wps.to_i, project: project, type: type)
end
And(/^the time to get the unfiltered results should not exceed (\d+)\.(\d+)s$/) do |seconds,milliseconds|
puts "----Unfiltered Benchmark----"
puts @unfiltered_benchmark
@unfiltered_benchmark.total.should < "#{seconds}.#{milliseconds}".to_f
end
And(/^the time to get the filtered results should not exceed (\d+)\.(\d+)s$/) do |seconds, milliseconds|
puts "----Filtered Benchmark----"
puts @filtered_benchmark
@filtered_benchmark.total.should < "#{seconds}.#{milliseconds}".to_f
end
Then(/^the time to get the filtered results should be faster than the time to get the unfiltered results$/) do
@filtered_benchmark.total.should < @unfiltered_benchmark.total
end
def work_package_names
decoded_json["planning_elements"].map{|wp| wp["name"]}
end
def decoded_json
@decoded_json ||= ActiveSupport::JSON.decode last_json
end
def last_json
page.source
end
def get_filtered_json(params)
@filtered_benchmark = Benchmark.measure("Filtered Results") do
visit api_v2_project_planning_elements_path(project_id: params[:project_name],
format: params[:format],
f: params[:filters],
op: params[:operators],
v: params[:values])
end
end

@ -181,7 +181,7 @@ 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
raise "This user must be member of a project to have issues" unless p raise "This user must be member of a project to have issues" unless p
i = WorkPackage.generate_for_project!(p) i = FactoryGirl.create(:work_package, project: p)
t = TimeEntry.generate t = TimeEntry.generate
t.user = u t.user = u
t.issue = i t.issue = i
@ -195,7 +195,7 @@ Given /^the [Uu]ser "([^\"]*)" has 1 time entry with (\d+\.?\d*) hours? at the p
p = Project.find_by_name(project) || Project.find_by_identifier(project) p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin do as_admin do
t = TimeEntry.generate t = TimeEntry.generate
i = WorkPackage.generate_for_project!(p) i = FactoryGirl.create(:work_package, project: p)
t.project = p t.project = p
t.issue = i t.issue = i
t.hours = hours.to_f t.hours = hours.to_f
@ -211,7 +211,7 @@ Given /^the [Pp]roject "([^\"]*)" has (\d+) [tT]ime(?: )?[eE]ntr(?:ies|y) with t
p = Project.find_by_name(project) || Project.find_by_identifier(project) p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin count do as_admin count do
t = TimeEntry.generate t = TimeEntry.generate
i = WorkPackage.generate_for_project!(p) i = FactoryGirl.create(:work_package, project: p)
t.project = p t.project = p
t.work_package = i t.work_package = i
t.activity.project = p t.activity.project = p

@ -27,17 +27,19 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
Then /^I should see a modal window with selector "(.*?)"$/ do |selector| Then(/^I should see a modal window with selector "(.*?)"$/) do |selector|
page.should have_selector(selector) page.should have_selector(selector)
dialog = find(selector) dialog = find(selector)
dialog["class"].include?("ui-dialog-content").should be_true dialog["class"].include?("ui-dialog-content").should be_true
end end
Then(/^I should see the column "(.*?)" before the column "(.*?)" in the timelines table$/) do |content1, content2| Then(/^I should see the column "(.*?)" before the column "(.*?)" in the timelines table$/) do |content1, content2|
steps %Q{ steps %Q{
Then I should see the column "#{content1}" before the column "#{content2}" in ".tl-main-table" Then I should see the column "#{content1}" before the column "#{content2}" in ".tl-main-table"
} }
end end
Then(/^I should see the column "(.*?)" before the column "(.*?)" in "(.*?)"$/) do |content1, content2, table| Then(/^I should see the column "(.*?)" before the column "(.*?)" in "(.*?)"$/) do |content1, content2, table|
#Check that the things really exist and wait until the exist #Check that the things really exist and wait until the exist
steps %Q{ steps %Q{
@ -48,23 +50,25 @@ Then(/^I should see the column "(.*?)" before the column "(.*?)" in "(.*?)"$/) d
elements = find_lowest_containing_element content2, table elements = find_lowest_containing_element content2, table
elements[-1].should have_xpath("preceding::th/descendant-or-self::*[text()='#{content1}']") elements[-1].should have_xpath("preceding::th/descendant-or-self::*[text()='#{content1}']")
end end
Then /^I should see a modal window$/ do
Then(/^I should see a modal window$/) do
steps 'Then I should see a modal window with selector "#modalDiv"' steps 'Then I should see a modal window with selector "#modalDiv"'
end end
Then /^(.*) in the modal$/ do |step| Then(/^(.*) in the modal$/) do |step|
step(step + ' in the iframe "modalIframe"') step(step + ' in the iframe "modalIframe"')
end end
Then(/^I should not see the planning element "(.*?)"$/) do |planning_element_name| Then(/^I should (not )?see the work package "(.*?)" in the timeline$/) do |negate, work_package_name|
steps %Q{ steps %Q{
Then I should not see "#{planning_element_name}" within ".tl-left-main" Then I should #{negate}see "#{work_package_name}" within ".timeline .tl-left-main"
} }
end end
Then(/^the project "(.*?)" should have an indent of (\d+)$/) do |project_name, indent| Then(/^the project "(.*?)" should have an indent of (\d+)$/) do |project_name, indent|
find(".tl-indent-#{indent}", :text => project_name).should_not be_nil find(".tl-indent-#{indent}", :text => project_name).should_not be_nil
end end
Then(/^the project "(.*?)" should follow after "(.*?)"$/) do |project_name_one, project_name_two| Then(/^the project "(.*?)" should follow after "(.*?)"$/) do |project_name_one, project_name_two|
#Check that the things really exist and wait until the exist #Check that the things really exist and wait until the exist
steps %Q{ steps %Q{
@ -75,22 +79,25 @@ Then(/^the project "(.*?)" should follow after "(.*?)"$/) do |project_name_one,
elements = find_lowest_containing_element project_name_one, ".tl-word-ellipsis" elements = find_lowest_containing_element project_name_one, ".tl-word-ellipsis"
elements[-1].should have_xpath("preceding::span[@class='tl-word-ellipsis']/descendant-or-self::*[text()='#{project_name_two}']") elements[-1].should have_xpath("preceding::span[@class='tl-word-ellipsis']/descendant-or-self::*[text()='#{project_name_two}']")
end end
Then(/^I should see the project "(.*?)"$/) do |project_name| Then(/^I should see the project "(.*?)"$/) do |project_name|
steps %Q{ steps %Q{
Then I should see "#{project_name}" within ".tl-left-main" Then I should see "#{project_name}" within ".tl-left-main"
} }
end end
Then(/^I should not see the project "(.*?)"$/) do |project_name| Then(/^I should not see the project "(.*?)"$/) do |project_name|
steps %Q{ steps %Q{
Then I should not see "#{project_name}" within ".tl-left-main" Then I should not see "#{project_name}" within ".tl-left-main"
} }
end end
Then /^the first table column should not take more than 25% of the space$/ do
Then(/^the first table column should not take more than 25% of the space$/) do
result = page.evaluate_script("jQuery('.tl-left-main th').width() < (jQuery('body').width() * 0.25 + 22)") result = page.evaluate_script("jQuery('.tl-left-main th').width() < (jQuery('body').width() * 0.25 + 22)")
result.should be_true result.should be_true
end end
Then /^the "([^"]*)" row should (not )?be marked as default$/ do |title, negation| Then(/^the "([^"]*)" row should (not )?be marked as default$/) do |title, negation|
should_be_visible = !negation should_be_visible = !negation
table_row = find_field(title).find(:xpath, "./ancestor::tr") table_row = find_field(title).find(:xpath, "./ancestor::tr")
@ -105,7 +112,7 @@ Then /^the "([^"]*)" row should (not )?be marked as default$/ do |title, negatio
end end
end end
Then /^I should see that "([^"]*)" is( not)? a milestone and( not)? shown in aggregation$/ do |name, not_milestone, not_in_aggregation| Then(/^I should see that "([^"]*)" is( not)? a milestone and( not)? shown in aggregation$/) do |name, not_milestone, not_in_aggregation|
row = page.find(:css, ".timelines-pet-name", :text => Regexp.new("^#{name}$")).find(:xpath, './ancestor::tr') row = page.find(:css, ".timelines-pet-name", :text => Regexp.new("^#{name}$")).find(:xpath, './ancestor::tr')
nodes = row.all(:css, '.timelines-pet-is_milestone img[alt=checked]') nodes = row.all(:css, '.timelines-pet-is_milestone img[alt=checked]')
@ -123,7 +130,7 @@ Then /^I should see that "([^"]*)" is( not)? a milestone and( not)? shown in agg
end end
end end
Then /^the "([^"]*)" row should (not )?be marked as allowing associations$/ do |title, negation| Then(/^the "([^"]*)" row should (not )?be marked as allowing associations$/) do |title, negation|
should_be_visible = !negation should_be_visible = !negation
table_row = page.all(:css, "table.list tbody tr td", :text => title).first.find(:xpath, "./ancestor::tr") table_row = page.all(:css, "table.list tbody tr td", :text => title).first.find(:xpath, "./ancestor::tr")
@ -135,34 +142,34 @@ Then /^the "([^"]*)" row should (not )?be marked as allowing associations$/ do |
end end
end end
Then /^I should see that "([^"]*)" is a color$/ do |name| Then(/^I should see that "([^"]*)" is a color$/) do |name|
cell = page.all(:css, ".timelines-color-name", :text => name) cell = page.all(:css, ".timelines-color-name", :text => name)
cell.should_not be_empty cell.should_not be_empty
end end
Then /^I should not see the "([^"]*)" color$/ do |name| Then(/^I should not see the "([^"]*)" color$/) do |name|
cell = page.all(:css, ".timelines-color-name", :text => name) cell = page.all(:css, ".timelines-color-name", :text => name)
cell.should be_empty cell.should be_empty
end end
Then /^"([^"]*)" should be the first element in the list$/ do |name| Then(/^"([^"]*)" should be the first element in the list$/) do |name|
should have_selector("table.list tbody tr td", :text => Regexp.new("^#{name}$")) should have_selector("table.list tbody tr td", :text => Regexp.new("^#{name}$"))
end end
Then /^"([^"]*)" should be the last element in the list$/ do |name| Then(/^"([^"]*)" should be the last element in the list$/) do |name|
has_css?("table.list tbody tr td", :text => Regexp.new("^#{name}$")) has_css?("table.list tbody tr td", :text => Regexp.new("^#{name}$"))
end end
Then /^I should see an? (notice|warning|error) flash stating "([^"]*)"$/ do |class_name, message| Then(/^I should see an? (notice|warning|error) flash stating "([^"]*)"$/) do |class_name, message|
page.all(:css, ".flash.#{class_name}, .flash.#{class_name} *", :text => message).should_not be_empty page.all(:css, ".flash.#{class_name}, .flash.#{class_name} *", :text => message).should_not be_empty
end end
Then /^I should see a planning element named "([^"]*)"$/ do |name| Then(/^I should see a planning element named "([^"]*)"$/) do |name|
cells = page.all(:css, "table td.timelines-pe-name *", :text => name) cells = page.all(:css, "table td.timelines-pe-name *", :text => name)
cells.should_not be_empty cells.should_not be_empty
end end
Then /^I should( not)? see "([^"]*)" below "([^"]*)"$/ do |negation, text, heading| Then(/^I should( not)? see "([^"]*)" below "([^"]*)"$/) do |negation, text, heading|
cells = page.all(:css, "h1, h2, h3, h4, h5, h6", :text => heading) cells = page.all(:css, "h1, h2, h3, h4, h5, h6", :text => heading)
cells.should_not be_empty cells.should_not be_empty
@ -175,19 +182,19 @@ Then /^I should( not)? see "([^"]*)" below "([^"]*)"$/ do |negation, text, headi
end end
end end
Then /^I should not be able to add new project associations$/ do Then(/^I should not be able to add new project associations$/) do
link = page.all(:css, "a.timelines-new-project-associations") link = page.all(:css, "a.timelines-new-project-associations")
link.should be_empty link.should be_empty
end end
Then /^I should (not )?see a planning element link for "([^"]*)"$/ do |negate, planning_element_subject| Then(/^I should (not )?see a planning element link for "([^"]*)"$/) do |negate, planning_element_subject|
planning_element = PlanningElement.find_by_subject(planning_element_subject) planning_element = PlanningElement.find_by_subject(planning_element_subject)
text = "*#{planning_element.id}" text = "*#{planning_element.id}"
step %Q{I should #{negate}see "#{text}"} step %Q{I should #{negate}see "#{text}"}
end end
Then /^I should (not )?see the timeline "([^"]*)"$/ do |negate, timeline_name| Then(/^I should (not )?see the timeline "([^"]*)"$/) do |negate, timeline_name|
selector = "div.timeline div.tl-left-main" selector = "div.timeline div.tl-left-main"
timeline = Timeline.find_by_name(timeline_name) timeline = Timeline.find_by_name(timeline_name)

@ -61,6 +61,14 @@ When (/^I make the planning element "([^"]*?)" vertical for the timeline "([^"]*
page.execute_script("jQuery('#content form').submit()") page.execute_script("jQuery('#content form').submit()")
end end
When (/^I edit the settings of the current timeline$/) do
timeline_name = @timeline_name
project_name = @project.name
steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}"
}
end
When (/^I set the first level grouping criteria to "(.*?)" for the timeline "(.*?)" of the project called "(.*?)"$/) do |grouping_project_name, timeline_name, project_name| When (/^I set the first level grouping criteria to "(.*?)" for the timeline "(.*?)" of the project called "(.*?)"$/) do |grouping_project_name, timeline_name, project_name|
steps %Q{ steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}" When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}"
@ -74,11 +82,44 @@ When (/^I set the first level grouping criteria to "(.*?)" for the timeline "(.*
page.execute_script("jQuery('#content form').submit()") page.execute_script("jQuery('#content form').submit()")
end end
When (/^I show only work packages which have the responsible "(.*?)"$/) do |responsible|
steps %Q{
When I edit the settings of the current timeline
}
responsible = User.find_by_login(responsible)
page.execute_script(<<-JavaScript)
jQuery('#timeline_options_planning_element_responsibles').val('#{responsible.id}')
jQuery('#content form').submit()
JavaScript
end
When (/^I show only work packages which have no responsible$/) do
steps %Q{
When I edit the settings of the current timeline
}
page.execute_script(<<-JavaScript)
jQuery('#timeline_options_planning_element_responsibles').val('-1')
jQuery('#content form').submit()
JavaScript
end
When (/^I show only work packages which have the type "(.*?)"$/) do |type|
steps %Q{
When I edit the settings of the current timeline
}
type = Type.find_by_name(type)
page.execute_script(<<-JavaScript)
jQuery('#timeline_options_planning_element_types').val('#{type.id}')
jQuery('#content form').submit()
JavaScript
end
When (/^I show only projects which have a planning element which lies between "(.*?)" and "(.*?)" and has the type "(.*?)"$/) do |start_date, due_date, type| When (/^I show only projects which have a planning element which lies between "(.*?)" and "(.*?)" and has the type "(.*?)"$/) do |start_date, due_date, type|
timeline_name = @timeline_name
project_name = @project.name
steps %Q{ steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}" When I edit the settings of the current timeline
} }
page.should have_selector("#timeline_options_planning_element_time_types", :visible => false) page.should have_selector("#timeline_options_planning_element_time_types", :visible => false)
@ -103,11 +144,9 @@ When (/^I set the second level grouping criteria to "(.*?)" for the timeline "(.
page.execute_script("jQuery('#timeline_options_grouping_two_selection').val('#{project_type.id}')") page.execute_script("jQuery('#timeline_options_grouping_two_selection').val('#{project_type.id}')")
page.execute_script("jQuery('#content form').submit()") page.execute_script("jQuery('#content form').submit()")
end end
When(/^I set the columns shown in the timeline to:$/) do |table| When (/^I set the columns shown in the timeline to:$/) do |table|
timeline_name = @timeline_name
project_name = @project.name
steps %Q{ steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}" When I edit the settings of the current timeline
} }
result = [] result = []
table.raw.each do |_perm| table.raw.each do |_perm|
@ -125,11 +164,10 @@ When(/^I set the columns shown in the timeline to:$/) do |table|
page.execute_script("jQuery('#content form').submit()") page.execute_script("jQuery('#content form').submit()")
end end
When (/^I set the first level grouping criteria to:$/) do |table| When (/^I set the first level grouping criteria to:$/) do |table|
timeline_name = @timeline_name
project_name = @project.name
steps %Q{ steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}" When I edit the settings of the current timeline
} }
result = [] result = []
table.raw.each do |_perm| table.raw.each do |_perm|
@ -150,10 +188,8 @@ When (/^I set the first level grouping criteria to:$/) do |table|
end end
When (/^I set the sortation of the first level grouping criteria to explicit order$/) do When (/^I set the sortation of the first level grouping criteria to explicit order$/) do
timeline_name = @timeline_name
project_name = @project.name
steps %Q{ steps %Q{
When I go to the edit page of the timeline "#{timeline_name}" of the project called "#{project_name}" When I edit the settings of the current timeline
} }
page.should have_selector("#timeline_options_grouping_one_sort", :visible => false) page.should have_selector("#timeline_options_grouping_one_sort", :visible => false)

@ -63,6 +63,28 @@ When /^(.*) within (.*[^:])$/ do |step_name, parent|
with_scope(parent) { step step_name } with_scope(parent) { step step_name }
end end
When(/^I ctrl\-click on "([^\"]+)"$/) do |text|
builder = page.driver.browser.action
#Hold control key down
builder.key_down(:control)
#Click all elements that you want, in this case we click all lis
#Note that you can retrieve the elements using capybara's
# standard methods. When passing them to the builder
# make sure to do .native
elements = page.all('a', :text => text)
elements.each do |e|
builder.click(e.native)
end
#Release control key
builder.key_up(:control)
#Do the action setup
builder.perform
end
# Single-line step scoper # Single-line step scoper
When /^(.*) within_hidden (.*[^:])$/ do |step_name, parent| When /^(.*) within_hidden (.*[^:])$/ do |step_name, parent|
with_scope(parent, visible: false) { step step_name } with_scope(parent, visible: false) { step step_name }
@ -388,6 +410,13 @@ Given /^I (accept|dismiss) the alert dialog$/ do |method|
end end
end end
Then(/^(.*) in the new window$/) do |step|
new_window=page.driver.browser.window_handles.last
page.within_window new_window do
step(step)
end
end
Then /^(.*) in the iframe "([^\"]+)"$/ do |step, iframe_name| Then /^(.*) in the iframe "([^\"]+)"$/ do |step, iframe_name|
browser = page.driver.browser browser = page.driver.browser
browser.switch_to.frame(iframe_name) browser.switch_to.frame(iframe_name)

@ -35,6 +35,9 @@
require 'cucumber/rails' require 'cucumber/rails'
require 'capybara-screenshot/cucumber' require 'capybara-screenshot/cucumber'
# json-spec is used to specifiy our json-apis
require "json_spec/cucumber"
# Load paths to ensure they are loaded before the plugin's paths.rbs. # Load paths to ensure they are loaded before the plugin's paths.rbs.
# Plugin's path_to functions rely on being loaded after the core's path_to # Plugin's path_to functions rely on being loaded after the core's path_to
# function, since they call super if they don't match and the core doesn't. # function, since they call super if they don't match and the core doesn't.
@ -100,3 +103,7 @@ end
# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature
Cucumber::Rails::Database.javascript_strategy = :truncation Cucumber::Rails::Database.javascript_strategy = :truncation
# Capybara.register_driver :selenium do |app|
# Capybara::Selenium::Driver.new(app, :browser => :chrome)
# end
#

@ -65,5 +65,5 @@ Feature: View work packages in a timeline
When I go to the page of the timeline "Testline" of the project called "ecookbook" When I go to the page of the timeline "Testline" of the project called "ecookbook"
And I wait for timeline to load table And I wait for timeline to load table
Then I should see the planning element "Some planning element" in the timeline Then I should see the work package "Some planning element" in the timeline
Then I should see the issue "Some issue" in the timeline Then I should see the work package "Some issue" in the timeline

@ -47,13 +47,13 @@ Feature: Timeline View Tests
And I am already logged in as "manager" And I am already logged in as "manager"
And there are the following planning elements: And there are the following work packages:
| Start date | Due date | description | planning_element_status | responsible | Subject | | Start date | Due date | description | responsible | Subject |
| 2012-01-01 | 2012-01-31 | Avocado Hall | closed | manager | January | | 2012-01-01 | 2012-01-31 | #2 http://google.de | manager | January |
| 2012-02-01 | 2012-02-24 | Avocado Rincon | closed | manager | February | | 2012-02-01 | 2012-02-24 | Avocado Rincon | manager | February |
| 2012-03-01 | 2012-03-30 | Hass | closed | manager | March | | 2012-03-01 | 2012-03-30 | Hass | manager | March |
| 2012-04-01 | 2012-04-30 | Avocado Choquette | closed | manager | April | | 2012-04-01 | 2012-04-30 | Avocado Choquette | manager | April |
| 2012-04-01 | 2012-04-30 | Relish | closed | manager | Loremipsumdolorsitamet,consecteturadipisicingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniam | | 2012-04-01 | 2012-04-30 | Relish | manager | Test2 |
@javascript @javascript
Scenario: planning element click should show modal window Scenario: planning element click should show modal window
@ -63,8 +63,11 @@ Feature: Timeline View Tests
And I click on the Planning Element with name "January" And I click on the Planning Element with name "January"
Then I should see a modal window Then I should see a modal window
And I should see "#1: January" in the modal And I should see "#1: January" in the modal
And I should see "Avocado Hall" in the modal And I should see "http://google.de" in the modal
And I should see "01/01/2012" in the modal And I should see "01/01/2012" in the modal
And I should see "01/31/2012" in the modal And I should see "01/31/2012" in the modal
And I should see "New timeline report" And I should see "New timeline report"
And I should be on the page of the timeline "Testline" of the project called "ecookbook" And I should be on the page of the timeline "Testline" of the project called "ecookbook"
When I ctrl-click on "#2" in the modal
Then I should see "February" in the new window
Then I should see "Avocado Rincon" in the new window

@ -76,7 +76,6 @@ Feature: Timeline View Tests
And I should see the column "Start date" before the column "Type" in the timelines table And I should see the column "Start date" before the column "Type" in the timelines table
And I should see the column "Type" before the column "End date" in the timelines table And I should see the column "Type" before the column "End date" in the timelines table
@javascript @javascript
Scenario: switch timeline Scenario: switch timeline
When there is a timeline "Testline" for project "ecookbook" When there is a timeline "Testline" for project "ecookbook"

@ -0,0 +1,135 @@
#-- 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: Timeline view with filter tests
As an openproject user
I want to view filtered timelines
Background:
Given there is 1 user with:
| login | manager |
And there is a role "manager"
And the role "manager" may have the following rights:
| view_timelines |
| edit_timelines |
| view_work_packages |
And there are the following project types:
| Name |
| Pilot |
And there is a project named "Space Pilot 3000" of type "Pilot"
And I am working in project "Space Pilot 3000"
And the project uses the following modules:
| timelines |
And the user "manager" is a "manager"
And I am already logged in as "manager"
And there are the following types:
| Name | Is Milestone | In aggregation |
| Phase | false | true |
| Milestone | true | true |
And the following types are enabled for projects of type "Pilot"
| Phase |
| Milestone |
And there is a timeline "Storyboard" for project "Space Pilot 3000"
@javascript
Scenario: The timeline w/o filters renders properly
Given there are the following work packages in project "Space Pilot 3000":
| Subject | Start date | Due date | Type | Parent |
| Mission to the moon | 3000-01-02 | 3000-01-03 | Phase | |
| Mom captures Nibblonians | 3000-04-01 | 3000-04-13 | Phase | |
When I go to the page of the timeline of the project called "Space Pilot 3000"
And I wait for timeline to load table
Then I should see the work package "Mission to the moon" in the timeline
And I should see the work package "Mom captures Nibblonians" in the timeline
@javascript
Scenario: The timeline w/ type filters renders properly
Given there are the following work packages in project "Space Pilot 3000":
| Subject | Start date | Due date | Type | Parent |
| Hubert Farnsworth's Birthday | 2841-04-09 | 2841-04-09 | Milestone | |
| Second year | 3000-01-01 | 3000-01-05 | Phase | |
| Hubert Farnsworth's second Birthday | 2842-04-09 | 2842-04-09 | Milestone | Second year |
| Hubert Farnsworth's third Birthday | 2843-04-09 | 2843-04-09 | Milestone | Second year |
And I am working in the timeline "Storyboard" of the project called "Space Pilot 3000"
When I go to the page of the timeline of the project called "Space Pilot 3000"
And I show only work packages which have the type "Milestone"
And I wait for timeline to load table
Then I should see the work package "Hubert Farnsworth's Birthday" in the timeline
Then I should see the work package "Hubert Farnsworth's second Birthday" in the timeline
Then I should see the work package "Hubert Farnsworth's third Birthday" in the timeline
And I should not see the work package "Second year" in the timeline
@javascript
Scenario: The timeline w/ responsibles filters renders properly
Given there is 1 user with:
| Login | hubert |
| Firstname | Hubert |
| Lastname | Farnsworth |
And there are the following work packages in project "Space Pilot 3000":
| Subject | Start date | Due date | Responsible | Parent |
| Hubert Farnsworth's Birthday | 2841-04-09 | 2841-04-09 | hubert | |
| Second year | 3000-01-01 | 3000-01-05 | | |
| Hubert Farnsworth's second Birthday | 2842-04-09 | 2842-04-09 | hubert | Second year |
| Hubert Farnsworth's third Birthday | 2843-04-09 | 2843-04-09 | hubert | Second year |
And I am working in the timeline "Storyboard" of the project called "Space Pilot 3000"
When I go to the page of the timeline of the project called "Space Pilot 3000"
And I show only work packages which have the responsible "hubert"
And I wait for timeline to load table
Then I should see the work package "Hubert Farnsworth's Birthday" in the timeline
And I should see the work package "Hubert Farnsworth's second Birthday" in the timeline
And I should see the work package "Hubert Farnsworth's third Birthday" in the timeline
And I should not see the work package "Second year" in the timeline
@javascript
Scenario: The timeline w/ responsibles filters renders properly
Given there is 1 user with:
| Login | hubert |
| Firstname | Hubert |
| Lastname | Farnsworth |
And there are the following work packages in project "Space Pilot 3000":
| Subject | Start date | Due date | Responsible | Parent |
| Hubert Farnsworth's Birthday | 2841-04-09 | 2841-04-09 | hubert | |
| Second year | 3000-01-01 | 3000-01-05 | | |
| Hubert Farnsworth's second Birthday | 2842-04-09 | 2842-04-09 | hubert | Second year |
| Hubert Farnsworth's third Birthday | 2843-04-09 | 2843-04-09 | hubert | Second year |
And I am working in the timeline "Storyboard" of the project called "Space Pilot 3000"
When I go to the page of the timeline of the project called "Space Pilot 3000"
And I show only work packages which have no responsible
And I wait for timeline to load table
Then I should not see the work package "Hubert Farnsworth's Birthday" in the timeline
And I should not see the work package "Hubert Farnsworth's second Birthday" in the timeline
And I should not see the work package "Hubert Farnsworth's third Birthday" in the timeline
And I should see the work package "Second year" in the timeline

@ -90,12 +90,11 @@ Feature: Timeline View Tests with reporters
| timelines | | timelines |
And there are the following work packages: And there are the following work packages:
| Subject | Start date | Due date | description | status | responsible | type | | Subject | Start date | Due date | description | status | responsible | type |
| January | 2012-01-01 | 2012-01-31 | Aioli Grande | closed | manager | Phase1 | | January | 2012-01-01 | 2012-01-31 | Aioli Grande | closed | manager | Phase1 |
| February | 2012-02-01 | 2012-02-24 | Aioli Sali | closed | manager | Phase2 | | February | 2012-02-01 | 2012-02-24 | Aioli Sali | closed | manager | Phase2 |
| March | 2012-03-01 | 2012-03-30 | Sali Grande | closed | manager | Phase3 | | March | 2012-03-01 | 2012-03-30 | Sali Grande | closed | manager | Phase3 |
| April | 2012-04-01 | 2012-04-30 | Aioli Sali Grande | closed | manager | Phase4 | | April | 2012-04-01 | 2012-04-30 | Aioli Sali Grande | closed | manager | Phase4 |
And there is a project named "ecookbook13" of type "Standard Project" And there is a project named "ecookbook13" of type "Standard Project"
And I am working in project "ecookbook13" And I am working in project "ecookbook13"
@ -105,11 +104,11 @@ Feature: Timeline View Tests with reporters
| timelines | | timelines |
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 |
| January13 | 2013-01-01 | 2013-01-31 | Aioli Grande | closed | manager | | January13 | 2013-01-01 | 2013-01-31 | Aioli Grande | closed | manager |
| February13 | 2013-02-01 | 2013-02-24 | Aioli Sali | closed | manager | | February13 | 2013-02-01 | 2013-02-24 | Aioli Sali | closed | manager |
| March13 | 2013-03-01 | 2013-03-30 | Sali Grande | closed | manager | | March13 | 2013-03-01 | 2013-03-30 | Sali Grande | closed | manager |
| April13 | 2013-04-01 | 2013-04-30 | Aioli Sali Grande | closed | manager | | April13 | 2013-04-01 | 2013-04-30 | Aioli Sali Grande | closed | manager |
And there is a project named "ecookbook_q3" of type "Extraordinary Project" And there is a project named "ecookbook_q3" of type "Extraordinary Project"
And the following types are enabled for projects of type "Extraordinary Project" And the following types are enabled for projects of type "Extraordinary Project"
@ -127,12 +126,10 @@ Feature: Timeline View Tests with reporters
| timelines | | timelines |
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 |
| July | 2012-07-01 | 2013-07-31 | Aioli Grande | closed | manager | | July | 2012-07-01 | 2013-07-31 | Aioli Grande | closed | manager |
| August | 2012-08-01 | 2013-08-31 | Aioli Sali | closed | manager | | August | 2012-08-01 | 2013-08-31 | Aioli Sali | closed | manager |
| Septembre | 2012-09-01 | 2013-09-30 | Sali Grande | closed | manager | | Septembre | 2012-09-01 | 2013-09-30 | Sali Grande | closed | manager |
And there is a project named "ecookbook_empty" of type "Standard Project" And there is a project named "ecookbook_empty" of type "Standard Project"
And I am working in project "ecookbook_empty" And I am working in project "ecookbook_empty"
@ -141,7 +138,6 @@ Feature: Timeline View Tests with reporters
And the project uses the following modules: And the project uses the following modules:
| timelines | | timelines |
And there are the following reportings: And there are the following reportings:
| Project | Reporting To Project | | Project | Reporting To Project |
| ecookbook_empty | ecookbook | | ecookbook_empty | ecookbook |
@ -149,12 +145,10 @@ Feature: Timeline View Tests with reporters
| ecookbook13 | ecookbook | | ecookbook13 | ecookbook |
| ecookbook0 | ecookbook | | ecookbook0 | ecookbook |
And there are the following project associations: And there are the following project associations:
| Project A | Project B | | Project A | Project B |
| ecookbook0 | ecookbook_q3 | | ecookbook0 | ecookbook_q3 |
And I am already logged in as "manager" And I am already logged in as "manager"
@javascript @javascript
@ -202,9 +196,9 @@ Feature: Timeline View Tests with reporters
And I should not see the project "ecookbook_empty" And I should not see the project "ecookbook_empty"
And I should not see the project "ecookbook_q3" And I should not see the project "ecookbook_q3"
And I should not see the project "ecookbook13" And I should not see the project "ecookbook13"
And I should see the planning element "March" in the timeline And I should see the work package "March" in the timeline
And I should not see the planning element "August" in the timeline And I should not see the work package "August" in the timeline
And I should not see the planning element "March13" in the timeline And I should not see the work package "March13" in the timeline
@javascript @javascript
Scenario: First level grouping and sortation Scenario: First level grouping and sortation
@ -212,11 +206,11 @@ Feature: Timeline View Tests with reporters
When there is a timeline "Testline" for project "ecookbook" When there is a timeline "Testline" for project "ecookbook"
And I set the sortation of the first level grouping criteria to explicit order And I set the sortation of the first level grouping criteria to explicit order
And I set the first level grouping criteria to: And I set the first level grouping criteria to:
| ecookbook | | ecookbook |
| ecookbook13 | | ecookbook13 |
And I wait for timeline to load table And I wait for timeline to load table
Then I should see the project "ecookbook_empty" Then I should see the project "ecookbook_empty"
And I should see the project "ecookbook_q3" And I should see the project "ecookbook_q3"
And I should see the project "ecookbook13" And I should see the project "ecookbook13"
And I should see the project "ecookbook0" And I should see the project "ecookbook0"
@ -232,7 +226,7 @@ Feature: Timeline View Tests with reporters
| ecookbook | | ecookbook |
And I wait for timeline to load table And I wait for timeline to load table
Then I should see the project "ecookbook_empty" Then I should see the project "ecookbook_empty"
And I should see the project "ecookbook_q3" And I should see the project "ecookbook_q3"
And I should see the project "ecookbook13" And I should see the project "ecookbook13"
And I should see the project "ecookbook0" And I should see the project "ecookbook0"

@ -49,7 +49,7 @@ module OpenProject
# #
# 2.0.0debian-2 # 2.0.0debian-2
def self.special def self.special
'pre17' 'pre19'
end end
def self.revision def self.revision

@ -0,0 +1,131 @@
#-- 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::StatusesController do
let(:valid_user) { FactoryGirl.create(:user) }
let(:status) {FactoryGirl.create(:issue_status)}
before do
User.stub(:current).and_return valid_user
end
describe 'authentication of index' do
def fetch
get 'index', :format => 'json'
end
it_should_behave_like "a controller action with require_login"
end
describe 'authentication of show' do
def fetch
get 'show', :format => 'json', :id => status.id
end
it_should_behave_like "a controller action with require_login"
end
describe 'looking up a singular status' do
let(:closed){FactoryGirl.create(:issue_status, name: "Closed")}
it 'that does not exist should raise an error' do
get 'show', :id => '0', :format => 'json'
response.response_code.should == 404
end
it 'that exists should return the proper status' do
get 'show', :id => closed.id, :format => 'json'
expect(assigns(:status)).to eql closed
end
end
describe 'looking up statuses' do
let(:open) {FactoryGirl.create(:issue_status, name: "Open")}
let(:in_progress) {FactoryGirl.create(:issue_status, name: "In Progress")}
let(:closed){FactoryGirl.create(:issue_status, name: "Closed")}
let(:no_see_status){FactoryGirl.create(:issue_status, name: "You don't see me.")}
let(:workflows) do
workflows = [FactoryGirl.create(:workflow, old_status: open, new_status: in_progress, role: role),
FactoryGirl.create(:workflow, old_status: in_progress, new_status: closed, role: role)]
end
let(:no_see_workflows) do
workflows = [FactoryGirl.create(:workflow, old_status: closed, new_status: no_see_status, role: role)]
end
let(:project) do
type = FactoryGirl.create(:type, name: "Standard", workflows: workflows)
project = FactoryGirl.create(:project, types: [type])
end
let(:invisible_project) do
invisible_type = FactoryGirl.create(:type, name: "No See", workflows: no_see_workflows)
project = FactoryGirl.create(:project, types: [invisible_type], is_public: false)
end
let(:role) { FactoryGirl.create(:role) }
let(:member) { FactoryGirl.create(:member, :project => project,
:user => valid_user,
:roles => [role]) }
before do
member
workflows
end
describe 'with project-scope' do
it 'with unknown project raises ActiveRecord::RecordNotFound errors' do
get 'index', :project_id => '0', :format => 'json'
expect(response.response_code).to eql 404
end
it "should return the available statuses _only_ for the given project" do
get 'index', :project_id => project.id, :format => 'json'
expect(assigns(:statuses)).to include open, in_progress, closed
expect(assigns(:statuses)).not_to include no_see_status
end
end
describe 'without project-scope' do
it "should return only status for visible projects" do
# create the invisible type/workflow/status
invisible_project
get 'index', :format => 'json'
expect(assigns(:statuses)).to include open, in_progress, closed
expect(assigns(:statuses)).not_to include no_see_status
end
end
end
end

@ -52,7 +52,7 @@ describe VersionsController do
it { response.should be_success } it { response.should be_success }
it { response.should render_template("index") } it { response.should render_template("index") }
it { assert_select "script", :text => Regexp.new(Regexp.escape("new ContextMenu('/issues/context_menu')")) } it { assert_select "script", :text => Regexp.new(Regexp.escape("new ContextMenu('/work_packages/context_menu')")) }
subject { assigns(:versions) } subject { assigns(:versions) }
it "shows Version with no date set" do it "shows Version with no date set" do

@ -0,0 +1,347 @@
#-- 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 WorkPackages::ContextMenusController do
let(:user) { FactoryGirl.create(:user) }
let(:type) { FactoryGirl.create(:type_standard) }
let(:project_1) { FactoryGirl.create(:project,
types: [type]) }
let(:project_2) { FactoryGirl.create(:project,
types: [type],
is_public: false) }
let(:role) { FactoryGirl.create(:role,
permissions: [:view_work_packages,
:add_work_packages,
:edit_work_packages,
:move_work_packages,
:delete_work_packages]) }
let(:member) { FactoryGirl.create(:member,
project: project_1,
principal: user,
roles: [role]) }
let(:status_1) { FactoryGirl.create(:issue_status) }
let(:work_package_1) { FactoryGirl.create(:work_package,
author: user,
type: type,
status: status_1,
project: project_1) }
let(:work_package_2) { FactoryGirl.create(:work_package,
author: user,
type: type,
status: status_1,
project: project_1) }
let(:work_package_3) { FactoryGirl.create(:work_package,
author: user,
type: type,
status: status_1,
project: project_2) }
before do
member
User.stub(:current).and_return user
end
describe :index do
render_views
shared_examples_for "successful response" do
before { get :index, ids: ids }
subject { response }
it { should be_success }
it { should render_template('context_menu') }
end
shared_examples_for :edit do
let(:edit_link) { "/work_packages/#{ids.first}/edit" }
it_behaves_like :edit_impl
end
shared_examples_for :bulk_edit do
let(:edit_link) { "/issues/bulk_edit?#{ids_link}" }
it_behaves_like :edit_impl
end
shared_examples_for :edit_impl do
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: 'Edit',
attributes: { href: edit_link,
:class => 'icon-edit' }
end
end
shared_examples_for :status do
let(:status_2) { FactoryGirl.create(:issue_status) }
let(:status_3) { FactoryGirl.create(:issue_status) }
let(:workflow_1) { FactoryGirl.create(:workflow,
role: role,
type_id: type.id,
old_status: status_1,
new_status: status_2) }
let(:workflow_2) { FactoryGirl.create(:workflow,
role: role,
type_id: type.id,
old_status: status_2,
new_status: status_3) }
before do
workflow_1
workflow_2
get :index, ids: ids
end
let(:status_link) { "/issues/bulk_update?#{ids_link}"\
"&amp;issue%5Bstatus_id%5D=#{status_2.id}" }
it do
assert_tag tag: 'a',
content: status_2.name,
attributes: { href: status_link,
:class => '' }
end
end
shared_examples_for :priority do
let(:priority_immediate) { FactoryGirl.create(:priority_immediate) }
let(:priority_link) { "/issues/bulk_update?#{ids_link}"\
"&amp;issue%5Bpriority_id%5D=#{priority_immediate.id}" }
before do
priority_immediate
get :index, ids: ids
end
it do
assert_tag :tag => 'a',
content: 'Immediate',
attributes: { href: priority_link,
:class => '' }
end
end
shared_examples_for :version do
let(:version_1) { FactoryGirl.create(:version,
project: project_1) }
let(:version_2) { FactoryGirl.create(:version,
project: project_1) }
let(:version_link_1) { "/issues/bulk_update?#{ids_link}"\
"&amp;issue%5Bfixed_version_id%5D=#{version_1.id}" }
let(:version_link_2) { "/issues/bulk_update?#{ids_link}"\
"&amp;issue%5Bfixed_version_id%5D=#{version_2.id}" }
before do
version_1
version_2
get :index, ids: ids
end
it do
assert_tag tag: 'a',
content: version_2.name,
attributes: { href: version_link_2,
:class => '' }
end
end
shared_examples_for :assigned_to do
let(:assigned_to_link) { "/issues/bulk_update?#{ids_link}"\
"&amp;issue%5Bassigned_to_id%5D=#{user.id}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: user.name,
attributes: { href: assigned_to_link,
:class => '' }
end
end
shared_examples_for :duplicate do
let(:duplicate_link) { "/projects/#{project_1.identifier}/work_packages"\
"/new?copy_from=#{ids.first}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: 'Duplicate',
attributes: { href: duplicate_link,
:class => 'icon-duplicate' }
end
end
shared_examples_for :copy do
let(:copy_link) { "/work_packages/move/new?copy_options%5Bcopy%5D=t&amp;"\
"#{ids_link}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: 'Copy',
attributes: { href: copy_link }
end
end
shared_examples_for :move do
let(:move_link) { "/work_packages/move/new?#{ids_link}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: 'Move',
attributes: { href: move_link }
end
end
shared_examples_for :delete do
let(:delete_link) { "/work_packages?#{ids_link}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: 'Delete',
attributes: { href: delete_link }
end
end
context "one work package" do
let(:ids) { [work_package_1.id] }
let(:ids_link) { ids.map {|id| "ids%5B%5D=#{id}"}.join('&amp;') }
it_behaves_like "successful response"
it_behaves_like :edit
it_behaves_like :status
it_behaves_like :priority
it_behaves_like :version
it_behaves_like :assigned_to
it_behaves_like :duplicate
it_behaves_like :copy
it_behaves_like :move
it_behaves_like :delete
context "anonymous user" do
let(:anonymous) { FactoryGirl.create(:anonymous) }
before { User.stub(:current).and_return anonymous }
it_behaves_like "successful response"
describe :delete do
before { get :index, ids: ids }
it { assert_select "a.disabled", :text => /Delete/ }
end
end
end
context "multiple work packages" do
context "in same project" do
let(:ids) { [work_package_1.id, work_package_2.id] }
let(:ids_link) { ids.map {|id| "ids%5B%5D=#{id}"}.join('&amp;') }
it_behaves_like "successful response"
it_behaves_like :bulk_edit
it_behaves_like :status
it_behaves_like :priority
it_behaves_like :assigned_to
it_behaves_like :copy
it_behaves_like :move
it_behaves_like :delete
end
context "in different projects" do
let(:ids) { [work_package_1.id, work_package_2.id, work_package_3.id] }
describe "with project rights" do
let(:ids_link) { ids.map {|id| "ids%5B%5D=#{id}"}.join('&amp;') }
let(:member_2) { FactoryGirl.create(:member,
project: project_2,
principal: user,
roles: [role]) }
before { member_2 }
it_behaves_like "successful response"
it_behaves_like :bulk_edit
it_behaves_like :status
it_behaves_like :priority
it_behaves_like :assigned_to
it_behaves_like :delete
end
describe "w/o project rights" do
it_behaves_like "successful response"
describe :work_packages do
before { get :index, ids: ids }
it { assigns(:work_packages).collect(&:id).should =~ [work_package_1.id, work_package_2.id] }
end
end
end
end
end
end

@ -30,7 +30,7 @@ FactoryGirl.define do
factory :time_entry do factory :time_entry do
project project
user user
work_package :factory => :issue work_package
spent_on Date.today spent_on Date.today
activity :factory => :time_entry_activity activity :factory => :time_entry_activity
hours 1.0 hours 1.0

@ -29,7 +29,7 @@
require 'spec_helper' require 'spec_helper'
describe Query do describe Query do
describe 'available_columns' describe 'available_columns' do
let(:query) { FactoryGirl.build(:query) } let(:query) { FactoryGirl.build(:query) }
context 'with work_package_done_ratio NOT disabled' do context 'with work_package_done_ratio NOT disabled' do
@ -47,4 +47,7 @@ describe Query do
query.available_columns.find {|column| column.name == :done_ratio}.should be_nil query.available_columns.find {|column| column.name == :done_ratio}.should be_nil
end end
end end
end
end end

@ -0,0 +1,42 @@
#-- 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 WorkPackages::AutoCompletesController do
it "should connect GET /work_packages/auto_completes to work_package/auto_complete#index" do
get("/work_packages/auto_complete").should route_to( controller: 'work_packages/auto_completes',
action: 'index' )
end
it "should connect PUT /work_packages/auto_completes to work_package/auto_complete#index" do
get("/work_packages/auto_complete").should route_to( controller: 'work_packages/auto_completes',
action: 'index' )
end
end

@ -0,0 +1,43 @@
#-- 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 WorkPackages::CalendarsController do
it "should connect GET /work_packages/calendar to work_package/calendar#index" do
get("/work_packages/calendar").should route_to( controller: 'work_packages/calendars',
action: 'index' )
end
it "should connect GET /project/1/work_packages/calendar to work_package/calendar#index" do
get("/projects/1/work_packages/calendar").should route_to( controller: 'work_packages/calendars',
action: 'index',
project_id: '1')
end
end

@ -0,0 +1,37 @@
#-- 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 WorkPackages::ContextMenusController do
it "should connect GET /work_packages/context_menu to work_package/context_menu#index" do
get("/work_packages/context_menu").should route_to( controller: 'work_packages/context_menus',
action: 'index' )
end
end

@ -0,0 +1,44 @@
#-- 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 WorkPackagesController do
it "should connect PUT /work_packages/1/preview to work_packages#preview" do
put("/work_packages/1/preview").should route_to( :controller => 'work_packages',
:action => 'preview',
:id => '1' )
end
it "should connect PUT /project/1/work_packages/preview to work_packages#preview" do
put("/projects/1/work_packages/preview").should route_to( :controller => 'work_packages',
:action => 'preview',
:project_id => '1' )
end
end

@ -104,37 +104,4 @@ describe WorkPackagesController do
:action => 'update', :action => 'update',
:id => '1' ) :id => '1' )
end end
it "should connect PUT /work_packages/1/preview to work_packages#preview" do
put("/work_packages/1/preview").should route_to( :controller => 'work_packages',
:action => 'preview',
:id => '1' )
end
it "should connect PUT /project/1/work_packages/preview to work_packages#preview" do
put("/projects/1/work_packages/preview").should route_to( :controller => 'work_packages',
:action => 'preview',
:project_id => '1' )
end
it "should connect GET /work_packages/auto_completes to work_package/auto_complete#index" do
get("/work_packages/auto_complete").should route_to( controller: 'work_packages/auto_completes',
action: 'index' )
end
it "should connect PUT /work_packages/auto_completes to work_package/auto_complete#index" do
get("/work_packages/auto_complete").should route_to( controller: 'work_packages/auto_completes',
action: 'index' )
end
it "should connect GET /work_packages/calendar to work_package/calendar#index" do
get("/work_packages/calendar").should route_to( controller: 'work_packages/calendars',
action: 'index' )
end
it "should connect GET /project/1/work_packages/calendar to work_package/calendar#index" do
get("/projects/1/work_packages/calendar").should route_to( controller: 'work_packages/calendars',
action: 'index',
project_id: '1')
end
end end

@ -33,7 +33,7 @@ class Journal < ActiveRecord::Base
def self.generate_issue def self.generate_issue
project = Project.generate! project = Project.generate!
WorkPackage.generate_for_project!(project) FactoryGirl.create(:work_package, project: project)
end end
def self.generate_user def self.generate_user

@ -1,138 +0,0 @@
#-- 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.
#++
require File.expand_path('../../../test_helper', __FILE__)
class Issues::ContextMenusControllerTest < ActionController::TestCase
fixtures :all
def test_context_menu_one_issue
@request.session[:user_id] = 2
get :issues, :ids => [1]
assert_response :success
assert_template 'context_menu'
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => '/work_packages/1/edit',
:class => 'icon-edit' }
assert_tag :tag => 'a', :content => 'Closed',
:attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5',
:class => '' }
assert_tag :tag => 'a', :content => 'Immediate',
:attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8',
:class => '' }
# Versions
assert_tag :tag => 'a', :content => '2.0',
:attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3',
:class => '' }
assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
:attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4',
:class => '' }
assert_tag :tag => 'a', :content => 'Dave Lopper',
:attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3',
:class => '' }
assert_tag :tag => 'a', :content => 'Duplicate',
:attributes => { :href => '/projects/ecookbook/work_packages/new?copy_from=1',
:class => 'icon-duplicate' }
assert_tag :tag => 'a', :content => 'Copy',
:attributes => { :href => '/work_packages/move/new?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1' }
assert_tag :tag => 'a', :content => 'Move',
:attributes => { :href => '/work_packages/move/new?ids%5B%5D=1'}
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => '/work_packages?ids%5B%5D=1' }
end
def test_context_menu_one_issue_by_anonymous
get :issues, :ids => [1]
assert_response :success
assert_template 'context_menu'
assert_select "a.disabled", :text => /Delete/
end
def test_context_menu_multiple_issues_of_same_project
@request.session[:user_id] = 2
get :issues, :ids => [1, 2]
assert_response :success
assert_template 'context_menu'
assert_not_nil assigns(:issues)
assert_equal [1, 2], assigns(:issues).map(&:id).sort
ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => "/issues/bulk_edit?#{ids}",
:class => 'icon-edit' }
assert_tag :tag => 'a', :content => 'Closed',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
:class => '' }
assert_tag :tag => 'a', :content => 'Immediate',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
:class => '' }
assert_tag :tag => 'a', :content => 'Dave Lopper',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3",
:class => '' }
assert_tag :tag => 'a', :content => 'Copy',
:attributes => { :href => "/work_packages/move/new?copy_options%5Bcopy%5D=t&amp;#{ids}"}
assert_tag :tag => 'a', :content => 'Move',
:attributes => { :href => "/work_packages/move/new?#{ids}"}
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => "/work_packages?#{ids}"}
end
def test_context_menu_multiple_issues_of_different_projects
@request.session[:user_id] = 2
get :issues, :ids => [1, 2, 6]
assert_response :success
assert_template 'context_menu'
assert_not_nil assigns(:issues)
assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => "/issues/bulk_edit?#{ids}",
:class => 'icon-edit' }
assert_tag :tag => 'a', :content => 'Closed',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
:class => '' }
assert_tag :tag => 'a', :content => 'Immediate',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
:class => '' }
assert_tag :tag => 'a', :content => 'John Smith',
:attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2",
:class => '' }
assert_tag :tag => 'a', :content => 'Delete',
:attributes => { :href => "/work_packages?#{ids}"}
end
def test_context_menu_issue_visibility
get :issues, :ids => [1, 4]
assert_response :success
assert_template 'context_menu'
assert_equal [1], assigns(:issues).collect(&:id)
end
end

@ -147,11 +147,6 @@ class RoutingTest < ActionDispatch::IntegrationTest
:format => 'xml') :format => 'xml')
# Extra actions # Extra actions
should route(:get, "/issues/context_menu").to( :controller => 'issues/context_menus',
:action => 'issues')
should route(:post, "/issues/context_menu").to( :controller => 'issues/context_menus',
:action => 'issues')
should route(:get, "/issues/changes").to( :controller => 'journals', should route(:get, "/issues/changes").to( :controller => 'journals',
:action => 'index') :action => 'index')

@ -48,7 +48,7 @@ class TypeTest < ActiveSupport::TestCase
Workflow.create!(:role_id => 1, :type_id => 1, :old_status_id => 2, :new_status_id => 3) Workflow.create!(:role_id => 1, :type_id => 1, :old_status_id => 2, :new_status_id => 3)
Workflow.create!(:role_id => 2, :type_id => 1, :old_status_id => 3, :new_status_id => 5) Workflow.create!(:role_id => 2, :type_id => 1, :old_status_id => 3, :new_status_id => 5)
assert_kind_of Array, type.issue_statuses assert_kind_of Array, type.issue_statuses.all
assert_kind_of IssueStatus, type.issue_statuses.first assert_kind_of IssueStatus, type.issue_statuses.first
assert_equal [2, 3, 5], Type.find(1).issue_statuses.collect(&:id) assert_equal [2, 3, 5], Type.find(1).issue_statuses.collect(&:id)
end end

Loading…
Cancel
Save