Merge pull request #445 from opf/feature/add_server_side_timeline_filters

Feature/add server side timeline filters
pull/462/head
Martin Czuchra 11 years ago
commit d00823f0f6
  1. 1
      Gemfile
  2. 4
      Gemfile.lock
  3. 2
      app/assets/javascripts/context_menu.js
  4. 169
      app/assets/javascripts/timelines.js
  5. 37
      app/controllers/api/v2/planning_elements_controller.rb
  6. 78
      app/controllers/api/v2/statuses_controller.rb
  7. 8
      app/models/query.rb
  8. 22
      app/models/type.rb
  9. 19
      app/views/api/v2/statuses/_status.api.rsb
  10. 34
      app/views/api/v2/statuses/index.api.rsb
  11. 30
      app/views/api/v2/statuses/show.api.rsb
  12. 4
      app/views/work_packages/index.html.erb
  13. 2
      config/routes.rb
  14. 7
      doc/CHANGELOG.md
  15. 209
      features/planning_elements/filter.feature
  16. 150
      features/step_definitions/api_steps.rb
  17. 45
      features/step_definitions/timelines_then_steps.rb
  18. 17
      features/step_definitions/timelines_when_steps.rb
  19. 7
      features/support/env.rb
  20. 4
      features/timelines/show.feature
  21. 1
      features/timelines/timeline_view.feature
  22. 90
      features/timelines/timeline_view_with_filters.feature
  23. 12
      features/timelines/timeline_view_with_reporters.feature
  24. 131
      spec/controllers/api/v2/statuses_controller_spec.rb
  25. 5
      spec/models/query_spec.rb
  26. 2
      test/unit/type_test.rb

@ -116,6 +116,7 @@ group :test do
gem 'ruby-prof'
gem 'simplecov', ">= 0.8.pre"
gem "shoulda-matchers"
gem "json_spec"
end
group :ldap do

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

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

