diff --git a/Gemfile b/Gemfile index 14fde2f174..0c53e73a3d 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,8 @@ gem 'awesome_nested_set' gem 'color-tools', '~> 1.3.0', :require => 'color' +gem "ruby-progressbar" + gem 'tinymce-rails' gem 'tinymce-rails-langs' @@ -114,6 +116,7 @@ group :test do gem 'ruby-prof' gem 'simplecov', ">= 0.8.pre" gem "shoulda-matchers" + gem "json_spec" end group :ldap do diff --git a/Gemfile.lock b/Gemfile.lock index 0a49613c83..684c837cfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,6 +165,9 @@ GEM jquery-rails railties (>= 3.1.0) json (1.8.0) + json_spec (1.1.1) + multi_json (~> 1.0) + rspec (~> 2.0) launchy (2.3.0) addressable (~> 2.3) letter_opener (1.0.0) @@ -278,6 +281,7 @@ GEM rspec-mocks (~> 2.13.0) ruby-openid (2.2.3) ruby-prof (0.13.0) + ruby-progressbar (1.2.0) rubytree (0.8.3) json (>= 1.7.5) structured_warnings (>= 0.1.3) @@ -379,6 +383,7 @@ DEPENDENCIES jquery-rails (~> 2.0.3) jquery-ui-rails jruby-openssl + json_spec launchy (~> 2.3.0) letter_opener (~> 1.0.0) loofah @@ -409,6 +414,7 @@ DEPENDENCIES rspec-rails (~> 2.0) ruby-openid (~> 2.2.3) ruby-prof + ruby-progressbar rubytree (~> 0.8.3) sass-rails (~> 3.2.3) select2-rails (~> 3.3.2) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cfabbecde9..01c291d29b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -61,6 +61,7 @@ //= require issues //= require work_packages //= require settings +//= require modal //source: http://stackoverflow.com/questions/8120065/jquery-and-prototype-dont-work-together-with-array-prototype-reverse if (typeof []._reverse == 'undefined') { @@ -70,6 +71,20 @@ if (typeof []._reverse == 'undefined') { } 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") { var regions = $.datepicker.regional; var regional = regions[CS.lang] || regions[""]; diff --git a/app/assets/javascripts/context_menu.js b/app/assets/javascripts/context_menu.js index 76814542c1..fc790ce985 100644 --- a/app/assets/javascripts/context_menu.js +++ b/app/assets/javascripts/context_menu.js @@ -144,7 +144,7 @@ ContextMenu.prototype = { {asynchronous:true, method: 'get', evalScripts:true, - parameters:Form.serialize(Event.findElement(e, 'form')), + parameters:jQuery(e.srcElement).closest("form").serialize(), onComplete:function(request){ dims = $('context-menu').getDimensions(); menu_width = dims.width; diff --git a/app/assets/javascripts/modal.js b/app/assets/javascripts/modal.js index 30e265217c..02801f1bdf 100644 --- a/app/assets/javascripts/modal.js +++ b/app/assets/javascripts/modal.js @@ -58,7 +58,6 @@ var ModalHelper = (function() { /** replace all data-modal links and all inside modal links */ body.on("click", "[data-modal]", modalFunction); - modalDiv.on("click", "a", modalFunction); // close when body is clicked body.on("click", ".ui-widget-overlay", jQuery.proxy(modalHelper.close, modalHelper)); @@ -99,8 +98,18 @@ var ModalHelper = (function() { modalDiv.data('changed', false); + var document_host = document.location.href.split("/")[2]; body.on("click", "a", function (e) { 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) { jQuery(e.target).attr("href", modalHelper.tweakLink(url)); } @@ -195,6 +204,11 @@ var ModalHelper = (function() { * @param callback called when done. called with modal div. */ 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; url = this.tweakLink(url); @@ -238,3 +252,4 @@ var ModalHelper = (function() { return ModalHelper; })(); +var modalHelperInstance = new ModalHelper(); diff --git a/app/assets/javascripts/timelines.js b/app/assets/javascripts/timelines.js index 946f26d3d9..c8b18f6d5f 100644 --- a/app/assets/javascripts/timelines.js +++ b/app/assets/javascripts/timelines.js @@ -290,8 +290,7 @@ Timeline = { // prerequisites (3rd party libs) this.checkPrerequisites(); - - this.modalHelper = new ModalHelper(); + this.modalHelper = modalHelperInstance; this.modalHelper.setupTimeline( this, { @@ -305,26 +304,7 @@ Timeline = { timeline.reload(); }); - timelineLoader = 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, - 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() - } - ); + timelineLoader = this.provideTimelineLoader() jQuery(timelineLoader).on('complete', jQuery.proxy(function(e, data) { jQuery.extend(this, data); @@ -348,26 +328,8 @@ Timeline = { }, reload: function() { delete this.lefthandTree; - var timelineLoader = 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, - 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() - } - ); + + var timelineLoader = this.provideTimelineLoader() jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) { @@ -386,6 +348,31 @@ Timeline = { 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) { var timeline = this; var result; @@ -432,6 +419,85 @@ Timeline = { // ╭───────────────────────────────────────────────────────────────────╮ // │ 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 () { /** @@ -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) { this.inChunks(ids, function (projectIdsOfPacket, i) { @@ -1030,26 +1122,31 @@ Timeline = { "/" + projectIdsOfPacket.join(','); + var qsb = new Timeline.FilterQueryStringBuilder( + this.provideServerSideFilterHash()); + + var url = qsb.build(projectPrefix + '/planning_elements.json'); + // load current planning elements. this.loader.register( Timeline.PlanningElement.identifier + '_' + i, - { url : projectPrefix + - '/planning_elements.json?exclude=scenarios' + - this.comparisonCurrentUrlSuffix()}, + { url : url }, { storeIn: Timeline.PlanningElement.identifier } ); + /* TODO! // load historical planning elements. if (this.options.target_time) { this.loader.register( Timeline.HistoricalPlanningElement.identifier + '_' + i, { url : projectPrefix + - '/planning_elements.json?exclude=scenarios' + + '/planning_elements.json' + this.comparisonTargetUrlSuffix() }, { storeIn: Timeline.HistoricalPlanningElement.identifier, readFrom: Timeline.PlanningElement.identifier } ); } + */ }); }; @@ -2085,12 +2182,8 @@ Timeline = { return false; }, filteredOut: function() { - var filtered = this.filteredOutForProjectFilter() || - this.filteredOutForPlanningElementTypes() || - this.filteredOutForResponsibles(); - + var filtered = this.filteredOutForProjectFilter(); this.filteredOut = function() { return filtered; }; - return filtered; }, inTimeFrame: function () { @@ -2099,18 +2192,6 @@ Timeline = { filteredOutForProjectFilter: function() { 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) { // collect all planning elements var r = timeline.planning_elements; @@ -3605,7 +3686,7 @@ Timeline = { getAvailableRows: function() { var timeline = this; 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) { var ptName, petName; if (pt !== undefined) { @@ -3622,8 +3703,13 @@ Timeline = { return jQuery('' + (ptName || petName || "-") + ''); }, - project_status: function(data) { + status: function(data) { var status; + + if (data.planning_element_status) { + status = data.planning_element_status; + } + if (data.getProjectStatus instanceof Function) { status = data.getProjectStatus(); } @@ -4194,7 +4280,7 @@ Timeline = { text = timeline.escape(data.name); if (data.getUrl instanceof Function) { - text = jQuery('').append(text).attr("title", text); + text = jQuery('').append(text).attr("title", text); } if (data.is(Timeline.Project)) { diff --git a/app/assets/javascripts/timelines_modal.js b/app/assets/javascripts/timelines_modal.js index 931a385bc3..0cfe7c44d4 100644 --- a/app/assets/javascripts/timelines_modal.js +++ b/app/assets/javascripts/timelines_modal.js @@ -57,4 +57,4 @@ ModalHelper.prototype.setupTimeline = function(timeline, options) { } } }); -}; \ No newline at end of file +}; diff --git a/app/assets/javascripts/timelines_select_boxes.js b/app/assets/javascripts/timelines_select_boxes.js index 64f8b43170..1cb66d0148 100644 --- a/app/assets/javascripts/timelines_select_boxes.js +++ b/app/assets/javascripts/timelines_select_boxes.js @@ -53,11 +53,17 @@ jQuery(document).ready(function($) { $("#timeline_options_project_status"), $("#timeline_options_project_types"), $("#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_time_types"), - $("#timeline_options_grouping_two_selection") + $("#timeline_options_planning_element_status") ].each(function (item) { - $(item).timelinesAutocomplete({ ajax: {null_element: {id: -1, name: I18n.t("js.timelines.filter.none")}} }) + $(item).timelinesAutocomplete({}); }); var item = $("#timeline_options_columns_"); diff --git a/app/controllers/api/v2/planning_elements_controller.rb b/app/controllers/api/v2/planning_elements_controller.rb index fc24b0c2e3..d168ef64dc 100644 --- a/app/controllers/api/v2/planning_elements_controller.rb +++ b/app/controllers/api/v2/planning_elements_controller.rb @@ -51,6 +51,7 @@ module Api def index optimize_planning_elements_for_less_db_queries + rewire_ancestors respond_to do |format| format.api @@ -169,6 +170,12 @@ module Api # is called as a before filter and as a method def assign_planning_elements(projects = (@projects || [@project])) @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 # remove this and replace by calls it with calls @@ -203,7 +210,7 @@ module Api return if @planning_elements.class == Array # 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 # further db calls. @@ -242,6 +249,34 @@ module Api pe.send(:association_instance_set, :children, children) 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 diff --git a/app/controllers/api/v2/statuses_controller.rb b/app/controllers/api/v2/statuses_controller.rb new file mode 100644 index 0000000000..50f5818f87 --- /dev/null +++ b/app/controllers/api/v2/statuses_controller.rb @@ -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 diff --git a/app/controllers/issues/context_menus_controller.rb b/app/controllers/work_packages/context_menus_controller.rb similarity index 73% rename from app/controllers/issues/context_menus_controller.rb rename to app/controllers/work_packages/context_menus_controller.rb index 0e1662216d..524587fd03 100644 --- a/app/controllers/issues/context_menus_controller.rb +++ b/app/controllers/work_packages/context_menus_controller.rb @@ -27,34 +27,36 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Issues::ContextMenusController < ApplicationController +class WorkPackages::ContextMenusController < ApplicationController - def issues - @issues = WorkPackage.visible.all(:conditions => {:id => params[:ids]}, :include => :project) + def index + @work_packages = WorkPackage.visible.all(order: "#{WorkPackage.table_name}.id", + conditions: {id: params[:ids]}, + include: :project) - if (@issues.size == 1) - @issue = @issues.first - @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + if (@work_packages.size == 1) + @work_package = @work_packages.first + @allowed_statuses = @work_package.new_statuses_allowed_to(User.current) else - @allowed_statuses = @issues.map do |i| + @allowed_statuses = @work_packages.map do |i| i.new_statuses_allowed_to(User.current) end.inject do |memo,s| memo & s end end - @projects = @issues.collect(&:project).compact.uniq + @projects = @work_packages.collect(&:project).compact.uniq @project = @projects.first if @projects.size == 1 @can = {:edit => User.current.allowed_to?(:edit_work_packages, @projects), :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?)), :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) } if @project @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 else #when multiple projects, we only keep the intersection of each set diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index 20d8c2d15b..02524ee2ee 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -28,6 +28,8 @@ #++ class IssueStatus < ActiveRecord::Base + extend Pagination::Model + before_destroy :check_integrity has_many :workflows, :foreign_key => "old_status_id" acts_as_list @@ -41,6 +43,12 @@ class IssueStatus < ActiveRecord::Base 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 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) end @@ -95,6 +103,10 @@ class IssueStatus < ActiveRecord::Base end end + def self.search_scope(query) + like(query) + end + def <=>(status) position <=> status.position end diff --git a/app/models/journal/attachable_journal.rb b/app/models/journal/attachable_journal.rb index 10c45f61db..e6fffaef2f 100644 --- a/app/models/journal/attachable_journal.rb +++ b/app/models/journal/attachable_journal.rb @@ -27,9 +27,8 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::AttachableJournal < ActiveRecord::Base +class Journal::AttachableJournal < Journal::BaseJournal self.table_name = "attachable_journals" - belongs_to :journal belongs_to :attachment end diff --git a/app/models/journal/attachment_journal.rb b/app/models/journal/attachment_journal.rb index b67b6d989c..056a7c68de 100644 --- a/app/models/journal/attachment_journal.rb +++ b/app/models/journal/attachment_journal.rb @@ -27,24 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::AttachmentJournal < ActiveRecord::Base +class Journal::AttachmentJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/base_journal.rb b/app/models/journal/base_journal.rb new file mode 100644 index 0000000000..9149b562ed --- /dev/null +++ b/app/models/journal/base_journal.rb @@ -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 diff --git a/app/models/journal/changeset_journal.rb b/app/models/journal/changeset_journal.rb index a12492bab7..e1b128b266 100644 --- a/app/models/journal/changeset_journal.rb +++ b/app/models/journal/changeset_journal.rb @@ -27,22 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::ChangesetJournal < ActiveRecord::Base +class Journal::ChangesetJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/customizable_journal.rb b/app/models/journal/customizable_journal.rb index 08e84f547f..126fba7937 100644 --- a/app/models/journal/customizable_journal.rb +++ b/app/models/journal/customizable_journal.rb @@ -27,9 +27,8 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::CustomizableJournal < ActiveRecord::Base +class Journal::CustomizableJournal < Journal::BaseJournal self.table_name = "customizable_journals" - belongs_to :journal belongs_to :custom_field, foreign_key: :custom_field_id end diff --git a/app/models/journal/message_journal.rb b/app/models/journal/message_journal.rb index e86a54f7bb..054a7ac55d 100644 --- a/app/models/journal/message_journal.rb +++ b/app/models/journal/message_journal.rb @@ -27,22 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::MessageJournal < ActiveRecord::Base +class Journal::MessageJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/news_journal.rb b/app/models/journal/news_journal.rb index 74fb72ca4c..bb9abe4316 100644 --- a/app/models/journal/news_journal.rb +++ b/app/models/journal/news_journal.rb @@ -27,20 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::NewsJournal < ActiveRecord::Base +class Journal::NewsJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/time_entry_journal.rb b/app/models/journal/time_entry_journal.rb index 4bfd6146f4..c511ab2e42 100644 --- a/app/models/journal/time_entry_journal.rb +++ b/app/models/journal/time_entry_journal.rb @@ -27,24 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::TimeEntryJournal < ActiveRecord::Base +class Journal::TimeEntryJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/wiki_content_journal.rb b/app/models/journal/wiki_content_journal.rb index c0362dde47..62a746f3cd 100644 --- a/app/models/journal/wiki_content_journal.rb +++ b/app/models/journal/wiki_content_journal.rb @@ -27,17 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::WikiContentJournal < ActiveRecord::Base +class Journal::WikiContentJournal < Journal::BaseJournal 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 diff --git a/app/models/journal/work_package_journal.rb b/app/models/journal/work_package_journal.rb index 4c9442d7f3..bd4fafc07c 100644 --- a/app/models/journal/work_package_journal.rb +++ b/app/models/journal/work_package_journal.rb @@ -27,33 +27,6 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Journal::WorkPackageJournal < ActiveRecord::Base +class Journal::WorkPackageJournal < Journal::BaseJournal 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 diff --git a/app/models/query.rb b/app/models/query.rb index 1f6f8d9ddc..e82d482a85 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -29,6 +29,8 @@ class Query < ActiveRecord::Base + @@user_filters = %w{assigned_to_id author_id watcher_id responsible_id}.freeze + belongs_to :project belongs_to :user serialize :filters @@ -86,6 +88,7 @@ class Query < ActiveRecord::Base QueryColumn.new(:subject, :sortable => "#{WorkPackage.table_name}.subject"), QueryColumn.new(:author), 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(: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), @@ -181,12 +184,15 @@ class Query < ActiveRecord::Base author_values += user_values @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] } @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] } @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? # 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 @@ -424,7 +430,7 @@ class Query < ActiveRecord::Base operator = operator_for(field) # "me" value subsitution - if %w(assigned_to_id author_id watcher_id).include?(field) + if @@user_filters.include? field if v.delete("me") if User.current.logged? v.push(User.current.id.to_s) @@ -548,8 +554,12 @@ class Query < ActiveRecord::Base sql = '' case operator when "=" - if value.present? - sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + if value.present? then + 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 # empty set of allowed values produces no result sql = "0=1" diff --git a/app/models/timeline.rb b/app/models/timeline.rb index 2623722535..c4f2604d85 100644 --- a/app/models/timeline.rb +++ b/app/models/timeline.rb @@ -66,9 +66,9 @@ class Timeline < ActiveRecord::Base "compare_to_relative", "compare_to_relative_unit", "comparison", + "exclude_empty", "exclude_own_planning_elements", "exclude_reporters", - "exclude_empty", "exist", "grouping_one_enabled", "grouping_one_selection", @@ -81,19 +81,20 @@ class Timeline < ActiveRecord::Base "initial_outline_expansion", "parents", "planning_element_responsibles", - "planning_element_types", - "planning_element_time_types", + "planning_element_status", + "planning_element_time", "planning_element_time_absolute_one", "planning_element_time_absolute_two", "planning_element_time_relative_one", - "planning_element_time_relative_two", "planning_element_time_relative_one_unit", + "planning_element_time_relative_two", "planning_element_time_relative_two_unit", - "planning_element_time", + "planning_element_time_types", + "planning_element_types", "project_responsibles", + "project_sort", "project_status", "project_types", - "project_sort", "timeframe_end", "timeframe_start", "vertical_planning_elements", @@ -105,7 +106,7 @@ class Timeline < ActiveRecord::Base "start_date", "end_date", "responsible", - "project_status" + "status" ] @@available_zoom_factors = [ @@ -187,6 +188,17 @@ class Timeline < ActiveRecord::Base Type.find(:all, :order => :name) 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 resolve_with_none_element(:planning_element_types) do |ary| Type.find(ary) diff --git a/app/models/type.rb b/app/models/type.rb index 5e6e1c6ccf..04d1990660 100644 --- a/app/models/type.rb +++ b/app/models/type.rb @@ -83,21 +83,17 @@ class Type < ActiveRecord::Base find(:all, :order => 'position') end - # Returns an array of IssueStatus that are used - # in the type's workflows - def issue_statuses - if @issue_statuses - return @issue_statuses - elsif new_record? - return [] + def self.issue_statuses(types) + workflow_table, status_table = [Workflow, IssueStatus].map(&:arel_table) + old_id_subselect, new_id_subselect = [:old_status_id, :new_status_id].map do |foreign_key| + workflow_table.project(workflow_table[foreign_key]).where(workflow_table[:type_id].in(types)) end + IssueStatus.where(status_table[:id].in(old_id_subselect).or(status_table[:id].in(new_id_subselect))) + end - ids = Workflow. - connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE type_id = #{id}"). - flatten. - uniq - - @issue_statuses = IssueStatus.find_all_by_id(ids).sort + def issue_statuses + return [] if new_record? + @issue_statuses ||= Type.issue_statuses([id]) end def self.search_scope(query) diff --git a/app/models/work_package.rb b/app/models/work_package.rb index 50474ae9ea..70c216122e 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -626,6 +626,65 @@ class WorkPackage < ActiveRecord::Base return done_date <= Date.today 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 def recalculate_attributes_for(work_package_id) @@ -737,65 +796,6 @@ class WorkPackage < ActiveRecord::Base projects 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) alias_method_chain(:attributes=, :type_first) unless method_defined?(:attributes_without_type_first=) diff --git a/features/step_definitions/timelines_steps.rb b/app/views/api/v2/statuses/_status.api.rsb similarity index 71% rename from features/step_definitions/timelines_steps.rb rename to app/views/api/v2/statuses/_status.api.rsb index 8855ef070a..a8df392c12 100644 --- a/features/step_definitions/timelines_steps.rb +++ b/app/views/api/v2/statuses/_status.api.rsb @@ -1,4 +1,3 @@ -#encoding: utf-8 #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2013 the OpenProject Foundation (OPF) @@ -27,15 +26,13 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -# Merge those two once Issue == PlanningElement == WorkPackage -Then(/^I should (not )?see the planning element "(.*?)" in the timeline$/) do |negate, planning_element_name| - steps %Q{ - Then I should #{negate}see "#{planning_element_name}" within ".timeline .tl-left-main" - } -end +api.status do + api.id(status.id) + + api.name(status.name) -Then(/^I should (not )?see the issue "(.*?)" in the timeline$/) do |negate, issue_name| - steps %Q{ - Then I should #{negate}see "#{issue_name}" within ".timeline .tl-left-main" - } + api.position(status.position) + api.is_default(status.is_default) + api.is_closed(status.is_closed) + api.default_done_ratio(status.default_done_ratio) end diff --git a/app/views/api/v2/statuses/index.api.rsb b/app/views/api/v2/statuses/index.api.rsb new file mode 100644 index 0000000000..3a41194215 --- /dev/null +++ b/app/views/api/v2/statuses/index.api.rsb @@ -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 diff --git a/app/views/api/v2/statuses/show.api.rsb b/app/views/api/v2/statuses/show.api.rsb new file mode 100644 index 0000000000..0655106634 --- /dev/null +++ b/app/views/api/v2/statuses/show.api.rsb @@ -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) diff --git a/app/views/timelines/_timeline.html.erb b/app/views/timelines/_timeline.html.erb index 47ef2807ef..1212129e67 100644 --- a/app/views/timelines/_timeline.html.erb +++ b/app/views/timelines/_timeline.html.erb @@ -51,7 +51,7 @@ See doc/COPYRIGHT.rdoc for more details. 'timelines.filter.column.end_date', 'timelines.filter.column.name', 'timelines.filter.column.type', - 'timelines.filter.column.project_status', + 'timelines.filter.column.status', 'timelines.filter.column.responsible', 'timelines.filter.column.start_date', '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 'Bitstream_Vera_Sans_400.font.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' %> <% @timeline_header_included = true %> <% end %> diff --git a/app/views/timelines/filter/_planning_elements.html.erb b/app/views/timelines/filter/_planning_elements.html.erb index 9227f61a7c..af0547bbb0 100644 --- a/app/views/timelines/filter/_planning_elements.html.erb +++ b/app/views/timelines/filter/_planning_elements.html.erb @@ -44,6 +44,33 @@ See doc/COPYRIGHT.rdoc for more details.

