Merge remote-tracking branch 'origin/dev' into feature/detailed_filters_on_dates_3865

pull/852/head
Hagen Schink 11 years ago
commit 517f24c19a
  1. 2
      .travis.yml
  2. 3
      app/assets/javascripts/application.js.erb
  3. 202
      app/assets/javascripts/jquery.trap.js
  4. 4
      app/assets/javascripts/modal.js
  5. 16
      app/controllers/activities_controller.rb
  6. 3
      app/controllers/work_packages/bulk_controller.rb
  7. 7
      app/controllers/work_packages/context_menus_controller.rb
  8. 4
      app/helpers/timelines_helper.rb
  9. 6
      app/helpers/work_packages_helper.rb
  10. 2
      app/models/activity/work_package_activity_provider.rb
  11. 2
      app/models/mail_handler.rb
  12. 27
      app/models/project.rb
  13. 4
      app/models/queries/work_packages/available_filter_options.rb
  14. 2
      app/models/query.rb
  15. 30
      app/models/work_package.rb
  16. 1
      app/views/activities/index.html.erb
  17. 2
      app/views/categories/_form.html.erb
  18. 2
      app/views/projects/form/attributes/_responsible_id.html.erb
  19. 1
      app/views/user_mailer/_issue_details.html.erb
  20. 1
      app/views/user_mailer/_issue_details.text.erb
  21. 2
      app/views/work_packages/_relations.html.erb
  22. 6
      app/views/work_packages/bulk/edit.html.erb
  23. 14
      app/views/work_packages/context_menus/index.html.erb
  24. 4
      app/views/work_packages/moves/new.html.erb
  25. 2
      config/locales/de.yml
  26. 2
      config/locales/en.yml
  27. 9
      doc/CHANGELOG.md
  28. 6
      features/step_definitions/work_package_steps.rb
  29. 105
      features/work_packages/bulk.feature
  30. 22
      features/work_packages/update.feature
  31. 2
      lib/redmine.rb
  32. 2
      lib/redmine/scm/adapters/abstract_adapter.rb
  33. 67
      spec/controllers/activities_controller_spec.rb
  34. 46
      spec/controllers/work_packages/bulk_controller_spec.rb
  35. 25
      spec/controllers/work_packages/context_menus_controller_spec.rb
  36. 66
      spec/controllers/work_packages_controller_spec.rb
  37. 33
      spec/models/work_package_spec.rb
  38. 19
      test/unit/journal_observer_test.rb

@ -28,7 +28,7 @@
language: ruby
rvm:
- 2.0
- 2.0.0
branches:
only:
- dev