@ -305,26 +305,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,7 +329,28 @@ Timeline = {
},
reload: function() {
delete this.lefthandTree;
var timelineLoader = new Timeline.TimelineLoader(
var timelineLoader = this.provideTimelineLoader()
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) {
jQuery.extend(this, data);
jQuery(this).trigger('dataReLoaded');
if (this.isGrouping() && this.options.grouping_two_enabled) {
this.secondLevelGroupingAdjustments();
}
this.adjustForPlanningElements();
this.rebuildAll();
}, this));
timelineLoader.load();
},
provideTimelineLoader: function() {
return new Timeline.TimelineLoader(
this,
{
api_prefix : this.options.api_prefix,
@ -360,6 +362,7 @@ Timeline = {
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,
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,
@ -368,23 +371,6 @@ Timeline = {
include_planning_elements : this.verticalPlanningElementIds()
}
);
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) {
jQuery.extend(this, data);
jQuery(this).trigger('dataReLoaded');
if (this.isGrouping() && this.options.grouping_two_enabled) {
this.secondLevelGroupingAdjustments();
}
this.adjustForPlanningElements();
this.rebuildAll();
}, this));
timelineLoader.load();
},
defer: function(action, delay) {
var timeline = this;
@ -432,6 +418,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) {
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 +1086,14 @@ Timeline = {
});
};
TimelineLoader.prototype.provideServerSideFilterHash = function() {
if (this.options.planning_element_types !== undefined) {
return {"type_id": this.options.planning_element_types};
} else {
return {};
}
}
TimelineLoader.prototype.registerPlanningElements = function (ids) {
this.inChunks(ids, function (projectIdsOfPacket, i) {
@ -1030,26 +1103,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 }
);
}
*/
});
};
@ -2086,7 +2164,6 @@ Timeline = {
},
filteredOut: function() {
var filtered = this.filteredOutForProjectFilter() ||
this.filteredOutForPlanningElementTypes() ||
this.filteredOutForResponsibles();
this.filteredOut = function() { return filtered; };
@ -2105,12 +2182,6 @@ Timeline = {
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;

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

@ -0,0 +1,78 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
# 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 ::Api::V2::ApiController
rescue_from ActiveRecord::RecordNotFound, with: lambda{render_404}
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

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

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

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

@ -0,0 +1,34 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
api.array :statuses, :size => @statuses.size do
@statuses.each do |status|
render(:partial => '/api/v2/statuses/status.api',
:object => status)
end
end

@ -0,0 +1,30 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
render(:partial => '/api/v2/reported_project_statuses/status.api',
:object => @status)

@ -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 %>
</p>
<% end %>

@ -76,6 +76,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 +87,7 @@ OpenProject::Application.routes.draw do
resources :project_associations do
get :available_projects, :on => :collection
end
resources :statuses, :only => [:index, :show]
end
end

@ -37,6 +37,13 @@ See doc/COPYRIGHT.rdoc for more details.
* `#1990` Migrate issue relation
* `#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

@ -0,0 +1,209 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
Feature: Filtering work packages via the api
Background:
Given there is 1 project with the following:
| identifier | sample_project |
| name | sample_project |
And I am working in project "sample_project"
And the project "sample_project" has the following types:
| name | position |
| Bug | 1 |
| Task | 2 |
| Story | 3 |
| Epic | 4 |
And there is a default issuepriority with:
| name | Normal |
And there is a issuepriority with:
| name | High |
And there is a issuepriority with:
| name | Immediate |
And there are the following issue status:
| name | is_closed | is_default |
| New | false | true |
| In Progress | false | true |
| Closed | false | true |
And the project uses the following modules:
| timelines |
And there is a role "member"
And the role "member" may have the following rights:
| edit_work_packages |
| view_projects |
| view_reportings |
| view_timelines |
| view_work_packages |
And there is 1 user with the following:
| login | bob |
And there is 1 user with the following:
| login | peter |
And there is 1 user with the following:
| login | pamela |
And the user "bob" is a "member" in the project "sample_project"
And the user "peter" is a "member" in the project "sample_project"
And the user "pamela" is a "member" in the project "sample_project"
And I am already logged in as "bob"
Scenario: Call the endpoint of the api without filters
Given there are the following work packages in project "sample_project":
| subject | type |
| work_package#1 | Bug |
| work_package#2 | Story |
When I call the work_package-api on project "sample_project" requesting format "json" without any filters
Then the json-response should include 2 work packages
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"
Scenario: Call the api filtering for type
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Bug | |
| work_package#1.1 | Bug | work_package#1 |
| work_package#2 | Story | |
| work_package#2.1 | Story | work_package#2 |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Bug"
Then the json-response should include 2 work packages
Then the json-response should not contain a work_package "work_package#2"
And the json-response should contain a work_package "work_package#1"
Scenario: Call the api filtering for status
Given there are the following work packages in project "sample_project":
| subject | type | status |
| work_package#1 | Bug | New |
| work_package#2 | Story | In Progress |
| work_package#3 | Epic | Closed |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for status "In Progress"
Then the json-response should include 1 work package
Then the json-response should contain a work_package "work_package#2"
And the json-response should not contain a work_package "work_package#1"
Scenario: Filtering multiple types
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Bug | |
| work_package#1.1 | Bug | work_package#1 |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Bug,Epic"
Then the json-response should include 3 work packages
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#3"
And the json-response should not contain a work_package "work_package#3.1"
Scenario: Filter out children of work packages, if they don't have the right type
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#3 | Epic | |
| work_package#3.1 | Story | work_package#3 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic"
Then the json-response should include 1 work package
And the json-response should contain a work_package "work_package#3"
And the json-response should not contain a work_package "work_package#3.1"
Scenario: Filter out parents of work packages, if they don't have the right type
Given there are the following work packages in project "sample_project":
| subject | type |
| work_package#1 | Bug |
| work_package#2 | Story |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Story"
Then the json-response should include 1 work package
And the json-response should not contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"
Scenario: correctly export parent-child-relations
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#2 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" without any filters
Then the json-response should include 3 work packages
And the json-response should say that "work_package#1" is parent of "work_package#1.1"
Scenario: Move parent-relations up the ancestor-chain, when intermediate packages are fitered
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#1.1.1 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Task"
Then the json-response should include 2 work packages
And the json-response should not contain a work_package "work_package#1.1"
And the json-response should contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#1.1.1"
And the json-response should say that "work_package#1" is parent of "work_package#1.1.1"
Scenario: The parent should be rewired to the first ancestor present in the filtered set
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Task | work_package#1 |
| work_package#1.1.1 | Bug | work_package#1.1 |
| work_package#1.1.1.1 | Task | work_package#1.1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Task"
Then the json-response should include 3 work packages
And the json-response should say that "work_package#1.1" is parent of "work_package#1.1.1.1"
Scenario: When all ancestors are filtered, the work_package should have no parent
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Story | work_package#1 |
| work_package#1.1.1 | Task | work_package#1.1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Task"
Then the json-response should include 1 work packages
And the json-response should say that "work_package#1.1.1" has no parent
Scenario: Children are filtered out
Given there are the following work packages in project "sample_project":
| subject | type | parent |
| work_package#1 | Epic | |
| work_package#1.1 | Task | work_package#1 |
| work_package#1.2 | Story | work_package#1 |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for type "Epic,Story"
And the json-response should say that "work_package#1" has 1 child
Scenario: Filtering for responsibles
Given there are the following work packages in project "sample_project":
| subject | type | responsible |
| work_package#1 | Task | bob |
| work_package#2 | Task | peter |
| work_package#3 | Task | pamela |
When I call the work_package-api on project "sample_project" requesting format "json" filtering for responsible "peter"
Then the json-response should include 1 work package
And the json-response should not contain a work_package "work_package#1"
And the json-response should contain a work_package "work_package#2"

@ -0,0 +1,150 @@
#encoding: utf-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require "benchmark"
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" without any filters$/) do |project_name, format|
@project = Project.find(project_name)
@unfiltered_benchmark = Benchmark.measure("Unfiltered Results") do
visit api_v2_project_planning_elements_path(project_id: project_name, format: format)
end
end
Then(/^the json\-response should include (\d+) work package(s?)$/) do |number_of_wps, plural|
expect(work_package_names.size).to eql number_of_wps.to_i
end
Then(/^the json\-response should( not)? contain a work_package "(.*?)"$/) do |negation, work_package_name|
if negation
expect(work_package_names).not_to include work_package_name
else
expect(work_package_names).to include work_package_name
end
end
And(/^the json\-response should say that "(.*?)" is parent of "(.*?)"$/) do |parent_name, child_name|
child = decoded_json["planning_elements"].select {|wp| wp["name"] == child_name}.first
expect(child["parent"]["name"]).to eql parent_name
end
And(/^the json\-response should say that "(.*?)" has no parent$/) do |child_name|
child = decoded_json["planning_elements"].select {|wp| wp["name"] == child_name}.first
expect(child["parent"]).to be_nil
end
And(/^the json\-response should say that "(.*?)" has (\d+) child(ren)?$/) do |parent_name, nr_of_children,plural|
parent = decoded_json["planning_elements"].select {|wp| wp["name"] == parent_name}.first
expect(parent["children"].size).to eql nr_of_children.to_i
end
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for status "(.*?)"$/) do |project_name, format, status_names|
statuses = IssueStatus.where(name: status_names.split(','))
get_filtered_json(project_name: project_name,
format: format,
filters: [:status_id],
operators: {status_id: "="},
values: {status_id: statuses.map(&:id)} )
end
Then(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for type "(.*?)"$/) do |project_name, format, type_names|
types = Project.find_by_identifier(project_name).types.where(name: type_names.split(","))
get_filtered_json(project_name: project_name,
format: format,
filters: [:type_id],
operators: {type_id: "="},
values: {type_id: types.map(&:id)} )
end
When(/^I call the work_package\-api on project "(.*?)" requesting format "(.*?)" filtering for responsible "(.*?)"$/) do |project_name, format, responsible_names|
responsibles = User.where(login: responsible_names.split(','))
get_filtered_json(project_name: project_name,
format: format,
filters: [:responsible_id],
operators: {responsible_id: "="},
values: {responsible_id: responsibles.map(&:id)} )
end
And(/^there are (\d+) work packages of type "(.*?)" in project "(.*?)"$/) do |nr_of_wps, type_name, project_name|
project = Project.find_by_identifier(project_name)
type = project.types.find_by_name(type_name)
FactoryGirl.create_list(:work_package, nr_of_wps.to_i, project: project, type: type)
end
And(/^the time to get the unfiltered results should not exceed (\d+)\.(\d+)s$/) do |seconds,milliseconds|
puts "----Unfiltered Benchmark----"
puts @unfiltered_benchmark
@unfiltered_benchmark.total.should < "#{seconds}.#{milliseconds}".to_f
end
And(/^the time to get the filtered results should not exceed (\d+)\.(\d+)s$/) do |seconds, milliseconds|
puts "----Filtered Benchmark----"
puts @filtered_benchmark
@filtered_benchmark.total.should < "#{seconds}.#{milliseconds}".to_f
end
Then(/^the time to get the filtered results should be faster than the time to get the unfiltered results$/) do
@filtered_benchmark.total.should < @unfiltered_benchmark.total
end
def work_package_names
decoded_json["planning_elements"].map{|wp| wp["name"]}
end
def decoded_json
@decoded_json ||= ActiveSupport::JSON.decode last_json
end
def last_json
page.source
end
def get_filtered_json(params)
@filtered_benchmark = Benchmark.measure("Filtered Results") do
visit api_v2_project_planning_elements_path(project_id: params[:project_name],
format: params[:format],
f: params[:filters],
op: params[:operators],
v: params[:values])
end
end

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

@ -74,6 +74,20 @@ 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 type "(.*?)"$/) do |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}"
}
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
@ -103,7 +117,7 @@ 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|
When (/^I set the columns shown in the timeline to:$/) do |table|
timeline_name = @timeline_name
project_name = @project.name
steps %Q{
@ -125,6 +139,7 @@ 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

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

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

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

@ -0,0 +1,90 @@
#-- 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/o 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 | |
| Unrelated | 3000-01-01 | 3000-01-05 | Phase | |
| Mission to the moon | 3000-01-02 | 3000-01-02 | Milestone | Unrelated |
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
And I should see the work package "Mission to the moon" in the timeline
And I should not see the work package "Unrelated" in the timeline

@ -96,7 +96,6 @@ Feature: Timeline View Tests with reporters
| 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"
And the user "manager" is a "manager"
@ -132,8 +131,6 @@ Feature: Timeline View Tests with reporters
| 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"
And the user "manager" is a "manager"
@ -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 |
| 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

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

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

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

Loading…
Cancel
Save