+

+ <%= 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 %> +

+

<%= label_tag 'timeline_options_planning_element_types', l("timelines.filter.types") %> diff --git a/app/views/versions/index.html.erb b/app/views/versions/index.html.erb index 079f9de286..1db7972e28 100644 --- a/app/views/versions/index.html.erb +++ b/app/views/versions/index.html.erb @@ -84,4 +84,4 @@ See doc/COPYRIGHT.rdoc for more details. <% html_title(l(:label_roadmap)) %> -<%= context_menu issues_context_menu_path %> +<%= context_menu work_packages_context_menu_path %> diff --git a/app/views/issues/context_menus/issues.html.erb b/app/views/work_packages/context_menus/index.html.erb similarity index 84% rename from app/views/issues/context_menus/issues.html.erb rename to app/views/work_packages/context_menus/index.html.erb index ccd5d169af..84ca66127a 100644 --- a/app/views/issues/context_menus/issues.html.erb +++ b/app/views/work_packages/context_menus/index.html.erb @@ -28,23 +28,23 @@ See doc/COPYRIGHT.rdoc for more details. ++#%>

diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index 64890c41b9..4dda0e5ed5 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -81,7 +81,7 @@ See doc/COPYRIGHT.rdoc for more details. :update => "content", :method => :get, :complete => "apply_filters_observer()", - :with => "Form.serialize('query_form')" + :with => "jQuery('#query_form').serialize()" }, :class => 'icon icon-checked' %> <%= link_to_remote l(:button_clear), @@ -91,7 +91,7 @@ See doc/COPYRIGHT.rdoc for more details. }, :class => 'icon icon-reload' %> <% 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 %> @@ -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)) %> <% end %> -<%= context_menu issues_context_menu_path %> +<%= context_menu work_packages_context_menu_path %> diff --git a/app/views/work_packages/show.html.erb b/app/views/work_packages/show.html.erb index bd01542517..1b2a519f60 100644 --- a/app/views/work_packages/show.html.erb +++ b/app/views/work_packages/show.html.erb @@ -154,7 +154,7 @@ See doc/COPYRIGHT.rdoc for more details. <%= stylesheet_link_tag 'context_menu_rtl' if l(:direction) == 'rtl' %> <% end %> -<%= 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_headers_tags -%> diff --git a/config/locales/de.yml b/config/locales/de.yml index 38788ddc4f..ce4082a738 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1361,7 +1361,7 @@ de: type: "Typ" end_date: "Abschlussdatum" name: "Name" - project_status: "Projekt-Status" + status: "Status" responsible: "Planungsverantwortlicher" start_date: "Startdatum" columns: "Spalten" @@ -1402,6 +1402,8 @@ de: project_responsible: "Projekte von diesem Planungsverantwortlichen anzeigen" project_status: "Projekt-Status anzeigen" project_types: "Projekt-Typ anzeigen" + types: "Typ anzeigen" + status: "Status anzeigen" timeframe: "Zeitausschnitt bestimmen" timeframe_end: "bis" timeframe_start: "von" diff --git a/config/locales/en.yml b/config/locales/en.yml index 6f6f1cf728..134bd3b248 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1340,7 +1340,7 @@ en: type: "Type" end_date: "End date" name: "Name" - project_status: "Project status" + status: "Status" responsible: "Responsible" start_date: "Start date" columns: "Columns" @@ -1374,6 +1374,7 @@ en: planning_element_filters: "Filter planning elements" planning_element_responsible: "Show planning elements with responsible" 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_historical: "from %{startdate} to %{enddate}" diff --git a/config/routes.rb b/config/routes.rb index 3b6e5dbf24..af31733898 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,7 +58,11 @@ OpenProject::Application.routes.draw do resources :authentication 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 :planning_element_types do collection do @@ -76,6 +80,7 @@ OpenProject::Application.routes.draw do get :paginate_reported_project_statuses end end + resources :statuses, :only => [:index, :show] resources :timelines resources :projects do @@ -86,6 +91,7 @@ OpenProject::Application.routes.draw do resources :project_associations do get :available_projects, :on => :collection end + resources :statuses, :only => [:index, :show] end end @@ -294,6 +300,7 @@ OpenProject::Application.routes.draw do namespace :work_packages do 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] end diff --git a/db/migrate/20130916094339_legacy_issues_to_work_packages.rb b/db/migrate/20130916094339_legacy_issues_to_work_packages.rb index 336cd8f5a5..127b380e28 100644 --- a/db/migrate/20130916094339_legacy_issues_to_work_packages.rb +++ b/db/migrate/20130916094339_legacy_issues_to_work_packages.rb @@ -19,13 +19,12 @@ class LegacyIssuesToWorkPackages < ActiveRecord::Migration def up raise_on_existing_work_package_entries - copy_legacy_issues_to_work_packages + reset_public_key_sequence_in_postgres end def down raise_on_existing_legacy_issue_entries - copy_work_packages_to_legacy_issues end @@ -45,6 +44,11 @@ class LegacyIssuesToWorkPackages < ActiveRecord::Migration 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 execute <<-SQL INSERT INTO work_packages diff --git a/db/migrate/20130920081135_legacy_attachment_journal_data.rb b/db/migrate/20130920081135_legacy_attachment_journal_data.rb new file mode 100644 index 0000000000..2138be3a50 --- /dev/null +++ b/db/migrate/20130920081135_legacy_attachment_journal_data.rb @@ -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 diff --git a/db/migrate/20130920085055_legacy_changeset_journal_data.rb b/db/migrate/20130920085055_legacy_changeset_journal_data.rb new file mode 100644 index 0000000000..1edb1cfd23 --- /dev/null +++ b/db/migrate/20130920085055_legacy_changeset_journal_data.rb @@ -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 diff --git a/db/migrate/20130920090201_legacy_news_journal_data.rb b/db/migrate/20130920090201_legacy_news_journal_data.rb new file mode 100644 index 0000000000..f748ff7d0b --- /dev/null +++ b/db/migrate/20130920090201_legacy_news_journal_data.rb @@ -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 diff --git a/db/migrate/20130920090641_legacy_message_journal_data.rb b/db/migrate/20130920090641_legacy_message_journal_data.rb new file mode 100644 index 0000000000..d78f9e3b71 --- /dev/null +++ b/db/migrate/20130920090641_legacy_message_journal_data.rb @@ -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 diff --git a/db/migrate/20130920092800_legacy_time_entry_journal_data.rb b/db/migrate/20130920092800_legacy_time_entry_journal_data.rb new file mode 100644 index 0000000000..e01bdb9900 --- /dev/null +++ b/db/migrate/20130920092800_legacy_time_entry_journal_data.rb @@ -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 diff --git a/db/migrate/20130920093823_legacy_wiki_content_journal_data.rb b/db/migrate/20130920093823_legacy_wiki_content_journal_data.rb new file mode 100644 index 0000000000..4d471104cb --- /dev/null +++ b/db/migrate/20130920093823_legacy_wiki_content_journal_data.rb @@ -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 diff --git a/db/migrate/20130920094524_legacy_issue_journal_data.rb b/db/migrate/20130920094524_legacy_issue_journal_data.rb new file mode 100644 index 0000000000..a8b686560d --- /dev/null +++ b/db/migrate/20130920094524_legacy_issue_journal_data.rb @@ -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 diff --git a/db/migrate/20130920095747_legacy_planning_element_journal_data.rb b/db/migrate/20130920095747_legacy_planning_element_journal_data.rb new file mode 100644 index 0000000000..2226953d1d --- /dev/null +++ b/db/migrate/20130920095747_legacy_planning_element_journal_data.rb @@ -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 diff --git a/db/migrate/20130920142714_update_attachment_container.rb b/db/migrate/20130920142714_update_attachment_container.rb new file mode 100644 index 0000000000..96da900831 --- /dev/null +++ b/db/migrate/20130920142714_update_attachment_container.rb @@ -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 diff --git a/db/migrate/20130920150143_journal_activities_data.rb b/db/migrate/20130920150143_journal_activities_data.rb new file mode 100644 index 0000000000..b8afeb14c8 --- /dev/null +++ b/db/migrate/20130920150143_journal_activities_data.rb @@ -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 diff --git a/db/migrate/migration_utils/db_worker.rb b/db/migrate/migration_utils/db_worker.rb new file mode 100644 index 0000000000..c108c1a501 --- /dev/null +++ b/db/migrate/migration_utils/db_worker.rb @@ -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 diff --git a/db/migrate/migration_utils/journal_migrator_concerns.rb b/db/migrate/migration_utils/journal_migrator_concerns.rb new file mode 100644 index 0000000000..0854d7ed36 --- /dev/null +++ b/db/migrate/migration_utils/journal_migrator_concerns.rb @@ -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 diff --git a/db/migrate/migration_utils/legacy_journal_migrator.rb b/db/migrate/migration_utils/legacy_journal_migrator.rb new file mode 100644 index 0000000000..9f7f521010 --- /dev/null +++ b/db/migrate/migration_utils/legacy_journal_migrator.rb @@ -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 diff --git a/db/migrate/migration_utils/legacy_table_checker.rb b/db/migrate/migration_utils/legacy_table_checker.rb new file mode 100644 index 0000000000..25534c6203 --- /dev/null +++ b/db/migrate/migration_utils/legacy_table_checker.rb @@ -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 diff --git a/db/migrate/migration_utils/utils.rb b/db/migrate/migration_utils/utils.rb index 2cb828818c..cf717ad933 100644 --- a/db/migrate/migration_utils/utils.rb +++ b/db/migrate/migration_utils/utils.rb @@ -21,7 +21,7 @@ module Migration def update_column_values(table, column_list, updater, conditions) updated_rows = [] - + select_rows_from_database(table, column_list, conditions).each do |row| updated_rows << updater.call(row) end diff --git a/db/seeds/production.rb b/db/seeds/production.rb index 743feef79e..dea9dae805 100644 --- a/db/seeds/production.rb +++ b/db/seeds/production.rb @@ -26,4 +26,33 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -# add seeds specific for the production-environment here \ No newline at end of file +# 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 diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6788ae0937..ffab7bbab1 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -29,11 +29,35 @@ See doc/COPYRIGHT.rdoc for more details. # 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 * `#1770` New Comment Section layout errors * `#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 -* `#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 @@ -63,15 +87,16 @@ See doc/COPYRIGHT.rdoc for more details. * `#1850` Disable atom feeds via setting * `#1874` Move Scopes from Issue into Workpackage * `#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 * `#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 +* `#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 * `#1351` Generalize Modal Creation -# `#1557` Timeline Report Selection Not Visible * `#1755` Migrate helper-tests for issues into specs for work package * `#1766` Fixed bug: Viewing diff of Work Package description results in error 500 * `#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 * `#1876` Timelines do not show work packages when there is no status reporting * `#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 * `#1918` Custom fields are not displayed when issue is created ## 3.0.0pre14 -* `#1873` Move Validations from Issue into Workpackage -* `#825` Migrate Duration -* `#828` Remove Alternate Dates +* `#825` Migrate Duration +* `#828` Remove Alternate Dates * `#1421` Adapt issue created/updated wording to apply to work packages * `#1610` Move Planning Element Controller to API V2 * `#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 * `#1813` Run Data Generator on old AAJ schema * `#1859` Fix 20130814130142 down-migration (remove_documents) +* `#1873` Move Validations from Issue into Workpackage ## 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 * `#1541` Use Rails 3.2.14 instead of Git Branch * `#1595` Cleanup action menu for work packages -* `#1598` Switching type of work package looses inserted data * `#1596` Copy/Move work packages between projects +* `#1598` Switching type of work package looses inserted data * `#1618` Deactivate modal dialogs and respective cukes * `#1637` Removed files module * `#1648` Arbitrarily failing cuke: Navigating to the timeline page ## 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 * `#1247` Implement uniform "update" action for pe & issues * `#1411` Migrate database tables into the new model @@ -140,43 +162,45 @@ See doc/COPYRIGHT.rdoc for more details. * `#1437` Update seed data * `#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 +* `#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) ## 3.0.0pre9 -* `#1517` Journal changed_data cannot contain the changes of a wiki_content content * `#779` Integrate password expiration -* `#1461` Integration Activity Plugin -* `#1505` Removing all roles from a membership removes the project membership +* `#1314` Always set last activity timestamp and check session expiry if ttl-setting is enabled +* `#1371` Changing pagination per_page_param does not change page * `#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 -* `#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 +* `#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 +* `#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 -* `#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 -* `#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 +* `#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 +* `#778` Integrate ban of former passwords +* `#780` Add password brute force prevention * `#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 -* `#780` Add password brute force prevention +* `#1209` Fix adding watcher to issue * `#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 * `#1301` Ajax call when logged out should open a popup window -* `#778` Integrate ban of former passwords -* `#1209` Fix adding watcher to issue -* `#1034` Create changelog and document format +* `#1303` Watcherlist contains unescaped HTML +* `#1315` Correct spelling mistakes in German translation diff --git a/features/planning_elements/filter.feature b/features/planning_elements/filter.feature new file mode 100644 index 0000000000..47c3dcec5d --- /dev/null +++ b/features/planning_elements/filter.feature @@ -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" + + diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb new file mode 100644 index 0000000000..a2f6cbe1b0 --- /dev/null +++ b/features/step_definitions/api_steps.rb @@ -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 diff --git a/features/step_definitions/general_steps.rb b/features/step_definitions/general_steps.rb index 57ca7cc474..168b29953f 100644 --- a/features/step_definitions/general_steps.rb +++ b/features/step_definitions/general_steps.rb @@ -181,7 +181,7 @@ Given /^the [Uu]ser "([^\"]*)" has 1 time [eE]ntry$/ do |user| u = User.find_by_login user p = u.projects.last 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.user = u 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) as_admin do t = TimeEntry.generate - i = WorkPackage.generate_for_project!(p) + i = FactoryGirl.create(:work_package, project: p) t.project = p t.issue = i 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) as_admin count do t = TimeEntry.generate - i = WorkPackage.generate_for_project!(p) + i = FactoryGirl.create(:work_package, project: p) t.project = p t.work_package = i t.activity.project = p diff --git a/features/step_definitions/timelines_then_steps.rb b/features/step_definitions/timelines_then_steps.rb index d53e67f789..ae165756c7 100644 --- a/features/step_definitions/timelines_then_steps.rb +++ b/features/step_definitions/timelines_then_steps.rb @@ -27,17 +27,19 @@ # 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) dialog = find(selector) dialog["class"].include?("ui-dialog-content").should be_true end + Then(/^I should see the column "(.*?)" before the column "(.*?)" in the timelines table$/) do |content1, content2| steps %Q{ Then I should see the column "#{content1}" before the column "#{content2}" in ".tl-main-table" } end + 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 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[-1].should have_xpath("preceding::th/descendant-or-self::*[text()='#{content1}']") 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"' end -Then /^(.*) in the modal$/ do |step| +Then(/^(.*) in the modal$/) do |step| step(step + ' in the iframe "modalIframe"') 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{ - 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 Then(/^the project "(.*?)" should have an indent of (\d+)$/) do |project_name, indent| find(".tl-indent-#{indent}", :text => project_name).should_not be_nil end + Then(/^the project "(.*?)" should follow after "(.*?)"$/) do |project_name_one, project_name_two| #Check that the things really exist and wait until the exist 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[-1].should have_xpath("preceding::span[@class='tl-word-ellipsis']/descendant-or-self::*[text()='#{project_name_two}']") end + Then(/^I should see the project "(.*?)"$/) do |project_name| steps %Q{ Then I should see "#{project_name}" within ".tl-left-main" } end + Then(/^I should not see the project "(.*?)"$/) do |project_name| steps %Q{ Then I should not see "#{project_name}" within ".tl-left-main" } 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.should be_true 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 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 -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') 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 -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 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 -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.should_not be_empty 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.should be_empty 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}$")) 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}$")) 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 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.should_not be_empty 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.should_not be_empty @@ -175,19 +182,19 @@ Then /^I should( not)? see "([^"]*)" below "([^"]*)"$/ do |negation, text, headi 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.should be_empty 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) text = "*#{planning_element.id}" step %Q{I should #{negate}see "#{text}"} 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" timeline = Timeline.find_by_name(timeline_name) diff --git a/features/step_definitions/timelines_when_steps.rb b/features/step_definitions/timelines_when_steps.rb index 27134ff4fc..15f82e88a3 100644 --- a/features/step_definitions/timelines_when_steps.rb +++ b/features/step_definitions/timelines_when_steps.rb @@ -61,6 +61,14 @@ When (/^I make the planning element "([^"]*?)" vertical for the timeline "([^"]* page.execute_script("jQuery('#content form').submit()") 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| steps %Q{ 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()") 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| - 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}" + When I edit the settings of the current timeline } 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('#content form').submit()") end -When(/^I set the columns shown in the timeline to:$/) do |table| - timeline_name = @timeline_name - project_name = @project.name +When (/^I set the columns shown in the timeline to:$/) do |table| 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 = [] 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()") end + When (/^I set the first level grouping criteria to:$/) do |table| - 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}" + When I edit the settings of the current timeline } result = [] table.raw.each do |_perm| @@ -150,10 +188,8 @@ When (/^I set the first level grouping criteria to:$/) do |table| end 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{ - 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) diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb index bf7e859323..a893737942 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -63,6 +63,28 @@ When /^(.*) within (.*[^:])$/ do |step_name, parent| with_scope(parent) { step step_name } 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 When /^(.*) within_hidden (.*[^:])$/ do |step_name, parent| with_scope(parent, visible: false) { step step_name } @@ -388,6 +410,13 @@ Given /^I (accept|dismiss) the alert dialog$/ do |method| 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| browser = page.driver.browser browser.switch_to.frame(iframe_name) diff --git a/features/support/env.rb b/features/support/env.rb index 273cb84aaa..4dbc887920 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -35,6 +35,9 @@ require 'cucumber/rails' 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. # 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. @@ -100,3 +103,7 @@ end # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature Cucumber::Rails::Database.javascript_strategy = :truncation +# Capybara.register_driver :selenium do |app| +# Capybara::Selenium::Driver.new(app, :browser => :chrome) +# end +# diff --git a/features/timelines/show.feature b/features/timelines/show.feature index 1ece59d6ca..3879664ac0 100644 --- a/features/timelines/show.feature +++ b/features/timelines/show.feature @@ -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" 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 issue "Some issue" in the timeline + Then I should see the work package "Some planning element" in the timeline + Then I should see the work package "Some issue" in the timeline diff --git a/features/timelines/timeline_modal_views.feature.disabled b/features/timelines/timeline_modal_views.feature similarity index 59% rename from features/timelines/timeline_modal_views.feature.disabled rename to features/timelines/timeline_modal_views.feature index 951a767412..d90f0c725f 100644 --- a/features/timelines/timeline_modal_views.feature.disabled +++ b/features/timelines/timeline_modal_views.feature @@ -47,13 +47,13 @@ Feature: Timeline View Tests And I am already logged in as "manager" - And there are the following planning elements: - | Start date | Due date | description | planning_element_status | responsible | Subject | - | 2012-01-01 | 2012-01-31 | Avocado Hall | closed | manager | January | - | 2012-02-01 | 2012-02-24 | Avocado Rincon | closed | manager | February | - | 2012-03-01 | 2012-03-30 | Hass | closed | manager | March | - | 2012-04-01 | 2012-04-30 | Avocado Choquette | closed | manager | April | - | 2012-04-01 | 2012-04-30 | Relish | closed | manager | Loremipsumdolorsitamet,consecteturadipisicingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniam | + And there are the following work packages: + | Start date | Due date | description | responsible | Subject | + | 2012-01-01 | 2012-01-31 | #2 http://google.de | manager | January | + | 2012-02-01 | 2012-02-24 | Avocado Rincon | manager | February | + | 2012-03-01 | 2012-03-30 | Hass | manager | March | + | 2012-04-01 | 2012-04-30 | Avocado Choquette | manager | April | + | 2012-04-01 | 2012-04-30 | Relish | manager | Test2 | @javascript 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" Then I should see a modal window 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/31/2012" in the modal And I should see "New timeline report" 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 \ No newline at end of file diff --git a/features/timelines/timeline_view.feature b/features/timelines/timeline_view.feature index 8514086cf6..1339be448c 100644 --- a/features/timelines/timeline_view.feature +++ b/features/timelines/timeline_view.feature @@ -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 "Type" before the column "End date" in the timelines table - @javascript Scenario: switch timeline When there is a timeline "Testline" for project "ecookbook" diff --git a/features/timelines/timeline_view_with_filters.feature b/features/timelines/timeline_view_with_filters.feature new file mode 100644 index 0000000000..f23fe14688 --- /dev/null +++ b/features/timelines/timeline_view_with_filters.feature @@ -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 + diff --git a/features/timelines/timeline_view_with_reporters.feature b/features/timelines/timeline_view_with_reporters.feature index 0e8bf3d667..a16e61b62a 100644 --- a/features/timelines/timeline_view_with_reporters.feature +++ b/features/timelines/timeline_view_with_reporters.feature @@ -90,12 +90,11 @@ Feature: Timeline View Tests with reporters | timelines | And there are the following work packages: - | Subject | Start date | Due date | description | status | responsible | type | - | January | 2012-01-01 | 2012-01-31 | Aioli Grande | closed | manager | Phase1 | - | February | 2012-02-01 | 2012-02-24 | Aioli Sali | closed | manager | Phase2 | - | 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 | - + | Subject | Start date | Due date | description | status | responsible | type | + | January | 2012-01-01 | 2012-01-31 | Aioli Grande | closed | manager | Phase1 | + | February | 2012-02-01 | 2012-02-24 | Aioli Sali | closed | manager | Phase2 | + | 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 | And there is a project named "ecookbook13" of type "Standard Project" And I am working in project "ecookbook13" @@ -105,11 +104,11 @@ Feature: Timeline View Tests with reporters | timelines | And there are the following work packages: - | Subject | Start date | Due date | description | status | responsible | - | January13 | 2013-01-01 | 2013-01-31 | Aioli Grande | closed | manager | - | February13 | 2013-02-01 | 2013-02-24 | Aioli Sali | 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 | + | Subject | Start date | Due date | description | status | responsible | + | January13 | 2013-01-01 | 2013-01-31 | Aioli Grande | closed | manager | + | February13 | 2013-02-01 | 2013-02-24 | Aioli Sali | 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 | And there is a project named "ecookbook_q3" 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 | And there are the following work packages: - | Subject | Start date | Due date | description | status | responsible | - | July | 2012-07-01 | 2013-07-31 | Aioli Grande | closed | manager | - | August | 2012-08-01 | 2013-08-31 | Aioli Sali | closed | manager | - | Septembre | 2012-09-01 | 2013-09-30 | Sali Grande | closed | manager | - - + | Subject | Start date | Due date | description | status | responsible | + | July | 2012-07-01 | 2013-07-31 | Aioli Grande | closed | manager | + | August | 2012-08-01 | 2013-08-31 | Aioli Sali | 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 I am working in project "ecookbook_empty" @@ -141,7 +138,6 @@ Feature: Timeline View Tests with reporters And the project uses the following modules: | timelines | - And there are the following reportings: | Project | Reporting To Project | | ecookbook_empty | ecookbook | @@ -149,12 +145,10 @@ Feature: Timeline View Tests with reporters | ecookbook13 | ecookbook | | ecookbook0 | ecookbook | - And there are the following project associations: - | Project A | Project B | + | Project A | Project B | | ecookbook0 | ecookbook_q3 | - And I am already logged in as "manager" @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_q3" And I should not see the project "ecookbook13" - And I should see the planning element "March" in the timeline - And I should not see the planning element "August" in the timeline - And I should not see the planning element "March13" in the timeline + And I should see the work package "March" in the timeline + And I should not see the work package "August" in the timeline + And I should not see the work package "March13" in the timeline @javascript 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" And I set the sortation of the first level grouping criteria to explicit order And I set the first level grouping criteria to: - | ecookbook | + | ecookbook | | ecookbook13 | 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 "ecookbook13" And I should see the project "ecookbook0" @@ -232,7 +226,7 @@ Feature: Timeline View Tests with reporters | ecookbook | 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 "ecookbook13" And I should see the project "ecookbook0" diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index 70dee9dcf4..99902899bb 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -49,7 +49,7 @@ module OpenProject # # 2.0.0debian-2 def self.special - 'pre17' + 'pre19' end def self.revision diff --git a/spec/controllers/api/v2/statuses_controller_spec.rb b/spec/controllers/api/v2/statuses_controller_spec.rb new file mode 100644 index 0000000000..d610ea7efb --- /dev/null +++ b/spec/controllers/api/v2/statuses_controller_spec.rb @@ -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 + diff --git a/spec/controllers/versions_controller_spec.rb b/spec/controllers/versions_controller_spec.rb index b57707f66d..b14c92fec4 100644 --- a/spec/controllers/versions_controller_spec.rb +++ b/spec/controllers/versions_controller_spec.rb @@ -52,7 +52,7 @@ describe VersionsController do it { response.should be_success } 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) } it "shows Version with no date set" do diff --git a/spec/controllers/work_packages/context_menus_controller_spec.rb b/spec/controllers/work_packages/context_menus_controller_spec.rb new file mode 100644 index 0000000000..5544b23129 --- /dev/null +++ b/spec/controllers/work_packages/context_menus_controller_spec.rb @@ -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}"\ + "&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}"\ + "&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}"\ + "&issue%5Bfixed_version_id%5D=#{version_1.id}" } + let(:version_link_2) { "/issues/bulk_update?#{ids_link}"\ + "&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}"\ + "&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&"\ + "#{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('&') } + + 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('&') } + + 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('&') } + 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 diff --git a/spec/factories/time_entry_factory.rb b/spec/factories/time_entry_factory.rb index 00de44fb6e..165015594a 100644 --- a/spec/factories/time_entry_factory.rb +++ b/spec/factories/time_entry_factory.rb @@ -30,7 +30,7 @@ FactoryGirl.define do factory :time_entry do project user - work_package :factory => :issue + work_package spent_on Date.today activity :factory => :time_entry_activity hours 1.0 diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index 5681312251..084a91e116 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -29,7 +29,7 @@ require 'spec_helper' describe Query do - describe 'available_columns' + describe 'available_columns' do let(:query) { FactoryGirl.build(:query) } 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 end end + + end + end diff --git a/spec/routing/work_package/auto_completes_routing_spec.rb b/spec/routing/work_package/auto_completes_routing_spec.rb new file mode 100644 index 0000000000..87bdbf47c3 --- /dev/null +++ b/spec/routing/work_package/auto_completes_routing_spec.rb @@ -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 diff --git a/spec/routing/work_package/calendars_routing_spec.rb b/spec/routing/work_package/calendars_routing_spec.rb new file mode 100644 index 0000000000..bf039ecbe1 --- /dev/null +++ b/spec/routing/work_package/calendars_routing_spec.rb @@ -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 diff --git a/spec/routing/work_package/context_menus_routing_spec.rb b/spec/routing/work_package/context_menus_routing_spec.rb new file mode 100644 index 0000000000..543e6ee729 --- /dev/null +++ b/spec/routing/work_package/context_menus_routing_spec.rb @@ -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 diff --git a/spec/routing/work_package/previews_routing_spec.rb b/spec/routing/work_package/previews_routing_spec.rb new file mode 100644 index 0000000000..29020bf6eb --- /dev/null +++ b/spec/routing/work_package/previews_routing_spec.rb @@ -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 diff --git a/spec/routing/work_packages_spec.rb b/spec/routing/work_packages_spec.rb index d5b27faf02..dadb7935a2 100644 --- a/spec/routing/work_packages_spec.rb +++ b/spec/routing/work_packages_spec.rb @@ -104,37 +104,4 @@ describe WorkPackagesController do :action => 'update', :id => '1' ) 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 diff --git a/test/exemplars/journal_exemplar.rb b/test/exemplars/journal_exemplar.rb index 1b5b71930d..ecfc2770ce 100644 --- a/test/exemplars/journal_exemplar.rb +++ b/test/exemplars/journal_exemplar.rb @@ -33,7 +33,7 @@ class Journal < ActiveRecord::Base def self.generate_issue project = Project.generate! - WorkPackage.generate_for_project!(project) + FactoryGirl.create(:work_package, project: project) end def self.generate_user diff --git a/test/functional/issues/context_menus_controller_test.rb b/test/functional/issues/context_menus_controller_test.rb deleted file mode 100644 index 9d71b6ab7c..0000000000 --- a/test/functional/issues/context_menus_controller_test.rb +++ /dev/null @@ -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&issue%5Bstatus_id%5D=5', - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', - :class => '' } - # Versions - assert_tag :tag => 'a', :content => '2.0', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&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&issue%5Bfixed_version_id%5D=4', - :class => '' } - - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&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&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('&') - 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}&issue%5Bstatus_id%5D=5", - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", - :class => '' } - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", - :class => '' } - assert_tag :tag => 'a', :content => 'Copy', - :attributes => { :href => "/work_packages/move/new?copy_options%5Bcopy%5D=t&#{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('&') - 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}&issue%5Bstatus_id%5D=5", - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", - :class => '' } - assert_tag :tag => 'a', :content => 'John Smith', - :attributes => { :href => "/issues/bulk_update?#{ids}&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 diff --git a/test/integration/routing_test.rb b/test/integration/routing_test.rb index cb3ad81394..bec4dbf8f4 100644 --- a/test/integration/routing_test.rb +++ b/test/integration/routing_test.rb @@ -147,11 +147,6 @@ class RoutingTest < ActionDispatch::IntegrationTest :format => 'xml') # 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', :action => 'index') diff --git a/test/unit/type_test.rb b/test/unit/type_test.rb index 03b8bb6f2c..5215986b9b 100644 --- a/test/unit/type_test.rb +++ b/test/unit/type_test.rb @@ -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 => 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_equal [2, 3, 5], Type.find(1).issue_statuses.collect(&:id) end