@ -46,6 +46,7 @@
//= require jquery_ujs
//= require jquery_noconflict
//= require jquery.colorcontrast
//= require jquery.trap
//= require prototype
//= require effects
//= require dragdrop
@ -117,7 +118,7 @@ jQuery(document).ready(function ($) {
dateFormat: 'yy-mm-dd',
showButtonPanel: true,
calculateWeek: function (d) {
if (d.getDay() > 1) {
if (d.getDay() != 1) {
d.setDate(d.getDate() - d.getDay() + 1);
}
return $.datepicker.iso8601Week(d);

@ -0,0 +1,202 @@
/*!
Copyright (c) 2011, 2012 Julien Wajsberg <felash@gmail.com>
All rights reserved.
Official repository: https://github.com/julienw/jquery-trap-input
License is there: https://github.com/julienw/jquery-trap-input/blob/master/LICENSE
This is version 1.2.0.
*/
(function( $, undefined ){
/*
(this comment is after the first line of code so that uglifyjs removes it)
Redistribution and use in source and binary forms, with or without
modification, are permitted without condition.
Although that's not an obligation, I would appreciate that you provide a
link to the official repository.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED.
*/
/*jshint boss: true, bitwise: true, curly: true, expr: true, newcap: true, noarg: true, nonew: true, latedef: true, regexdash: true */
var DATA_ISTRAPPING_KEY = "trap.isTrapping";
function onkeypress(e) {
if (e.keyCode === 9) {
var goReverse = !!(e.shiftKey);
if (processTab(this, e.target, goReverse)) {
e.preventDefault();
e.stopPropagation();
}
}
}
// will return true if we could process the tab event
// otherwise, return false
function processTab(container, elt, goReverse) {
var $focussable = getFocusableElementsInContainer(container),
curElt = elt,
index, nextIndex, prevIndex, lastIndex;
do {
index = $focussable.index(curElt);
nextIndex = index + 1;
prevIndex = index - 1;
lastIndex = $focussable.length - 1;
switch(index) {
case -1:
return false; // that's strange, let the browser do its job
case 0:
prevIndex = lastIndex;
break;
case lastIndex:
nextIndex = 0;
break;
}
if (goReverse) {
nextIndex = prevIndex;
}
curElt = $focussable.get(nextIndex);
// IE sometimes throws when an element is not visible
try {
curElt.focus();
} catch(e) {
}
} while (elt === elt.ownerDocument.activeElement);
return true;
}
function filterKeepSpeciallyFocusable() {
return this.tabIndex > 0;
}
function filterKeepNormalElements() {
return !this.tabIndex; // true if no tabIndex or tabIndex == 0
}
function sortFocusable(a, b) {
return (a.t - b.t) || (a.i - b.i);
}
function getFocusableElementsInContainer(container) {
var $container = $(container);
var result = [],
cnt = 0;
fixIndexSelector.enable && fixIndexSelector.enable();
// leaving away command and details for now
$container.find("a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex=0]")
.filter(":visible")
.filter(filterKeepNormalElements)
.each(function(i, val) {
result.push({
v: val, // value
t: 0, // tabIndex
i: cnt++ // index for stable sort
});
});
$container
.find("[tabindex]")
.filter(":visible")
.filter(filterKeepSpeciallyFocusable)
.each(function(i, val) {
result.push({
v: val, // value
t: val.tabIndex, // tabIndex
i: cnt++ // index
});
});
fixIndexSelector.disable && fixIndexSelector.disable();
result = $.map(result.sort(sortFocusable), // needs stable sort
function(val) {
return val.v;
}
);
return $(result);
}
function trap() {
this.keydown(onkeypress);
this.data(DATA_ISTRAPPING_KEY, true);
return this;
}
function untrap() {
this.unbind('keydown', onkeypress);
this.removeData(DATA_ISTRAPPING_KEY);
return this;
}
function isTrapping() {
return !!this.data(DATA_ISTRAPPING_KEY);
}
$.fn.extend({
trap: trap,
untrap: untrap,
isTrapping: isTrapping
});
// jQuery 1.6.x tabindex attr hooks management
// this triggers problems for tabindex attribute
// selectors in IE7-
// see https://github.com/julienw/jquery-trap-input/issues/3
var fixIndexSelector = {};
if ($.find.find && $.find.attr !== $.attr) {
// jQuery uses Sizzle (this is jQuery >= 1.3)
// sizzle uses its own attribute handling (in jq 1.6.x and below)
(function() {
var tabindexKey = "tabindex";
var sizzleAttrHandle = $.expr.attrHandle;
// this function comes directly from jQuery 1.7.2 (propHooks.tabIndex.get)
// we have to put it here if we want to support jQuery < 1.6 which
// doesn't have an attrHooks object to reference.
function getTabindexAttr(elem) {
// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
var attributeNode = elem.getAttributeNode(tabindexKey);
return attributeNode && attributeNode.specified ?
parseInt( attributeNode.value, 10 ) :
undefined;
}
function fixSizzleAttrHook() {
// in jQ <= 1.6.x, we add to Sizzle the attrHook from jQuery's attr method
sizzleAttrHandle[tabindexKey] = sizzleAttrHandle.tabIndex = getTabindexAttr;
}
function unfixSizzleAttrHook() {
delete sizzleAttrHandle[tabindexKey];
delete sizzleAttrHandle.tabIndex;
}
fixIndexSelector = {
enable: fixSizzleAttrHook,
disable: unfixSizzleAttrHook
};
})();
}
})( jQuery );

@ -101,6 +101,10 @@ var ModalHelper = (function() {
this.hideLoadingModal();
this.loadingModal = false;
// use jquery.trap.js to keep the keyboard focus within the modal
// while it's open
body.trap();
body.on("keyup", function (e) {
if (e.which == 27) {
modalHelper.close();

@ -47,8 +47,8 @@ class ActivitiesController < ApplicationController
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
set_activity_scope
events = @activity.events(@date_from, @date_to)
censor_events_from_projects_with_disabled_activity!(events) unless @project
@ -100,4 +100,16 @@ class ActivitiesController < ApplicationController
event.project_id.nil? || allowed_project_ids.include?(event.project_id)
end
end
def set_activity_scope
if params[:apply]
@activity.scope_select {|t| !params["show_#{t}"].nil?}
elsif session[:activity]
@activity.scope = session[:activity]
else
@activity.scope = (@author.nil? ? :default : :all)
end
session[:activity] = @activity.scope
end
end

@ -43,7 +43,8 @@ class WorkPackages::BulkController < ApplicationController
@work_packages.sort!
@available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
@custom_fields = @projects.map{|p|p.all_work_package_custom_fields}.inject{|memo,c|memo & c}
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@assignables = @projects.map(&:possible_assignees).inject{|memo,a| memo & a}
@responsibles = @projects.map(&:possible_responsibles).inject{|memo,a| memo & a}
@types = @projects.map(&:types).inject{|memo,t| memo & t}
end

@ -56,12 +56,15 @@ class WorkPackages::ContextMenusController < ApplicationController
:delete => User.current.allowed_to?(:delete_work_packages, @projects)
}
if @project
@assignables = @project.assignable_users
@assignables = @project.possible_assignees
@assignables << @work_package.assigned_to if @work_package && @work_package.assigned_to && !@assignables.include?(@work_package.assigned_to)
@responsibles = @project.possible_responsibles
@responsibles << @work_package.responsible if @work_package && @work_package.responsible && !@responsibles.include?(@work_package.responsible)
@types = @project.types
else
#when multiple projects, we only keep the intersection of each set
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@assignables = @projects.map(&:possible_assignees).inject{|memo,a| memo & a}
@responsibles = @projects.map(&:possible_responsibles).inject{|memo,a| memo & a}
@types = @projects.map(&:types).inject{|memo,t| memo & t}
end

@ -85,10 +85,6 @@ module TimelinesHelper
ProjectType.all.map { |t| [t.name, t.id] }
end
def options_for_responsible(project)
project.users.map { |u| [u.name, u.id] }
end
def visible_parent_project(project)
parent = project.parent

@ -509,12 +509,12 @@ module WorkPackagesHelper
def work_package_form_assignee_attribute(form, work_package, locals = {})
WorkPackageAttribute.new(:assignee,
form.select(:assigned_to_id, (work_package.assignable_users.map {|m| [m.name, m.id]}), :include_blank => true))
form.select(:assigned_to_id, (work_package.assignable_assignees.map {|m| [m.name, m.id]}), :include_blank => true))
end
def work_package_form_responsible_attribute(form, work_package, locals = {})
WorkPackageAttribute.new(:assignee,
form.select(:responsible_id, options_for_responsible(locals[:project]), :include_blank => true))
WorkPackageAttribute.new(:responsible,
form.select(:responsible_id, work_package.assignable_responsibles.map {|m| [m.name, m.id]}, :include_blank => true))
end
def work_package_form_category_attribute(form, work_package, locals = {})

@ -47,7 +47,7 @@ class Activity::WorkPackageActivityProvider < Activity::BaseActivityProvider
end
def self.work_package_title(id, subject, type_name, status_name, is_standard)
title = "#{(is_standard) ? l(:default_type) : "#{type_name}"} ##{id}: #{subject}"
title = "#{(is_standard) ? "" : "#{type_name}"} ##{id}: #{subject}"
title << " (#{status_name})" unless status_name.blank?
end

@ -412,7 +412,7 @@ class MailHandler < ActionMailer::Base
def find_assignee_from_keyword(keyword, issue)
keyword = keyword.to_s.downcase
assignable = issue.assignable_users
assignable = issue.assignable_assignees
assignee = nil
assignee ||= assignable.detect {|a|
a.mail.to_s.downcase == keyword ||

@ -46,10 +46,14 @@ class Project < ActiveRecord::Base
# Specific overidden Activities
has_many :time_entry_activities
has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUSES[:active]}"
has_many :assignable_members,
has_many :possible_assignee_members,
:class_name => 'Member',
:include => [:principal, :roles],
:conditions => Proc.new { self.class.assignable_members_condition }
:conditions => Proc.new { self.class.possible_assignees_condition }
has_many :possible_responsible_members,
:class_name => 'Member',
:include => [:principal, :roles],
:conditions => Proc.new { self.class.possible_responsibles_condition }
has_many :memberships, :class_name => 'Member'
has_many :member_principals, :class_name => 'Member',
:include => :principal,
@ -597,8 +601,13 @@ class Project < ActiveRecord::Base
end
# Users/groups a work_package can be assigned to
def assignable_users
assignable_members.map(&:principal).compact.sort
def possible_assignees
possible_assignee_members.map(&:principal).compact.sort
end
# Users who can become responsible for a work_package
def possible_responsibles
possible_responsible_members.map(&:principal).compact.sort
end
# Returns the mail adresses of users that should be always notified on project events
@ -916,7 +925,7 @@ class Project < ActiveRecord::Base
protected
def self.assignable_members_condition
def self.possible_assignees_condition
condition = Setting.work_package_group_assignment? ?
["(#{Principal.table_name}.type=? OR #{Principal.table_name}.type=?)", 'User', 'Group'] :
@ -928,4 +937,12 @@ class Project < ActiveRecord::Base
sanitize_sql_array condition
end
def self.possible_responsibles_condition
condition = ["(#{Principal.table_name}.type=? AND #{User.table_name}.status=? AND roles.assignable = ?)",
'User', User::STATUSES[:active], true]
sanitize_sql_array condition
end
end

@ -112,7 +112,9 @@ module Queries::WorkPackages::AvailableFilterOptions
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
@available_work_package_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_work_package_filters["responsible_id"] = { type: :list_optional, order: 4, values: assigned_to_values } unless assigned_to_values.empty?
responsible_values = user_values.dup
responsible_values = [["<< #{l(:label_me)} >>", "me"]] + responsible_values if User.current.logged?
@available_work_package_filters["responsible_id"] = { type: :list_optional, order: 4, values: responsible_values } unless responsible_values.empty?
# watcher filters
if User.current.logged?

@ -62,7 +62,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: ["responsible.lastname", "responsible.firstname", "responsible.id"], groupable: "#{WorkPackage.table_name}.responsible_id", :join => "LEFT OUTER JOIN users as responsible ON (#{WorkPackage.table_name}.responsible_id = responsible.id)"),
QueryColumn.new(:responsible, sortable: ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], groupable: "#{WorkPackage.table_name}.responsible_id", :join => "LEFT OUTER JOIN users as responsible ON (#{WorkPackage.table_name}.responsible_id = responsible.id)"),
QueryColumn.new(:updated_at, :sortable => "#{WorkPackage.table_name}.updated_at", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{Category.table_name}.name", :groupable => true),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),

@ -375,8 +375,15 @@ class WorkPackage < ActiveRecord::Base
end
end
# Users/groups the work_package can be assigned to
def assignable_assignees
project.possible_assignees
end
# Users the work_package can be assigned to
delegate :assignable_users, :to => :project
def assignable_responsibles
project.possible_responsibles
end
# Versions that the work_package can be assigned to
# A work_package can be assigned to:
@ -391,7 +398,7 @@ class WorkPackage < ActiveRecord::Base
end
def to_s
"#{(kind.is_standard) ? l(:default_type) : "#{kind.name}"} ##{id}: #{subject}"
"#{(kind.is_standard) ? "" : "#{kind.name}"} ##{id}: #{subject}"
end
# Return true if the work_package is closed, otherwise false
@ -921,7 +928,7 @@ class WorkPackage < ActiveRecord::Base
end
def add_time_entry_for(user, attributes)
return if attributes.nil? || attributes.values.all?(&:blank?)
return if time_entry_blank?(attributes)
attributes.reverse_merge!({ :user => user,
:spent_on => Date.today })
@ -929,6 +936,23 @@ class WorkPackage < ActiveRecord::Base
time_entries.build(attributes)
end
##
# Checks if the time entry defined by the given attributes is blank.
# A time entry counts as blank despite a selected activity if that activity
# is simply the default activity and all other attributes are blank.
def time_entry_blank?(attributes)
return true if attributes.nil?
key = "activity_id"
id = attributes[key]
default_id = if id && !id.blank?
Enumeration.exists? :id => id, :is_default => true, :type => 'TimeEntryActivity'
else
true
end
default_id && attributes.except(key).values.all?(&:blank?)
end
# >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# this removes all attachments separately before destroying the issue
# avoids getting a ActiveRecord::StaleObjectError when deleting an issue

@ -93,6 +93,7 @@ See doc/COPYRIGHT.rdoc for more details.
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
<% end %>
<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
<%= hidden_field_tag('apply', true) %>
<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
<% end %>
<% end %>

@ -32,6 +32,6 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="box">
<p><%= f.text_field :name, :size => 30, :required => true %></p>
<p>
<%= f.select :assigned_to_id, @project.assignable_users.sort.collect{|u| [u.name, u.id]}, :include_blank => true %>
<%= f.select :assigned_to_id, @project.possible_assignees.sort.collect{|u| [u.name, u.id]}, :include_blank => true %>
</p>
</div>

@ -30,7 +30,7 @@ See doc/COPYRIGHT.rdoc for more details.
<p>
<% if project && project.persisted? %>
<% if User.current.impaired? %>
<%= form.select :responsible_id, options_for_responsible(project), :include_blank => true %>
<%= form.select :responsible_id, project.possible_responsibles.map {|m| [m.name, m.id]}, :include_blank => true %>
<% else %>
<% options = { :'data-ajaxURL' => url_for({:controller => "/members",
:action => "paginate_users" }),

@ -34,6 +34,7 @@ See doc/COPYRIGHT.rdoc for more details.
<li><%= WorkPackage.human_attribute_name(:status) %>: <%= issue.status %></li>
<li><%= WorkPackage.human_attribute_name(:priority) %>: <%= issue.priority %></li>
<li><%= WorkPackage.human_attribute_name(:assigned_to) %>: <%= issue.assigned_to %></li>
<li><%= WorkPackage.human_attribute_name(:responsible) %>: <%= issue.responsible %></li>
<li><%= WorkPackage.human_attribute_name(:category) %>: <%= issue.category %></li>
<li><%= WorkPackage.human_attribute_name(:fixed_version) %>: <%= issue.fixed_version %></li>

@ -34,6 +34,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= WorkPackage.human_attribute_name(:status) %>: <%= issue.status %>
<%= WorkPackage.human_attribute_name(:priority) %>: <%= issue.priority %>
<%= WorkPackage.human_attribute_name(:assigned_to) %>: <%= issue.assigned_to %>
<%= WorkPackage.human_attribute_name(:responsible) %>: <%= issue.responsible %>
<%= WorkPackage.human_attribute_name(:category) %>: <%= issue.category %>
<%= WorkPackage.human_attribute_name(:fixed_version) %>: <%= issue.fixed_version %>

@ -41,7 +41,7 @@ See doc/COPYRIGHT.rdoc for more details.
<th><%= WorkPackage.human_attribute_name(:type)%></th>
<th><%= WorkPackage.human_attribute_name(:start_date)%></th>
<th><%= WorkPackage.human_attribute_name(:due_date)%></th>
<th><spen class="hidden-for-sighted"><%= t('button_delete') %></span></th>
<th><span class="hidden-for-sighted"><%= t('button_delete') %></span></th>
</tr>
</thead>
<tbody>

@ -59,6 +59,12 @@ See doc/COPYRIGHT.rdoc for more details.
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@assignables, :id, :name)) %>
</p>
<p>
<label for='work_package_responsible_id'><%= WorkPackage.human_attribute_name(:responsible) %></label>
<%= select_tag('work_package[responsible_id]', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@responsibles, :id, :name)) %>
</p>
<% if @project %>
<p>
<label for='category_id'><%= WorkPackage.human_attribute_name(:category) %></label>

@ -81,7 +81,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %>
<% if @assignables.present? -%>
<% assignables = @assignables.dup << [nil, l(:label_none)] %>
<% assignables = @assignables.dup << ["none", l(:label_none)] %>
<% params = default_params.merge(:collection => assignables,
:attribute => 'assigned_to',
:selected => lambda { |user| @work_package && user == @work_package.assigned_to },
@ -89,8 +89,18 @@ See doc/COPYRIGHT.rdoc for more details.
<%= context_menu_entry(params) %>
<% end %>
<% if @responsibles.present? -%>
<% responsibles = @responsibles.dup << ["none", l(:label_none)] %>
<% params = default_params.merge(:collection => responsibles,
:attribute => 'responsible',
:selected => lambda { |user| @work_package && user == @work_package.responsible },
:disabled => lambda { |user| !@can[:update] }) %>
<%= context_menu_entry(params) %>
<% end %>
<% unless @project.nil? || (categories = @project.categories.to_a).empty? -%>
<% categories << [nil, l(:label_none)] %>
<% categories << ["none", l(:label_none)] %>
<% params = default_params.merge(:collection => categories,
:attribute => 'category',
:selected => lambda { |category| @work_package && category == @work_package.category },

@ -78,14 +78,14 @@ See doc/COPYRIGHT.rdoc for more details.
<%= select_tag('assigned_to_id',
content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@target_project.assignable_users, :id, :name)) %>
options_from_collection_for_select(@target_project.possible_assignees, :id, :name)) %>
</p>
<p>
<label for='responsible_id'><%= WorkPackage.human_attribute_name(:responsible) %></label>
<%= select_tag('responsible_id',
content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@target_project.assignable_users, :id, :name)) %>
options_from_collection_for_select(@target_project.possible_responsibles, :id, :name)) %>
</p>
</div>

@ -249,7 +249,7 @@ de:
group: "Gruppe"
issue: "Ticket"
category: "Kategorie"
status: "Ticket-Status"
status: "Arbeitspaket-Status"
member: "Mitglied"
news: "News"
project: "Projekt"

@ -247,7 +247,7 @@ en:
group: "Group"
issue: "Issue"
category: "Category"
status: "Issue status"
status: "Work package status"
member: "Member"
news: "News"
project: "Project"

@ -31,10 +31,19 @@ See doc/COPYRIGHT.rdoc for more details.
* `#2018` Cleanup journal tables
* `#2244` Fix: [Accessibility] correctly label document language - custom fields
* `#2594` Fix: [Activity] Too many filter selects than necessary
* `#3215` Datepicker - Timelines calendar weeks out of sync
* `#3332` [CodeClimate] Mass Assignment AuthSourcesController
* `#3333` [CodeClimate] Mass Assignment RolesController
* `#3438` Activity default value makes log time required
* `#3481` Fix: [Activity] Not possible to unselect all filters
* `#3730` Setting responsible via bulk edit
* `#3731` Setting responsible via context menu
* `#3844` Fixed Work Package status translation
* `#3865` Detailed filters on dates
* `#3854` Move function and Query filters allows to select groups as responsible
* `#3974` [Timelines] Typo at creating timelines
* `#4023` [Accessibility] Keep keyboard focus within modal while it's open
## 3.0.0pre43

@ -127,3 +127,9 @@ Then /^the work package should be shown with the following values:$/ do |table|
should have_css(".description", :text => table.rows_hash["Description"])
end
end
Then(/^the attribute "(.*?)" of work package "(.*?)" should be "(.*?)"$/) do |attribute, wp_name, value|
wp = WorkPackage.find_by_subject(wp_name)
wp ||= WorkPackages.where("subject like ?", wp_name).to_sql
wp.send(attribute).to_s.should == value
end

@ -0,0 +1,105 @@
#-- 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: Updating work packages
Background:
Given there is 1 user with:
| login | manager |
| firstname | the |
| lastname | manager |
And there are the following types:
| Name | Is milestone |
| Phase1 | false |
| Phase2 | false |
And there are the following project types:
| Name |
| Standard Project |
And there is 1 project with the following:
| identifier | ecookbook |
| name | ecookbook |
And the project named "ecookbook" is of the type "Standard Project"
And the following types are enabled for projects of type "Standard Project"
| Phase1 |
| Phase2 |
And there is a role "manager"
And the role "manager" may have the following rights:
| edit_work_packages |
| view_work_packages |
| manage_subtasks |
And I am working in project "ecookbook"
And the user "manager" is a "manager"
And there are the following priorities:
| name | default |
| prio1 | true |
| prio2 | |
And there are the following status:
| name | default |
| status1 | true |
| status2 | |
And the project "ecookbook" has 1 version with the following:
| name | version1 |
And the type "Phase1" has the default workflow for the role "manager"
And the type "Phase2" has the default workflow for the role "manager"
And there are the following work packages in project "ecookbook":
| subject | type | status | fixed_version | assigned_to |
| pe1 | Phase1 | status1 | version1 | manager |
| pe2 | | | | manager |
And I am already logged in as "manager"
@javascript
Scenario: Bulk updating the fixed version of several work packages
When I go to the work package index page of the project called "ecookbook"
And I open the context menu on the work packages:
| pe1 |
| pe2 |
And I hover over ".fixed_version .context_item"
And I follow "none" within "#context-menu"
Then I should see "Successful update"
And I follow "pe1"
And I should see "deleted (version1)"
@javascript
Scenario: Bulk updating several work packages without back url should return index
When I go to the work package index page of the project called "ecookbook"
And I open the context menu on the work packages:
| pe1 |
| pe2 |
And I follow "Edit" within "#context-menu"
And I press "Submit"
Then I should see "Work packages" within "#content"
@javascript
Scenario: Bulk updating the fixed version of several work packages
When I go to the work package index page of the project called "ecookbook"
And I open the context menu on the work packages:
| pe1 |
| pe2 |
And I hover over ".assigned_to .context_item"
And I follow "none" within "#context-menu"
Then I should see "Successful update"
Then the attribute "assigned_to" of work package "pe1" should be ""

@ -127,25 +127,3 @@ Feature: Updating work packages
Then I should be on the page of the work package "pe1"
And I should see a journal with the following:
| Notes | Note message |
@javascript
Scenario: Bulk updating the fixed version of several work packages
When I go to the work package index page of the project called "ecookbook"
And I open the context menu on the work packages:
| pe1 |
| pe2 |
And I hover over ".fixed_version .context_item"
And I follow "none" within "#context-menu"
Then I should see "Successful update"
And I follow "pe1"
And I should see "deleted (version1)"
@javascript
Scenario: Bulk updating several work packages without back url should return index
When I go to the work package index page of the project called "ecookbook"
And I open the context menu on the work packages:
| pe1 |
| pe2 |
And I follow "Edit" within "#context-menu"
And I press "Submit"
Then I should see "Work packages" within "#content"

@ -364,7 +364,7 @@ end
Redmine::Activity.map do |activity|
activity.register :work_packages, class_name: 'Activity::WorkPackageActivityProvider'
activity.register :changesets, class_name: 'Activity::ChangesetActivityProvider'
activity.register :news, class_name: 'Activity::NewsActivityProvider'
activity.register :news, class_name: 'Activity::NewsActivityProvider', default: false
activity.register :wiki_edits, class_name: 'Activity::WikiContentActivityProvider', default: false
activity.register :messages, class_name: 'Activity::MessageActivityProvider', default: false
activity.register :time_entries, class_name: 'Activity::TimeEntryActivityProvider', default: false

@ -176,7 +176,7 @@ module Redmine
def without_trailling_slash(path)
path ||= ''
(path[-1,1] == "/") ? path[0..-2] : path
end
end
def shell_quote(str)
self.class.shell_quote(str)

@ -39,6 +39,12 @@ describe ActivitiesController do
end
describe 'index' do
shared_examples_for 'valid index response' do
it { expect(response).to be_success }
it { expect(response).to render_template 'index' }
end
describe 'global' do
let(:work_package) { FactoryGirl.create(:work_package) }
let!(:journal) { FactoryGirl.create(:work_package_journal,
@ -53,11 +59,9 @@ describe ActivitiesController do
before { get 'index' }
it { expect(response).to be_success }
it { expect(response).to render_template 'index' }
it_behaves_like 'valid index response'
it { expect(assigns(:event_by_day)).to be_nil }
it { expect(assigns(:events_by_day)).not_to be_empty }
describe 'view' do
render_views
@ -72,6 +76,14 @@ describe ActivitiesController do
:content => /#{ERB::Util.html_escape(work_package.subject)}/ } } }
end
end
describe 'empty filter selection' do
before { get 'index', apply: true }
it_behaves_like 'valid index response'
it { expect(assigns(:events_by_day)).to be_empty }
end
end
describe 'with activated activity module' do
@ -96,6 +108,12 @@ describe ActivitiesController do
end
end
shared_context 'index with params' do
let(:session_values) { defined?(session_hash) ? session_hash : {} }
before { get :index, params, session_values }
end
describe :atom_feed do
let(:user) { FactoryGirl.create(:user) }
let(:project) { FactoryGirl.create(:project) }
@ -129,7 +147,7 @@ describe ActivitiesController do
let(:params) { { project_id: project.id,
format: :atom } }
before { get :index, params }
include_context 'index with params'
it { expect(assigns(:items).count).to eq(2) }
@ -145,15 +163,52 @@ describe ActivitiesController do
let!(:message_2) { FactoryGirl.create(:message,
board: board) }
let(:params) { { project_id: project.id,
apply: true,
show_messages: 1,
format: :atom } }
before { get :index, params }
include_context 'index with params'
it { expect(assigns(:items).count).to eq(2) }
it { expect(response).to render_template("common/feed") }
end
end
describe 'user selection' do
describe 'first activity request' do
let(:default_scope) { ['work_packages', 'changesets'] }
let(:params) { {} }
include_context 'index with params'
it { expect(assigns(:activity).scope).to match_array(default_scope) }
it { expect(session[:activity]).to match_array(default_scope) }
end
describe 'subsequent activity requests' do
let(:scope) { [] }
let(:params) { {} }
let(:session_hash) { { activity: [] } }
include_context 'index with params'
it { expect(assigns(:activity).scope).to match_array(scope) }
it { expect(session[:activity]).to match_array(scope) }
end
describe 'selection with apply' do
let(:scope) { [] }
let(:params) { { apply: true } }
include_context 'index with params'
it { expect(assigns(:activity).scope).to match_array(scope) }
it { expect(session[:activity]).to match_array(scope) }
end
end
end
end

@ -30,6 +30,7 @@ require 'spec_helper'
describe WorkPackages::BulkController do
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user)}
let(:custom_field_value) { '125' }
let(:custom_field_1) { FactoryGirl.create(:work_package_custom_field,
field_format: 'string',
@ -47,17 +48,22 @@ describe WorkPackages::BulkController do
permissions: [:edit_work_packages,
:view_work_packages,
:manage_subtasks]) }
let(:member_1) { FactoryGirl.create(:member,
let(:member1_p1) { FactoryGirl.create(:member,
project: project_1,
principal: user,
roles: [role]) }
let(:member_2) { FactoryGirl.create(:member,
let(:member2_p1) { FactoryGirl.create(:member,
project: project_1,
principal: user2,
roles: [role]) }
let(:member1_p2) { FactoryGirl.create(:member,
project: project_2,
principal: user,
roles: [role]) }
let(:work_package_1) { FactoryGirl.create(:work_package,
author: user,
assigned_to: user,
responsible: user2,
type: type,
status: status,
custom_field_values: { custom_field_1.id => custom_field_value },
@ -65,6 +71,7 @@ describe WorkPackages::BulkController do
let(:work_package_2) { FactoryGirl.create(:work_package,
author: user,
assigned_to: user,
responsible: user2,
type: type,
status: status,
custom_field_values: { custom_field_1.id => custom_field_value },
@ -80,7 +87,8 @@ describe WorkPackages::BulkController do
before do
custom_field_1
member_1
member1_p1
member2_p1
User.stub(:current).and_return user
end
@ -122,7 +130,7 @@ describe WorkPackages::BulkController do
context "different projects" do
before do
member_2
member1_p2
get :edit, ids: [work_package_1.id, work_package_2.id, work_package_3.id]
end
@ -156,6 +164,7 @@ describe WorkPackages::BulkController do
let(:work_packages) { WorkPackage.find_all_by_id(work_package_ids) }
let(:priority) { FactoryGirl.create(:priority_immediate) }
let(:group_id) { '' }
let(:responsible_id) {''}
describe :redirect do
context "in host" do
@ -205,8 +214,8 @@ describe WorkPackages::BulkController do
before do
# create user memberships to allow the user to watch work packages
member_1
member_2
member1_p1
member1_p2
# let other_user perform the bulk update
User.stub(:current).and_return other_user
put :update, ids: work_package_ids, work_package: work_package_params
@ -233,6 +242,7 @@ describe WorkPackages::BulkController do
notes: 'Bulk editing',
work_package: { priority_id: priority.id,
assigned_to_id: group_id,
responsible_id: responsible_id,
custom_field_values: { custom_field_1.id.to_s => '' },
send_notification: send_notification }
end
@ -302,7 +312,7 @@ describe WorkPackages::BulkController do
let(:work_package_ids) { [work_package_1.id, work_package_2.id, work_package_3.id] }
context "with permission" do
before { member_2 }
before { member1_p2 }
include_context :update_request
@ -338,6 +348,16 @@ describe WorkPackages::BulkController do
it { should =~ [group_id] }
end
describe :responsible do
let(:responsible_id) { user.id }
include_context :update_request
subject { work_packages.collect {|w| w.responsible_id }.uniq }
it { should =~ [responsible_id] }
end
describe :status do
let(:closed_status) { FactoryGirl.create(:closed_status) }
let(:workflow) { FactoryGirl.create(:workflow,
@ -402,6 +422,18 @@ describe WorkPackages::BulkController do
it { should =~ [nil] }
end
describe :delete_responsible do
before do
put :update,
ids: work_package_ids,
work_package: { responsible_id: 'none' }
end
subject { work_packages.collect(&:responsible_id).uniq }
it { should =~ [nil] }
end
describe :version do
describe "set fixed_version_id attribute to some version" do
let(:version) { FactoryGirl.create(:version,

@ -181,20 +181,29 @@ describe WorkPackages::ContextMenusController do
end
end
shared_examples_for :assigned_to do
let(:assigned_to_link) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5Bassigned_to_id%5D=#{user.id}" }
shared_examples_for :assignee_or_responsible do
let(:link) { "/work_packages/bulk?#{ids_link}"\
"&amp;work_package%5B#{assignee_or_responsible}_id%5D=#{user.id}" }
before { get :index, ids: ids }
it do
assert_tag tag: 'a',
content: user.name,
attributes: { href: assigned_to_link,
attributes: { href: link,
:class => '' }
end
end
shared_examples_for :assigned_to do
let(:assignee_or_responsible) { "assigned_to"}
include_examples :assignee_or_responsible
end
shared_examples_for :responsible do
let(:assignee_or_responsible) { "responsible"}
include_examples :assignee_or_responsible
end
shared_examples_for :duplicate do
let(:duplicate_link) { "/projects/#{project_1.identifier}/work_packages"\
"/new?copy_from=#{ids.first}" }
@ -262,6 +271,8 @@ describe WorkPackages::ContextMenusController do
it_behaves_like :assigned_to
it_behaves_like :responsible
it_behaves_like :duplicate
it_behaves_like :copy
@ -300,6 +311,8 @@ describe WorkPackages::ContextMenusController do
it_behaves_like :assigned_to
it_behaves_like :responsible
it_behaves_like :copy
it_behaves_like :move
@ -329,6 +342,8 @@ describe WorkPackages::ContextMenusController do
it_behaves_like :assigned_to
it_behaves_like :responsible
it_behaves_like :delete
end

@ -186,7 +186,11 @@ describe WorkPackagesController do
controller.should_receive(:send_data).with(mock_csv,
:type => 'text/csv; header=present',
:filename => 'export.csv').and_call_original
:filename => 'export.csv') do |*args|
# We need to render something because otherwise
# the controller will and he will not find a suitable template
controller.render :text => "success"
end
end
it 'should fulfill the defined should_receives' do
@ -207,7 +211,11 @@ describe WorkPackagesController do
controller.should_receive(:send_data).with(mock_pdf,
:type => 'application/pdf',
:filename => 'export.pdf').and_call_original
:filename => 'export.pdf') do |*args|
# We need to render something because otherwise
# the controller will and he will not find a suitable template
controller.render :text => "success"
end
end
it 'should fulfill the defined should_receives' do
@ -222,7 +230,11 @@ describe WorkPackagesController do
requires_export_permission do
before do
controller.should_receive(:render_feed).with(work_packages, anything()).and_call_original
controller.should_receive(:render_feed).with(work_packages, anything()) do |*args|
# We need to render something because otherwise
# the controller will and he will not find a suitable template
controller.render :text => "success"
end
end
it 'should fulfill the defined should_receives' do
@ -265,7 +277,11 @@ describe WorkPackagesController do
WorkPackage::Exporter.should_receive(:work_package_to_pdf).and_return(pdf)
controller.should_receive(:send_data).with(pdf,
:type => 'application/pdf',
:filename => expected_name).and_call_original
:filename => expected_name) do |*args|
# We need to render something because otherwise
# the controller will and he will not find a suitable template
controller.render :text => "success"
end
call_action
end
end
@ -395,6 +411,48 @@ describe WorkPackagesController do
end
end
describe 'update w/ a time entry' do
render_views
let(:admin) { FactoryGirl.create(:admin) }
let(:work_package) { FactoryGirl.create(:work_package) }
let(:default_activity) { FactoryGirl.create(:default_activity) }
let(:activity) { FactoryGirl.create(:activity) }
let(:params) do
lambda do |work_package_id, activity_id|
{
:id => work_package_id,
:work_package => {
:time_entry => {
:hours => '',
:comments => '',
:activity_id => activity_id
}
}
}
end
end
before do
User.stub(:current).and_return admin
end
it 'should not try to create a time entry if blank' do
# default activity counts as blank as long as everything else is blank too
put 'update', params.call(work_package.id, default_activity.id)
expect(response.status).to eq(200)
expect(response.body).to have_content("Successful update")
end
it 'should still give an error for a non-blank time entry' do
put 'update', params.call(work_package.id, activity.id)
expect(response.status).to eq(200) # shouldn't this be 400 or similar?
expect(response.body).to have_content("Log time is invalid")
end
end
describe 'update.html' do
let(:wp_params) { { :wp_attribute => double('wp_attribute') } }
let(:params) { { :id => stub_work_package.id, :work_package => wp_params } }

@ -167,15 +167,15 @@ describe WorkPackage do
it { should eq(category.assigned_to) }
end
describe :assignable_users do
describe :assignable_assignees do
let(:user) { FactoryGirl.build_stubbed(:user) }
context "single user" do
before { stub_work_package.project.stub(:assignable_users).and_return([user]) }
before { stub_work_package.project.stub(:possible_assignees).and_return([user]) }
subject { stub_work_package.assignable_users }
subject { stub_work_package.assignable_assignees }
it 'should return all users the project deems to be assignable' do
it 'should return all users the project deems to be possible assignees' do
should include(user)
end
end
@ -189,7 +189,7 @@ describe WorkPackage do
work_package.project.add_member! group, FactoryGirl.create(:role)
end
subject { work_package.assignable_users }
subject { work_package.assignable_assignees }
it { should include(group) }
end
@ -202,21 +202,36 @@ describe WorkPackage do
work_package.project.add_member! group, FactoryGirl.create(:role)
end
subject { work_package.assignable_users }
subject { work_package.assignable_assignees }
it { should_not include(group) }
end
context "multiple users" do
let(:user_2) { FactoryGirl.build_stubbed(:user) }
before { stub_work_package.project.stub(:assignable_users).and_return([user, user_2]) }
before { stub_work_package.project.stub(:assignable_assignees).and_return([user, user_2]) }
subject { stub_work_package.assignable_users.uniq }
subject { stub_work_package.assignable_assignees.uniq }
it { should eq(stub_work_package.assignable_users) }
it { should eq(stub_work_package.assignable_assignees) }
end
end
describe :assignable_responsibles do
let(:user) { FactoryGirl.create(:user) }
let(:group) { FactoryGirl.create(:group) }
before do
work_package.project.add_member! user, FactoryGirl.create(:role)
work_package.project.add_member! group, FactoryGirl.create(:role)
end
subject { work_package.assignable_responsibles }
it { should_not include(group) }
it { should include(user) }
end
describe :assignable_versions do
def stub_shared_versions(v = nil)
versions = v ? [v] : []

@ -40,11 +40,12 @@ class JournalObserverTest < ActiveSupport::TestCase
:member_in_project => @project
@issue = FactoryGirl.create :work_package,
:project => @project,
:author => @user,
:author => @user,
:type => @type,
:status => @workflow.old_status
@user.members.first.roles << @workflow.role
@user.reload
User.stubs(:current).returns(@user)
@ -57,7 +58,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_difference('ActionMailer::Base.deliveries.size', +1) do
@issue.add_journal(@user)
@issue.subject = "A change to the issue"
assert @issue.save
assert @issue.save(validate: false)
end
end
@ -66,7 +67,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.add_journal(@user)
@issue.subject = "A change to the issue"
assert @issue.save
assert @issue.save(validate: false)
end
end
end
@ -78,7 +79,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = ['issue_note_added']
assert_difference('ActionMailer::Base.deliveries.size', +1) do
@issue.add_journal(@user, 'This update has a note')
assert @issue.save
assert @issue.save(validate: false)
end
end
@ -86,7 +87,7 @@ class JournalObserverTest < ActiveSupport::TestCase
Setting.notified_events = []
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.add_journal(@user, 'This update has a note')
assert @issue.save
assert @issue.save(validate: false)
end
end
end
@ -97,7 +98,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_difference('ActionMailer::Base.deliveries.size', +1) do
@issue.add_journal(@user)
@issue.status = @workflow.new_status
assert @issue.save
assert @issue.save(validate: false)
end
end
@ -106,7 +107,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.add_journal(@user)
@issue.status = @workflow.new_status
assert @issue.save
assert @issue.save(validate: false)
end
end
end
@ -117,7 +118,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_difference('ActionMailer::Base.deliveries.size', +1) do
@issue.add_journal(@user)
@issue.priority = IssuePriority.generate!
assert @issue.save
assert @issue.save(validate: false)
end
end
@ -126,7 +127,7 @@ class JournalObserverTest < ActiveSupport::TestCase
assert_no_difference('ActionMailer::Base.deliveries.size') do
@issue.add_journal(@user)
@issue.priority = IssuePriority.generate!
assert @issue.save
assert @issue.save(validate: false)
end
end
end

Loading…
Cancel
Save