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

pull/827/head
Hagen Schink 11 years ago
commit 11f6942906
  1. 3
      app/assets/javascripts/application.js.erb
  2. 202
      app/assets/javascripts/jquery.trap.js
  3. 4
      app/assets/javascripts/modal.js
  4. 143
      app/assets/javascripts/types_checkboxes.js
  5. 12
      app/controllers/api/v2/planning_elements_controller.rb
  6. 16
      app/controllers/api/v2/users_controller.rb
  7. 52
      app/controllers/api/v2/work_package_priorities_controller.rb
  8. 15
      app/helpers/application_helper.rb
  9. 2
      app/models/activity/work_package_activity_provider.rb
  10. 2
      app/models/project.rb
  11. 4
      app/models/type.rb
  12. 6
      app/models/work_package.rb
  13. 9
      app/services/user_search_service.rb
  14. 32
      app/views/api/v2/work_package_priorities/index.api.rabl
  15. 76
      app/views/my/account.html.erb
  16. 3
      app/views/projects/_form.html.erb
  17. 99
      app/views/projects/form/_types.html.erb
  18. 1
      app/views/projects/settings/_types.html.erb
  19. 1
      config/routes.rb
  20. 6
      doc/CHANGELOG.md
  21. 2
      lib/redmine/scm/adapters/abstract_adapter.rb
  22. 86
      spec/controllers/api/v2/planning_elements_controller_spec.rb
  23. 85
      spec/controllers/api/v2/users_controller_spec.rb
  24. 69
      spec/controllers/api/v2/work_package_priorities_controller_spec.rb
  25. 30
      spec/models/work_package_spec.rb
  26. 40
      spec/routing/api/v2/work_package_priorities_routing_spec.rb
  27. 98
      spec/views/api/v2/work_package_priorities/index_api_xml_spec.rb

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

@ -0,0 +1,143 @@
//-- 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.
//++
(function($) {
var TypesCheckboxes = function () {
this.init();
};
TypesCheckboxes.prototype = $.extend(TypesCheckboxes.prototype, {
init: function () {
this.append_checkbox_listeners();
this.append_check_uncheck_all_listeners();
if (this.everything_unchecked()) {
this.check_and_disable_standard_type();
}
},
append_checkbox_listeners: function () {
var self = this;
this.all_checkboxes().on("change", function () {
if (self.everything_unchecked()) {
self.check_and_disable_standard_type();
self.display_explanation();
} else {
self.hide_explanation();
self.enable_standard_type();
}
});
},
append_check_uncheck_all_listeners: function () {
var self = this;
$("#project_types #check_all_types").click(function (event) {
self.enable_all_checkboxes();
self.check(self.all_checkboxes());
self.hide_explanation();
event.preventDefault();
});
$("#project_types #uncheck_all_types").click(function (event) {
self.enable_all_checkboxes();
self.uncheck(self.all_except_standard());
self.check_and_disable_standard_type();
self.display_explanation();
event.preventDefault();
});
},
everything_unchecked: function () {
return !(this.all_except_standard().filter(":checked").length > 0);
},
check_and_disable_standard_type: function () {
var standard = this.standard_check_boxes();
this.check($(standard));
this.disable($(standard));
},
enable_standard_type: function () {
this.enable(this.standard_check_boxes());
},
enable_all_checkboxes: function () {
this.enable(this.all_checkboxes())
},
check: function (boxes) {
$(boxes).prop("checked", true);
},
uncheck: function (boxes) {
$(boxes).prop("checked", false);
},
disable: function (boxes) {
var self = this;
$(boxes).prop('disabled', true);
$(boxes).each(function (ix, item) {
self.hidden_type_field($(item)).prop("value", $(item).prop("value"));
});
},
enable: function (boxes) {
var self = this;
$(boxes).prop('disabled', false);
$(boxes).each(function (ix, item) {
self.hidden_type_field($(item)).prop("value", "");
});
},
display_explanation: function () {
$("#types_flash_notice").show();
},
hide_explanation: function () {
$("#types_flash_notice").hide();
},
all_checkboxes: function () {
return $(".types :input[type='checkbox']");
},
all_except_standard: function () {
return $(".types :input[type='checkbox'][data-standard='false']");
},
standard_check_boxes: function () {
return $(".types :input[type='checkbox'][data-standard='true']");
},
hidden_type_field: function (for_box) {
return $(".types :input[type='hidden'][data-for='" + $(for_box).prop("id") + "']");
}
});
$('document').ready(function () {
new TypesCheckboxes();
});
})(jQuery);

