diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 4210ab9212..c070a3ebd8 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -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); diff --git a/app/assets/javascripts/jquery.trap.js b/app/assets/javascripts/jquery.trap.js new file mode 100644 index 0000000000..b99931983b --- /dev/null +++ b/app/assets/javascripts/jquery.trap.js @@ -0,0 +1,202 @@ +/*! +Copyright (c) 2011, 2012 Julien Wajsberg +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 ); diff --git a/app/assets/javascripts/modal.js b/app/assets/javascripts/modal.js index b7552c2fbd..758f9e99ff 100644 --- a/app/assets/javascripts/modal.js +++ b/app/assets/javascripts/modal.js @@ -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(); diff --git a/app/assets/javascripts/types_checkboxes.js b/app/assets/javascripts/types_checkboxes.js new file mode 100644 index 0000000000..5dc7c7c112 --- /dev/null +++ b/app/assets/javascripts/types_checkboxes.js @@ -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); diff --git a/app/controllers/api/v2/planning_elements_controller.rb b/app/controllers/api/v2/planning_elements_controller.rb index f07a19896a..9b242e662f 100644 --- a/app/controllers/api/v2/planning_elements_controller.rb +++ b/app/controllers/api/v2/planning_elements_controller.rb @@ -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 diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 178c56161e..46b7f31178 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -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 diff --git a/app/controllers/api/v2/work_package_priorities_controller.rb b/app/controllers/api/v2/work_package_priorities_controller.rb new file mode 100644 index 0000000000..5f998760c4 --- /dev/null +++ b/app/controllers/api/v2/work_package_priorities_controller.rb @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9df8a7423d..a05dc9cf6a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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? diff --git a/app/models/activity/work_package_activity_provider.rb b/app/models/activity/work_package_activity_provider.rb index 4078ff7534..df53231160 100644 --- a/app/models/activity/work_package_activity_provider.rb +++ b/app/models/activity/work_package_activity_provider.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 3797cfab0c..32abed3c33 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/type.rb b/app/models/type.rb index 02f7a8f604..92eca03332 100644 --- a/app/models/type.rb +++ b/app/models/type.rb @@ -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]) diff --git a/app/models/work_package.rb b/app/models/work_package.rb index ef618a6a24..288fca23ec 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -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 diff --git a/app/services/user_search_service.rb b/app/services/user_search_service.rb index eae0755e39..c5df7ec10a 100644 --- a/app/services/user_search_service.rb +++ b/app/services/user_search_service.rb @@ -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 \ No newline at end of file +end diff --git a/app/views/api/v2/work_package_priorities/index.api.rabl b/app/views/api/v2/work_package_priorities/index.api.rabl new file mode 100644 index 0000000000..22892cbecc --- /dev/null +++ b/app/views/api/v2/work_package_priorities/index.api.rabl @@ -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 diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb index a6f449584f..e5d71eee7d 100644 --- a/app/views/my/account.html.erb +++ b/app/views/my/account.html.erb @@ -39,43 +39,45 @@ See doc/COPYRIGHT.rdoc for more details. :builder => TabularFormBuilder, :lang => current_language, :html => { :id => 'my_account_form' } do |f| %> -
-

<%=l(:label_information_plural)%>

-
-

<%= f.text_field :firstname, :required => true %>

-

<%= f.text_field :lastname, :required => true %>

-

<%= f.text_field :mail, :required => true %>

-

<%= f.select :language, lang_options_for_select %>

-<% if Setting.openid? %> -

<%= f.text_field :identity_url %>

-<% end %> - -<% @user.custom_field_values.select(&:editable?).each do |value| %> -

<%= custom_field_tag_with_label :user, value %>

-<% end %> -<%= call_hook(:view_my_account, :user => @user, :form => f) %> -
- -<%= submit_tag l(:button_save) %> -
- -
-

<%= User.human_attribute_name(:mail_notification) %>

-
-<%= render :partial => 'users/mail_notifications' %> -
- -

<%=l(:label_ui, :app_title => Setting.app_title)%>

-
-<%= render :partial => 'users/impaired_settings' %> -
- -

<%=l(:label_preferences)%>

-
-<%= render :partial => 'users/preferences' %> -
- -
+
+
+

<%=l(:label_information_plural)%>

+
+

<%= f.text_field :firstname, :required => true %>

+

<%= f.text_field :lastname, :required => true %>

+

<%= f.text_field :mail, :required => true %>

+

<%= f.select :language, lang_options_for_select %>

+ <% if Setting.openid? %> +

<%= f.text_field :identity_url %>

+ <% end %> + + <% @user.custom_field_values.select(&:editable?).each do |value| %> +

<%= custom_field_tag_with_label :user, value %>

+ <% end %> + <%= call_hook(:view_my_account, :user => @user, :form => f) %> +
+
+ +
+

<%= User.human_attribute_name(:mail_notification) %>

+
+ <%= render :partial => 'users/mail_notifications' %> +
+ +

<%=l(:label_ui, :app_title => Setting.app_title)%>

+
+ <%= render :partial => 'users/impaired_settings' %> +
+ +

<%=l(:label_preferences)%>

+
+ <%= render :partial => 'users/preferences' %> +
+
+
+
+ + <%= submit_tag l(:button_save) %> <% end %> <% html_title(l(:label_my_account)) -%> diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index 431bbb7de3..c6cc9c5e86 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -46,10 +46,7 @@ See doc/COPYRIGHT.rdoc for more details. <% if (project.new_record? || project.module_enabled?('issue_tracking')) %> <% if renderTypes %> -
<%=l(:label_type_plural)%> (<%= check_all_links 'project_types' %>) <%= render :partial => 'projects/form/types', :locals => { :f => f, :project => project } %> - <%= hidden_field_tag 'project[type_ids][]', '' %> -
<% end %> <% end %> diff --git a/app/views/projects/form/_types.html.erb b/app/views/projects/form/_types.html.erb index 9947abc46c..1c18376702 100644 --- a/app/views/projects/form/_types.html.erb +++ b/app/views/projects/form/_types.html.erb @@ -27,45 +27,64 @@ See doc/COPYRIGHT.rdoc for more details. ++#%> - - - - - - - - - - +<%= javascript_include_tag 'types_checkboxes' %> - - <% Type.all.each do |type| %> - "> - - - - - +<%= render_flash_message :notice, + l(:notice_automatic_set_of_standard_type), + style: "display:none;", id: "types_flash_notice" %> + +
+ <%=l(:label_type_plural)%> + + (<%= link_to(l(:button_check_all), "#", id: "check_all_types") + + ' | ' + + link_to(l(:button_uncheck_all), "#", id: "uncheck_all_types") + %>) + + +
<%= Type.human_attribute_name(:active) %><%= Type.human_attribute_name(:name) %><%= Type.human_attribute_name(:in_aggregation) %><%= Type.human_attribute_name(:is_in_roadmap) %><%= Type.human_attribute_name(:is_milestone) %>
- <%= check_box_tag "project[type_ids][]", - type.id, - project.types.include?(type), - :id => "project_planning_element_type_ids_#{type.id}" %> - - - - - <%= checked_image(type.in_aggregation) %> - - <%= checked_image(type.is_in_roadmap) %> - - <%= checked_image(type.is_milestone) %> -
+ + + + + + + - <% end %> - -
<%= Type.human_attribute_name(:active) %><%= Type.human_attribute_name(:name) %><%= Type.human_attribute_name(:in_aggregation) %><%= Type.human_attribute_name(:is_in_roadmap) %><%= Type.human_attribute_name(:is_milestone) %>
\ No newline at end of file + + + + <% Type.all.each do |type| %> + "> + + <% 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 %> + + + + + + + <%= checked_image(type.in_aggregation) %> + + + <%= checked_image(type.is_in_roadmap) %> + + + <%= checked_image(type.is_milestone) %> + + + <% end %> + + + \ No newline at end of file diff --git a/app/views/projects/settings/_types.html.erb b/app/views/projects/settings/_types.html.erb index 86d0e43506..8be87e18e4 100644 --- a/app/views/projects/settings/_types.html.erb +++ b/app/views/projects/settings/_types.html.erb @@ -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)%> (<%= check_all_links 'project_types' %>) <%= render :partial => 'projects/form/types', :locals => { :f => f, :project => @project } %>

<%= submit_tag l(:button_save) %>

diff --git a/config/routes.rb b/config/routes.rb index d11fa10d54..1b78201a9d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 0bf79248b9..01111783c3 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -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 diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 37cebc945a..09fba3fc1b 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -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) diff --git a/spec/controllers/api/v2/planning_elements_controller_spec.rb b/spec/controllers/api/v2/planning_elements_controller_spec.rb index 482f4c1325..8d7af609ec 100644 --- a/spec/controllers/api/v2/planning_elements_controller_spec.rb +++ b/spec/controllers/api/v2/planning_elements_controller_spec.rb @@ -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) } diff --git a/spec/controllers/api/v2/users_controller_spec.rb b/spec/controllers/api/v2/users_controller_spec.rb index 3b56e69734..33f956d229 100644 --- a/spec/controllers/api/v2/users_controller_spec.rb +++ b/spec/controllers/api/v2/users_controller_spec.rb @@ -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 diff --git a/spec/controllers/api/v2/work_package_priorities_controller_spec.rb b/spec/controllers/api/v2/work_package_priorities_controller_spec.rb new file mode 100644 index 0000000000..986625ea2d --- /dev/null +++ b/spec/controllers/api/v2/work_package_priorities_controller_spec.rb @@ -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 + diff --git a/spec/models/work_package_spec.rb b/spec/models/work_package_spec.rb index b191e271f3..3a35553d9a 100644 --- a/spec/models/work_package_spec.rb +++ b/spec/models/work_package_spec.rb @@ -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 diff --git a/spec/routing/api/v2/work_package_priorities_routing_spec.rb b/spec/routing/api/v2/work_package_priorities_routing_spec.rb new file mode 100644 index 0000000000..ab9d0072af --- /dev/null +++ b/spec/routing/api/v2/work_package_priorities_routing_spec.rb @@ -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 + + diff --git a/spec/views/api/v2/work_package_priorities/index_api_xml_spec.rb b/spec/views/api/v2/work_package_priorities/index_api_xml_spec.rb new file mode 100644 index 0000000000..388fce5745 --- /dev/null +++ b/spec/views/api/v2/work_package_priorities/index_api_xml_spec.rb @@ -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