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. 14
      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. 7
      app/services/user_search_service.rb
  14. 32
      app/views/api/v2/work_package_priorities/index.api.rabl
  15. 8
      app/views/my/account.html.erb
  16. 3
      app/views/projects/_form.html.erb
  17. 25
      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. 86
      spec/controllers/api/v2/planning_elements_controller_spec.rb
  22. 81
      spec/controllers/api/v2/users_controller_spec.rb
  23. 69
      spec/controllers/api/v2/work_package_priorities_controller_spec.rb
  24. 28
      spec/models/work_package_spec.rb
  25. 40
      spec/routing/api/v2/work_package_priorities_routing_spec.rb
  26. 98
      spec/views/api/v2/work_package_priorities/index_api_xml_spec.rb

@ -46,6 +46,7 @@
//= require jquery_ujs //= require jquery_ujs
//= require jquery_noconflict //= require jquery_noconflict
//= require jquery.colorcontrast //= require jquery.colorcontrast
//= require jquery.trap
//= require prototype //= require prototype
//= require effects //= require effects
//= require dragdrop //= require dragdrop
@ -117,7 +118,7 @@ jQuery(document).ready(function ($) {
dateFormat: 'yy-mm-dd', dateFormat: 'yy-mm-dd',
showButtonPanel: true, showButtonPanel: true,
calculateWeek: function (d) { calculateWeek: function (d) {
if (d.getDay() > 1) { if (d.getDay() != 1) {
d.setDate(d.getDate() - d.getDay() + 1); d.setDate(d.getDate() - d.getDay() + 1);
} }
return $.datepicker.iso8601Week(d); 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.hideLoadingModal();
this.loadingModal = false; 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) { body.on("keyup", function (e) {
if (e.which == 27) { if (e.which == 27) {
modalHelper.close(); 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, before_filter :find_project_by_project_id,
:authorize, :except => [:index] :authorize, :except => [:index]
before_filter :parse_changed_since, only: [:index]
before_filter :assign_planning_elements, :except => [:index, :update, :create] before_filter :assign_planning_elements, :except => [:index, :update, :create]
# Attention: find_all_projects_by_project_id needs to mimic all of the above # Attention: find_all_projects_by_project_id needs to mimic all of the above
@ -91,7 +92,9 @@ module Api
def update def update
@planning_element = WorkPackage.find(params[:id]) @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 successfully_updated = @planning_element.save
@ -207,6 +210,7 @@ module Api
def current_work_packages(projects) def current_work_packages(projects)
work_packages = WorkPackage.for_projects(projects) work_packages = WorkPackage.for_projects(projects)
.changed_since(@since)
.includes(:status, :project, :type) .includes(:status, :project, :type)
if params[:f] if params[:f]
@ -278,6 +282,12 @@ module Api
end end
private
def parse_changed_since
@since = Time.at(Float(params[:changed_since] || 0).to_i) rescue render_400
end
end end
end end
end end

@ -6,18 +6,24 @@ module Api
skip_filter :require_admin, :only => :index skip_filter :require_admin, :only => :index
before_filter :check_scope_supplied
def index def index
@users = UserSearchService.new(params).search.visible_by(User.current) @users = UserSearchService.new(params).search
respond_to do |format| respond_to do |format|
format.api format.api
end 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
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 # Renders flash messages
def render_flash_messages def render_flash_messages
if User.current.impaired? flash.map { |k,v| render_flash_message(k, v) }.join.html_safe
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
end end
def join_flash_messages(messages) def join_flash_messages(messages)
@ -284,6 +280,15 @@ module ApplicationHelper
end end
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 # Renders tabs and their content
def render_tabs(tabs) def render_tabs(tabs)
if tabs.any? if tabs.any?

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

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

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

@ -78,6 +78,10 @@ class WorkPackage < ActiveRecord::Base
{:conditions => {:project_id => projects}} {:conditions => {:project_id => projects}}
} }
scope :changed_since, lambda { |changed_since|
changed_since ? where(["#{WorkPackage.table_name}.updated_at >= ?", changed_since]) : nil
}
# >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
scope :open, :conditions => ["#{Status.table_name}.is_closed = ?", false], :include => :status scope :open, :conditions => ["#{Status.table_name}.is_closed = ?", false], :include => :status
@ -394,7 +398,7 @@ class WorkPackage < ActiveRecord::Base
end end
def to_s def to_s
"#{(kind.is_standard) ? l(:default_type) : "#{kind.name}"} ##{id}: #{subject}" "#{(kind.is_standard) ? "" : "#{kind.name}"} ##{id}: #{subject}"
end end
# Return true if the work_package is closed, otherwise false # Return true if the work_package is closed, otherwise false

@ -1,6 +1,13 @@
class UserSearchService class UserSearchService
attr_accessor :params attr_accessor :params
SEARCH_SCOPES = [
'ids',
'group_id',
'status',
'name'
]
def initialize(params) def initialize(params)
self.params = params self.params = params
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,6 +39,7 @@ See doc/COPYRIGHT.rdoc for more details.
:builder => TabularFormBuilder, :builder => TabularFormBuilder,
:lang => current_language, :lang => current_language,
:html => { :id => 'my_account_form' } do |f| %> :html => { :id => 'my_account_form' } do |f| %>
<div>
<div class="splitcontentleft"> <div class="splitcontentleft">
<h3><%=l(:label_information_plural)%></h3> <h3><%=l(:label_information_plural)%></h3>
<div class="box tabular"> <div class="box tabular">
@ -55,8 +56,6 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %> <% end %>
<%= call_hook(:view_my_account, :user => @user, :form => f) %> <%= call_hook(:view_my_account, :user => @user, :form => f) %>
</div> </div>
<%= submit_tag l(:button_save) %>
</div> </div>
<div class="splitcontentright"> <div class="splitcontentright">
@ -74,8 +73,11 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="box tabular"> <div class="box tabular">
<%= render :partial => 'users/preferences' %> <%= render :partial => 'users/preferences' %>
</div> </div>
</div> </div>
<br style="clear:both;" />
</div>
<%= submit_tag l(:button_save) %>
<% end %> <% end %>
<% html_title(l(:label_my_account)) -%> <% 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 (project.new_record? || project.module_enabled?('issue_tracking')) %>
<% if renderTypes %> <% 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 } %> <%= render :partial => 'projects/form/types', :locals => { :f => f, :project => project } %>
<%= hidden_field_tag 'project[type_ids][]', '' %>
</fieldset>
<% end %> <% end %>
<% end %> <% end %>

@ -27,7 +27,22 @@ See doc/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<table class='list'> <%= javascript_include_tag 'types_checkboxes' %>
<%= 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> <thead>
<tr> <tr>
<th width="90px" class='center'><%= Type.human_attribute_name(:active) %></th> <th width="90px" class='center'><%= Type.human_attribute_name(:active) %></th>
@ -42,11 +57,14 @@ See doc/COPYRIGHT.rdoc for more details.
<% Type.all.each do |type| %> <% Type.all.each do |type| %>
<tr class="<%= cycle('odd', 'even', :name => "pet_table") %>"> <tr class="<%= cycle('odd', 'even', :name => "pet_table") %>">
<td class='center'> <td class='center'>
<% type_id = "project_planning_element_type_ids_#{type.id}" %>
<%= check_box_tag "project[type_ids][]", <%= check_box_tag "project[type_ids][]",
type.id, type.id,
project.types.include?(type), project.types.include?(type),
:id => "project_planning_element_type_ids_#{type.id}" %> :id => type_id,
<label class='hidden-for-sighted' for="project_planning_element_type_ids_<%= 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) %> <%= l('timelines.enable_type_in_project', :type => type.name) %>
</label> </label>
</td> </td>
@ -69,3 +87,4 @@ See doc/COPYRIGHT.rdoc for more details.
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</fieldset>

@ -34,7 +34,6 @@ See doc/COPYRIGHT.rdoc for more details.
:url => { :action => 'types', :id => @project }, :url => { :action => 'types', :id => @project },
:method => :put, :method => :put,
:html => {:id => 'types-form'} do |f| %> :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 } %> <%= render :partial => 'projects/form/types', :locals => { :f => f, :project => @project } %>
<p><%= submit_tag l(:button_save) %></p> <p><%= submit_tag l(:button_save) %></p>

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

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

@ -225,6 +225,56 @@ describe Api::V2::PlanningElementsController do
end end
end 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 end
describe 'w/ list of projects' do describe 'w/ list of projects' do
@ -551,6 +601,7 @@ describe Api::V2::PlanningElementsController do
describe 'update.xml' do describe 'update.xml' do
let(:project) { FactoryGirl.create(:project, :is_public => false) } let(:project) { FactoryGirl.create(:project, :is_public => false) }
let(:work_package) { FactoryGirl.create(:work_package) }
become_admin become_admin
@ -571,6 +622,41 @@ describe Api::V2::PlanningElementsController do
it_should_behave_like "a controller action which needs project permissions" it_should_behave_like "a controller action which needs project permissions"
end 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 describe 'with custom fields' do
let(:type) { Type.find_by_name("None") || FactoryGirl.create(:type_standard) } let(:type) { Type.find_by_name("None") || FactoryGirl.create(:type_standard) }

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

@ -1404,5 +1404,33 @@ describe WorkPackage do
expect(work_package.errors_on(:custom_values).size).to eq 1 expect(work_package.errors_on(:custom_values).size).to eq 1
end 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 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