@ -38,6 +38,7 @@ module Api
before_filter :find_project_by_project_id,
:authorize, :except => [:index]
before_filter :parse_changed_since, only: [:index]
before_filter :assign_planning_elements, :except => [:index, :update, :create]
# Attention: find_all_projects_by_project_id needs to mimic all of the above
@ -91,7 +92,9 @@ module Api
def update
@planning_element = WorkPackage.find(params[:id])
@planning_element.attributes = permitted_params.planning_element
@planning_element.attributes = permitted_params.planning_element.except :note
@planning_element.add_journal(User.current, permitted_params.planning_element[:note])
successfully_updated = @planning_element.save
@ -207,6 +210,7 @@ module Api
def current_work_packages(projects)
work_packages = WorkPackage.for_projects(projects)
.changed_since(@since)
.includes(:status, :project, :type)
if params[:f]
@ -278,6 +282,12 @@ module Api
end
private
def parse_changed_since
@since = Time.at(Float(params[:changed_since] || 0).to_i) rescue render_400
end
end
end
end

@ -6,18 +6,24 @@ module Api
skip_filter :require_admin, :only => :index
before_filter :check_scope_supplied
def index
@users = UserSearchService.new(params).search.visible_by(User.current)
@users = UserSearchService.new(params).search
respond_to do |format|
format.api
end
end
end
end
private
def check_scope_supplied
render_400 if params.select { |k,v| UserSearchService::SEARCH_SCOPES.include? k }
.select { |k,v| not v.blank? }
.empty?
end
end
end
end

@ -0,0 +1,52 @@
#-- 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 WorkPackagePrioritiesController < ApplicationController
include PaginationHelper
include ::Api::V2::ApiController
unloadable
accept_key_auth :index
def index
@priorities = IssuePriority.all
respond_to do |format|
format.api
end
end
end
end
end

@ -269,11 +269,7 @@ module ApplicationHelper
# Renders flash messages
def render_flash_messages
if User.current.impaired?
flash.map { |k,v| content_tag('div', content_tag('a', join_flash_messages(v), :href => 'javascript:;'), :class => "flash #{k} icon icon-#{k}") }.join.html_safe
else
flash.map { |k,v| content_tag('div', join_flash_messages(v), :class => "flash #{k} icon icon-#{k}") }.join.html_safe
end
flash.map { |k,v| render_flash_message(k, v) }.join.html_safe
end
def join_flash_messages(messages)
@ -284,6 +280,15 @@ module ApplicationHelper
end
end
def render_flash_message(type, message, html_options = {})
html_options = {:class => "flash #{type} icon icon-#{type}"}.merge(html_options)
if User.current.impaired?
content_tag('div', content_tag('a', join_flash_messages(message), :href => 'javascript:;'), html_options)
else
content_tag('div', join_flash_messages(message), html_options)
end
end
# Renders tabs and their content
def render_tabs(tabs)
if tabs.any?

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

@ -239,7 +239,7 @@ class Project < ActiveRecord::Base
self.enabled_module_names = Setting.default_projects_modules
end
if !initialized.key?('types') && !initialized.key?('type_ids')
self.types = Type.where(is_default: true)
self.types = Type.default
end
end

@ -89,6 +89,10 @@ class Type < ActiveRecord::Base
Type.where(is_standard: true).first
end
def self.default
Type.where(is_default: true)
end
def statuses
return [] if new_record?
@statuses ||= Type.statuses([id])

@ -78,6 +78,10 @@ class WorkPackage < ActiveRecord::Base
{:conditions => {:project_id => projects}}
}
scope :changed_since, lambda { |changed_since|
changed_since ? where(["#{WorkPackage.table_name}.updated_at >= ?", changed_since]) : nil
}
# >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
scope :open, :conditions => ["#{Status.table_name}.is_closed = ?", false], :include => :status
@ -394,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

@ -1,6 +1,13 @@
class UserSearchService
attr_accessor :params
SEARCH_SCOPES = [
'ids',
'group_id',
'status',
'name'
]
def initialize(params)
self.params = params
end
@ -46,4 +53,4 @@ class UserSearchService
# .order(sort_clause)
end
end
end

@ -0,0 +1,32 @@
#-- 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.
#++
collection @priorities => :work_package_priorities
attributes :id,
:name,
:position,
:is_default

@ -39,43 +39,45 @@ See doc/COPYRIGHT.rdoc for more details.
:builder => TabularFormBuilder,
:lang => current_language,
:html => { :id => 'my_account_form' } do |f| %>
<div class="splitcontentleft">
<h3><%=l(:label_information_plural)%></h3>
<div class="box tabular">
<p><%= f.text_field :firstname, :required => true %></p>
<p><%= f.text_field :lastname, :required => true %></p>
<p><%= f.text_field :mail, :required => true %></p>
<p><%= f.select :language, lang_options_for_select %></p>
<% if Setting.openid? %>
<p><%= f.text_field :identity_url %></p>
<% end %>
<% @user.custom_field_values.select(&:editable?).each do |value| %>
<p><%= custom_field_tag_with_label :user, value %></p>
<% end %>
<%= call_hook(:view_my_account, :user => @user, :form => f) %>
</div>
<%= submit_tag l(:button_save) %>
</div>
<div class="splitcontentright">
<h3><%= User.human_attribute_name(:mail_notification) %></h3>
<div class="box">
<%= render :partial => 'users/mail_notifications' %>
</div>
<h3><%=l(:label_ui, :app_title => Setting.app_title)%></h3>
<div class="box tabular">
<%= render :partial => 'users/impaired_settings' %>
</div>
<h3><%=l(:label_preferences)%></h3>
<div class="box tabular">
<%= render :partial => 'users/preferences' %>
</div>
</div>
<div>
<div class="splitcontentleft">
<h3><%=l(:label_information_plural)%></h3>
<div class="box tabular">
<p><%= f.text_field :firstname, :required => true %></p>
<p><%= f.text_field :lastname, :required => true %></p>
<p><%= f.text_field :mail, :required => true %></p>
<p><%= f.select :language, lang_options_for_select %></p>
<% if Setting.openid? %>
<p><%= f.text_field :identity_url %></p>
<% end %>
<% @user.custom_field_values.select(&:editable?).each do |value| %>
<p><%= custom_field_tag_with_label :user, value %></p>
<% end %>
<%= call_hook(:view_my_account, :user => @user, :form => f) %>
</div>
</div>
<div class="splitcontentright">
<h3><%= User.human_attribute_name(:mail_notification) %></h3>
<div class="box">
<%= render :partial => 'users/mail_notifications' %>
</div>
<h3><%=l(:label_ui, :app_title => Setting.app_title)%></h3>
<div class="box tabular">
<%= render :partial => 'users/impaired_settings' %>
</div>
<h3><%=l(:label_preferences)%></h3>
<div class="box tabular">
<%= render :partial => 'users/preferences' %>
</div>
</div>
<br style="clear:both;" />
</div>
<%= submit_tag l(:button_save) %>
<% end %>
<% html_title(l(:label_my_account)) -%>

@ -46,10 +46,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% if (project.new_record? || project.module_enabled?('issue_tracking')) %>
<% if renderTypes %>
<fieldset class="box" id="project_types"><legend><%=l(:label_type_plural)%> <span style="font-size:0.9em">(<%= check_all_links 'project_types' %>)</span></legend>
<%= render :partial => 'projects/form/types', :locals => { :f => f, :project => project } %>
<%= hidden_field_tag 'project[type_ids][]', '' %>
</fieldset>
<% end %>
<% end %>

@ -27,45 +27,64 @@ See doc/COPYRIGHT.rdoc for more details.
++#%>
<table class='list'>
<thead>
<tr>
<th width="90px" class='center'><%= Type.human_attribute_name(:active) %></th>
<th><%= Type.human_attribute_name(:name) %></th>
<th class='center'><%= Type.human_attribute_name(:in_aggregation) %></th>
<th class='center'><%= Type.human_attribute_name(:is_in_roadmap) %></th>
<th class='center'><%= Type.human_attribute_name(:is_milestone) %></th>
</tr>
</thead>
<%= javascript_include_tag 'types_checkboxes' %>
<tbody>
<% Type.all.each do |type| %>
<tr class="<%= cycle('odd', 'even', :name => "pet_table") %>">
<td class='center'>
<%= check_box_tag "project[type_ids][]",
type.id,
project.types.include?(type),
:id => "project_planning_element_type_ids_#{type.id}" %>
<label class='hidden-for-sighted' for="project_planning_element_type_ids_<%= type.id %>">
<%= l('timelines.enable_type_in_project', :type => type.name) %>
</label>
</td>
<td>
<label for="project_planning_element_type_ids_<%= type.id %>">
<%= icon_for_type(type) %>
<%=h type.name %>
</label>
</td>
<td class='center'>
<%= checked_image(type.in_aggregation) %>
</td>
<td class='center'>
<%= checked_image(type.is_in_roadmap) %>
</td>
<td class='center'>
<%= checked_image(type.is_milestone) %>
</td>
<%= render_flash_message :notice,
l(:notice_automatic_set_of_standard_type),
style: "display:none;", id: "types_flash_notice" %>
<fieldset class="box" id="project_types">
<legend><%=l(:label_type_plural)%>
<span style="font-size:0.9em">
(<%= link_to(l(:button_check_all), "#", id: "check_all_types") +
' | ' +
link_to(l(:button_uncheck_all), "#", id: "uncheck_all_types")
%>)
</span>
</legend>
<table class='list types'>
<thead>
<tr>
<th width="90px" class='center'><%= Type.human_attribute_name(:active) %></th>
<th><%= Type.human_attribute_name(:name) %></th>
<th class='center'><%= Type.human_attribute_name(:in_aggregation) %></th>
<th class='center'><%= Type.human_attribute_name(:is_in_roadmap) %></th>
<th class='center'><%= Type.human_attribute_name(:is_milestone) %></th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% Type.all.each do |type| %>
<tr class="<%= cycle('odd', 'even', :name => "pet_table") %>">
<td class='center'>
<% type_id = "project_planning_element_type_ids_#{type.id}" %>
<%= check_box_tag "project[type_ids][]",
type.id,
project.types.include?(type),
:id => type_id,
:'data-standard' => type.is_standard %>
<%= hidden_field_tag 'project[type_ids][]', '', :'data-for' => type_id %>
<label class='hidden-for-sighted' for=<%= type_id %>>
<%= l('timelines.enable_type_in_project', :type => type.name) %>
</label>
</td>
<td>
<label for="project_planning_element_type_ids_<%= type.id %>">
<%= icon_for_type(type) %>
<%=h type.name %>
</label>
</td>
<td class='center'>
<%= checked_image(type.in_aggregation) %>
</td>
<td class='center'>
<%= checked_image(type.is_in_roadmap) %>
</td>
<td class='center'>
<%= checked_image(type.is_milestone) %>
</td>
</tr>
<% end %>
</tbody>
</table>
</fieldset>

@ -34,7 +34,6 @@ See doc/COPYRIGHT.rdoc for more details.
:url => { :action => 'types', :id => @project },
:method => :put,
:html => {:id => 'types-form'} do |f| %>
<%=l(:label_type_plural)%> <span style="font-size:0.9em">(<%= check_all_links 'project_types' %>)</span>
<%= render :partial => 'projects/form/types', :locals => { :f => f, :project => @project } %>
<p><%= submit_tag l(:button_save) %></p>

@ -78,6 +78,7 @@ OpenProject::Application.routes.draw do
resources :reported_project_statuses
resources :statuses, :only => [:index, :show]
resources :timelines
resources :work_package_priorities, only: [:index]
resources :projects do
resources :planning_elements

@ -32,16 +32,22 @@ 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
* `#3347` [API] Make priorities available via API
* `#3438` Activity default value makes log time required
* `#3451` API references hidden users
* `#3481` Fix: [Activity] Not possible to unselect all filters
* `#3730` Setting responsible via bulk edit
* `#3731` Setting responsible via context menu
* `#3774` Fix: [API] Not possible to set journal notes via API
* `#3843` Prettier translations for member errors
* `#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

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

@ -225,6 +225,56 @@ describe Api::V2::PlanningElementsController do
end
end
end
describe 'changed since' do
let!(:work_package) do
work_package = Timecop.travel(5.hours.ago) do
wp = FactoryGirl.create(:work_package)
wp.save!
wp
end
work_package.subject = "Changed now!"
work_package.save!
work_package
end
become_admin { [work_package.project] }
shared_context 'get work packages changed since' do
before { get 'index', project_id: work_package.project_id, changed_since: timestamp, format: 'xml' }
end
describe 'valid timestamp' do
shared_examples_for 'valid timestamp' do
let(:timestamp) { (work_package.updated_at - 5.seconds).to_i }
include_context 'get work packages changed since'
it { expect(assigns(:planning_elements).collect(&:id)).to match_array([work_package.id]) }
end
shared_examples_for 'valid but early timestamp' do
let(:timestamp) { (work_package.updated_at + 5.seconds).to_i }
include_context 'get work packages changed since'
it { expect(assigns(:planning_elements)).to be_empty }
end
it_behaves_like 'valid timestamp'
it_behaves_like 'valid but early timestamp'
end
describe 'invalid timestamp' do
let(:timestamp) { 'eeek' }
include_context 'get work packages changed since'
it { expect(response.status).to eq(400) }
end
end
end
describe 'w/ list of projects' do
@ -551,6 +601,7 @@ describe Api::V2::PlanningElementsController do
describe 'update.xml' do
let(:project) { FactoryGirl.create(:project, :is_public => false) }
let(:work_package) { FactoryGirl.create(:work_package) }
become_admin
@ -571,6 +622,41 @@ describe Api::V2::PlanningElementsController do
it_should_behave_like "a controller action which needs project permissions"
end
describe 'empty' do
before do
put :update,
project_id: work_package.project_id,
id: work_package.id,
format: :xml
end
it { expect(response.status).to eq(400) }
end
describe 'notes' do
let(:note) { "A note set by API" }
before do
put :update,
project_id: work_package.project_id,
id: work_package.id,
planning_element: { note: note },
format: :xml
end
it { expect(response.status).to eq(204) }
describe 'journals' do
subject { work_package.reload.journals }
it { expect(subject.count).to eq(2) }
it { expect(subject.last.notes).to eq(note) }
it { expect(subject.last.user).to eq(User.current) }
end
end
describe 'with custom fields' do
let(:type) { Type.find_by_name("None") || FactoryGirl.create(:type_standard) }

@ -29,50 +29,91 @@
require 'spec_helper'
describe Api::V2::UsersController do
let(:current_user) { FactoryGirl.create(:admin) }
before do
User.stub(:current).and_return current_user
shared_context "As an admin" do
let(:current_user) { FactoryGirl.create(:admin) }
before { User.stub(:current).and_return current_user }
end
shared_context "As a normal user" do
let(:current_user) { FactoryGirl.create(:user) }
before { User.stub(:current).and_return current_user }
end
shared_examples_for "valid user API call" do
it { expect(assigns(:users).size).to eq(user_count) }
it { expect(response).to render_template('api/v2/users/index', formats: ["api"]) }
end
describe 'index.json' do
describe 'with 3 visible users' do
describe 'scopes' do
shared_examples_for "no scope provided" do
it { expect(response.status).to eq(400) }
end
before do
3.times do
FactoryGirl.create(:user)
end
context "no scope" do
before { get 'index', format: :json }
get 'index', :format => 'json'
it_behaves_like "no scope provided"
end
it 'returns 3 users' do
assigns(:users).size.should eql 3+1 # the admin is also available, when all users are selected
context "empty scope" do
before { get 'index', ids: "", format: :json }
it_behaves_like "no scope provided"
end
it 'renders the index template' do
response.should render_template('api/v2/users/index', :formats => ["api"])
context "filled scope" do
before { get 'index', ids: "1", format: :json }
it_behaves_like "valid user API call" do
let(:user_count) { 0 }
end
end
end
describe 'search for ids' do
let (:user_1) {FactoryGirl.create(:user)}
let (:user_2) {FactoryGirl.create(:user)}
it 'returns the users for requested ids' do
get 'index', ids: "#{user_1.id},#{user_2.id}", :format => 'json'
describe 'with 3 users' do
let(:ids) { User.all.collect(&:id).join(',') }
found_users = assigns(:users)
before { 3.times { FactoryGirl.create(:user) } }
found_users.size.should eql 2
found_users.should include user_1,user_2
context 'as an admin' do
include_context "As an admin"
before { get 'index', ids: ids, format: :json }
it_behaves_like "valid user API call" do
let(:user_count) { 4 }
end
end
context 'as a normal user' do
include_context "As a normal user"
before { get 'index', ids: ids, :format => 'json' }
it_behaves_like "valid user API call" do
let(:user_count) { 4 }
end
end
end
describe 'search for ids' do
include_context "As an admin"
let (:user_1) {FactoryGirl.create(:user)}
let (:user_2) {FactoryGirl.create(:user)}
before { get 'index', ids: "#{user_1.id},#{user_2.id}", :format => 'json' }
subject { assigns(:users) }
it { expect(subject.size).to eq(2) }
it { expect(subject).to include(user_1, user_2) }
end
end
end

@ -0,0 +1,69 @@
#-- 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::WorkPackagePrioritiesController do
let(:current_user) { FactoryGirl.create(:admin) }
before { User.stub(:current).and_return current_user }
describe '#index' do
shared_examples_for 'valid work package priority index request' do
it { expect(response).to be_success }
it { expect(response).to render_template('api/v2/work_package_priorities/index', format: ['api']) }
end
describe 'w/o priorities' do
before { get :index, format: :xml }
it { expect(assigns(:priorities)).to be_empty }
it_behaves_like 'valid work package priority index request'
end
describe 'w/o priorities' do
let!(:priority_0) { FactoryGirl.create(:priority) }
let!(:priority_1) { FactoryGirl.create(:priority,
position: 1) }
let!(:priority_2) { FactoryGirl.create(:priority,
position: 2,
is_default: true) }
before { get :index, format: :xml }
it { expect(assigns(:priorities)).not_to be_empty }
it { expect(assigns(:priorities).count).to eq(3) }
it_behaves_like 'valid work package priority index request'
end
end
end

@ -1402,7 +1402,35 @@ describe WorkPackage do
# assert that there is only one error
expect(work_package.errors.size).to eq 1
expect(work_package.errors_on(:custom_values).size).to eq 1
end
end
end
describe 'changed_since' do
let!(:work_package) do
work_package = Timecop.travel(5.hours.ago) do
wp = FactoryGirl.create(:work_package)
wp.save!
wp
end
end
describe 'null' do
subject { WorkPackage.changed_since(nil) }
it { expect(subject).to match_array([work_package]) }
end
describe 'now' do
subject { WorkPackage.changed_since(DateTime.now) }
it { expect(subject).to be_empty }
end
describe 'work package update' do
subject { WorkPackage.changed_since(work_package.updated_at) }
it { expect(subject).to match_array([work_package]) }
end
end
end

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

@ -0,0 +1,98 @@
#-- 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/work_package_priorities/index.api.rabl' do
before { params[:format] = 'xml' }
describe 'with no work package priorities available' do
before do
assign(:priorities, [])
render
end
subject { response.body }
it 'renders an empty work_package_priorities document' do
expect(subject).to have_selector('work_package_priorities', count: 1)
expect(subject).to have_selector('work_package_priorities[type=array]') do |tag|
expect(tag).to have_selector('work_package_priority', count: 0)
end
end
end
describe 'with 3 work package priorities available' do
let!(:priority_0) { FactoryGirl.create(:priority) }
let!(:priority_1) { FactoryGirl.create(:priority,
position: 1) }
let!(:priority_2) { FactoryGirl.create(:priority,
position: 2,
is_default: true) }
before do
assign(:priorities, [priority_0, priority_1, priority_2])
render
end
subject { Nokogiri.XML(response.body) }
it { expect(subject).to have_selector('work_package_priorities work_package_priority', count: 3) }
context 'priority 0' do
it 'has empty position' do
expect(subject).to have_selector('work_package_priorities work_package_priority id', text: priority_0.id) do |tag|
expect(tag.parent).to have_selector('position', text: nil)
end
end
it 'has empty default setting' do
expect(subject).to have_selector('work_package_priorities work_package_priority id', text: priority_0.id) do |tag|
expect(tag.parent).to have_selector('is_default', text: nil)
end
end
end
context 'priority 1' do
it 'has position' do
expect(subject).to have_selector('work_package_priorities work_package_priority id', text: priority_1.id) do |tag|
expect(tag.parent).to have_selector('position', text: priority_1.position)
end
end
end
context 'priority 2' do
it 'has default value set' do
expect(subject).to have_selector('work_package_priorities work_package_priority id', text: priority_2.id) do |tag|
expect(tag.parent).to have_selector('position', text: priority_2.is_default)
end
end
end
end
end
Loading…
Cancel
Save