diff --git a/app/assets/javascripts/custom-fields.js b/app/assets/javascripts/custom-fields.js index 9cb07500c7..5c13acb2f4 100644 --- a/app/assets/javascripts/custom-fields.js +++ b/app/assets/javascripts/custom-fields.js @@ -44,7 +44,8 @@ possibleValues = $('#custom_field_possible_values_attributes'), defaultValueFields = $('#custom_field_default_value_attributes'), spanDefaultText = $('#default_value_text'), - spanDefaultBool = $('#default_value_bool'); + spanDefaultBool = $('#default_value_bool'), + textOrientationField = $('#custom_field_text_orientation'); var deactivate = function(element) { element.hide().find('input, textarea').not('.destroy_flag,.-cf-ignore-disabled').attr('disabled', true); @@ -65,7 +66,7 @@ unsearchable = function() { searchable.attr('checked', false).hide(); }; // defaults (reset these fields before doing anything else) - $.each([spanDefaultBool, spanDefaultText, multiSelect], function(idx, element) { + $.each([spanDefaultBool, spanDefaultText, multiSelect, textOrientationField], function(idx, element) { deactivate(element); }); show(defaultValueFields); @@ -107,6 +108,11 @@ hide(lengthField, regexpField, defaultValueFields); unsearchable(); break; + case 'text': + show(lengthField, regexpField, searchable, textOrientationField); + deactivate(possibleValues); + activate(textOrientationField); + break; default: show(lengthField, regexpField, searchable); deactivate(possibleValues); diff --git a/app/assets/stylesheets/openproject/_generic.sass b/app/assets/stylesheets/openproject/_generic.sass index 5582932d30..6ff3713a65 100644 --- a/app/assets/stylesheets/openproject/_generic.sass +++ b/app/assets/stylesheets/openproject/_generic.sass @@ -86,6 +86,10 @@ font-style: italic .-small-font font-size: 12px +.-rtl + direction: rtl + .-placeholder & + direction: ltr .drop-zone.-dragged-over background-color: #eaeaea60 diff --git a/app/contracts/work_packages/base_contract.rb b/app/contracts/work_packages/base_contract.rb index 7b1c5fc9df..75016b0958 100644 --- a/app/contracts/work_packages/base_contract.rb +++ b/app/contracts/work_packages/base_contract.rb @@ -103,7 +103,9 @@ module WorkPackages message: :greater_than_or_equal_to_start_date, allow_blank: true }, unless: Proc.new { |wp| wp.start_date.blank? } + validate :validate_enabled_type + validate :validate_type_exists validate :validate_milestone_constraint validate :validate_parent_not_milestone @@ -112,13 +114,17 @@ module WorkPackages validate :validate_parent_in_same_project validate :validate_parent_not_subtask + validate :validate_status_exists validate :validate_status_transition validate :validate_active_priority + validate :validate_priority_exists validate :validate_category validate :validate_estimated_hours + validate :validate_assigned_to_exists + def initialize(work_package, user, options: {}) super @@ -149,11 +155,19 @@ module WorkPackages def validate_enabled_type # Checks that the issue can not be added/moved to a disabled type - if model.project && (model.type_id_changed? || model.project_id_changed?) + if type_context_changed? errors.add :type_id, :inclusion unless model.project.types.include?(model.type) end end + def validate_assigned_to_exists + errors.add :assigned_to, :does_not_exist if model.assigned_to&.is_a?(User::InexistentUser) + end + + def validate_type_exists + errors.add :type, :does_not_exist if type_inexistent? + end + def validate_milestone_constraint if model.is_milestone? && model.due_date && model.start_date && model.start_date != model.due_date errors.add :due_date, :not_start_date @@ -186,6 +200,10 @@ module WorkPackages end end + def validate_status_exists + errors.add :status, :does_not_exist if model.status&.is_a?(Status::InexistentStatus) + end + def validate_status_transition if status_changed? && status_exists? && !(model.type_id_changed? || status_transition_exists?) errors.add :status_id, :status_transition_invalid @@ -198,6 +216,10 @@ module WorkPackages end end + def validate_priority_exists + errors.add :priority, :does_not_exist if model.priority&.is_a?(Priority::InexistentPriority) + end + def validate_category if inexistent_category? errors.add :category, :does_not_exist @@ -287,5 +309,13 @@ module WorkPackages query end end + + def type_context_changed? + model.project && !type_inexistent? && (model.type_id_changed? || model.project_id_changed?) + end + + def type_inexistent? + model.type.is_a?(Type::InexistentType) + end end end diff --git a/app/contracts/work_packages/create_contract.rb b/app/contracts/work_packages/create_contract.rb index 2ac6bfa863..f191c54769 100644 --- a/app/contracts/work_packages/create_contract.rb +++ b/app/contracts/work_packages/create_contract.rb @@ -32,8 +32,6 @@ require 'work_packages/base_contract' module WorkPackages class CreateContract < BaseContract - # TODO: Think about whether this can be removed - # as it is unwriteable. So why bother checking for the correct author attribute :author_id, writeable: false do errors.add :author_id, :invalid if model.author != user diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index d32e6abb51..b25500f4dd 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -480,6 +480,7 @@ class PermittedParams :default_value, :possible_values, :multi_value, + :content_right_to_left, { custom_options_attributes: %i(id value default_value position) }, type_ids: [] ], diff --git a/app/models/priority/inexistent_priority.rb b/app/models/priority/inexistent_priority.rb new file mode 100644 index 0000000000..12a721167a --- /dev/null +++ b/app/models/priority/inexistent_priority.rb @@ -0,0 +1,31 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class Priority::InexistentPriority < IssuePriority; end diff --git a/app/models/status/inexistent_status.rb b/app/models/status/inexistent_status.rb new file mode 100644 index 0000000000..a7cb10fa7f --- /dev/null +++ b/app/models/status/inexistent_status.rb @@ -0,0 +1,31 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class Status::InexistentStatus < Status; end diff --git a/app/models/type/inexistent_type.rb b/app/models/type/inexistent_type.rb new file mode 100644 index 0000000000..2a0af5c851 --- /dev/null +++ b/app/models/type/inexistent_type.rb @@ -0,0 +1,32 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class Type::InexistentType < Type +end diff --git a/app/models/user/inexistent_user.rb b/app/models/user/inexistent_user.rb new file mode 100644 index 0000000000..6fd662b8b5 --- /dev/null +++ b/app/models/user/inexistent_user.rb @@ -0,0 +1,31 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +class User::InexistentUser < User; end diff --git a/app/services/work_packages/create_service.rb b/app/services/work_packages/create_service.rb index c8e35756bc..ca4b695121 100644 --- a/app/services/work_packages/create_service.rb +++ b/app/services/work_packages/create_service.rb @@ -74,7 +74,7 @@ class WorkPackages::CreateService end def set_attributes(attributes, wp) - WorkPackages::SetAttributesService + attributes_service_class .new(user: user, model: wp, contract_class: contract_class) @@ -96,4 +96,8 @@ class WorkPackages::CreateService result end + + def attributes_service_class + ::WorkPackages::SetAttributesService + end end diff --git a/app/services/work_packages/set_attributes_service.rb b/app/services/work_packages/set_attributes_service.rb index 2b9990cfa1..6a874320e7 100644 --- a/app/services/work_packages/set_attributes_service.rb +++ b/app/services/work_packages/set_attributes_service.rb @@ -177,7 +177,7 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes end def reassign_status(available_statuses) - return if available_statuses.include? work_package.status + return if available_statuses.include?(work_package.status) || work_package.status.is_a?(Status::InexistentStatus) new_status = available_statuses.detect(&:is_default) || available_statuses.first work_package.status = new_status if new_status.present? diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb index fa2bf08d37..18f59d2afa 100644 --- a/app/views/custom_fields/_form.html.erb +++ b/app/views/custom_fields/_form.html.erb @@ -107,6 +107,7 @@ See docs/COPYRIGHT.rdoc for more details.
<%= f.check_box :is_for_all %>
<%= f.check_box :is_filter %>
<%= f.check_box :searchable %>
+
<%= f.check_box :content_right_to_left %>
<% when "UserCustomField" %>
<%= f.check_box :is_required %>
<%= f.check_box :visible %>
diff --git a/config/locales/crowdin/cs.yml b/config/locales/crowdin/cs.yml index 37dcccbc17..129411c09d 100644 --- a/config/locales/crowdin/cs.yml +++ b/config/locales/crowdin/cs.yml @@ -30,33 +30,33 @@ cs: custom_styles: custom_colors: "Vlastní barvy" customize: "Přizpůsobte si svou OpenProject instalaci vlastním logem. Poznámka: Logo bude veřejně přístupné." - enterprise_notice: "As a special 'Thank you!' for their financial contribution to develop OpenProject, this tiny feature is only available for Enterprise Edition support subscribers." - manage_colors: "Edit color select options" + enterprise_notice: "Jako zvláštní 'Děkujeme!' za jejich finanční příspěvky na vývoj OpenProjektu, je tato malá funkce dostupná pouze pro podporu Enterprise Edition." + manage_colors: "Upravit možnosti výběru barvy" instructions: - alternative_color: "Strong accent color, typically used for most the important button on a screen." + alternative_color: "Silný odstín, obvykle používaný pro nejdůležitější tlačítko na obrazovce." content_link_color: "Barva písma většiny odkazů." primary_color: "Hlavní barva." - primary_color_dark: "Typically a darker version of the main color used for hover effects." + primary_color_dark: "Obvykle tmavší verze hlavní barvy použité pro efekt vznášení." header_bg_color: "Barva pozadí záhlaví." header_item_bg_hover_color: "Barva pozadí klikacích položek záhlaví při najetí myší." header_item_font_color: "Barva pozadí klikacích položek záhlaví." - header_item_font_hover_color: "Font color of clickable header items when hovered with the mouse." - header_border_bottom_color: "Thin line under the header. Leave this field empty if you don't want any line." - main_menu_bg_color: "Left side menu's background color." + header_item_font_hover_color: "Barva pozadí klikacích položek záhlaví při najetí myší." + header_border_bottom_color: "Tenký řádek pod záhlavím. Ponechte toto pole prázdné, pokud nechcete žádný řádek." + main_menu_bg_color: "Barva pozadí levého menu." enterprise: - upgrade_to_ee: "Upgrade to Enterprise Edition" - add_token: "Upload an Enterprise Edition support token" - replace_token: "Replace your current support token" - order: "Order Enterprise Edition" - paste: "Paste your Enterprise Edition support token" - required_for_feature: "This feature is only available with an active Enterprise Edition support token." - enterprise_link: "For more information, click here." + upgrade_to_ee: "Upgradovat na Enterprise Edition" + add_token: "Nahrát podpůrný token Enterprise Edition" + replace_token: "Nahradit aktuální podpůrný token" + order: "Objednávka Enterprise edice" + paste: "Vložte svůj podpůrný token Enterprise Edition" + required_for_feature: "Tato funkce je dostupná pouze s aktivním podpůrným tokenem pro Enterprise Edition." + enterprise_link: "Pro více informací klikněte zde." announcements: show_until: Ukázat až do is_active: aktuálně zobrazené is_inactive: v současné době nejsou zobrazeny attribute_help_texts: - text_overview: 'In this view, you can create custom help texts for attributes view. When defined, these texts can be shown by clicking the help icon next to its belonging attribute.' + text_overview: 'V tomto zobrazení můžete vytvořit vlastní nápovědné texty pro zobrazení atributů. Pokud je definováno, mohou být tyto texty zobrazeny klepnutím na ikonu nápovědy vedle jejího atributu.' label_plural: 'Texty nápovědy atributu' show_preview: 'Náhled textu' add_new: 'Přidat text nápovědy' @@ -66,16 +66,16 @@ cs: no_results_content_title: Nyní neexistují žádné ověřovací režimy. no_results_content_text: Vytvořit nový režim ověřování ldap_auth_sources: - connection_encryption: 'Connection encryption' + connection_encryption: 'Šifrování připojení' tls_mode: plain: 'žádný' - simple_tls: 'simple_tls' - start_tls: 'start_tls' - plain_description: "Plain unencrypted connection, no TLS negotiation." - simple_tls_description: "Implicit TLS encryption, but no certificate validation. Use with caution and implicit trust of the LDAP connection." - start_tls_description: "Explicit TLS encryption with full validation. Use for LDAP over TLS/SSL." + simple_tls: 'jednoduché_tls' + start_tls: 'začátek_tls' + plain_description: "Prosté nešifrované připojení, žádné jednání TLS." + simple_tls_description: "Implicitní TLS šifrování, ale bez ověření certifikátu. Používejte s opatrností a implicitní důvěru k LDAP připojení." + start_tls_description: "Explicitní TLS šifrování s úplnou validací. Použijte pro LDAP nad TLS/SSL." section_more_info_link_html: > - This section concerns the connection security of this LDAP authentication source. For more information, visit the Net::LDAP documentation. + Tato sekce se týká zabezpečení připojení tohoto LDAP autentifikačního zdroje. Pro více informací navštivte síť:LDAP dokumentaci. forums: show: no_results_title_text: There are currently no posts for the forum. @@ -89,7 +89,7 @@ cs: name: 'Akce' add: 'Přidat akci' assigned_to: - executing_user_value: '(Assign to executing user)' + executing_user_value: '(Přiřaďte k vykonání uživatele)' conditions: 'Podmínky' plural: 'Vlastní akce' new: 'Nová vlastní akce' @@ -100,11 +100,11 @@ cs: description: 'Vlastní akce zjednodušuje každodenní práci tím, že kombinuje několik kroků do jednoho tlačítka.' custom_fields: text_add_new_custom_field: > - To add new custom fields to a project you first need to create them before you can add them to this project. + Chcete-li přidat nová vlastní pole do projektu, musíte je nejprve vytvořit, než je budete moci přidat do tohoto projektu. is_enabled_globally: 'Globálně povoleno' enabled_in_project: 'Povoleno v projektu' - contained_in_type: 'Contained in type' - confirm_destroy_option: "Deleting an option will delete all of its occurrences (e.g. in work packages). Are you sure you want to delete it?" + contained_in_type: 'Obsahuje typ' + confirm_destroy_option: "Smazáním možnosti smažete všechny výskyty (např. v pracovních balíčcích). Opravdu ji chcete odstranit?" tab: no_results_title_text: V současné době nejsou žádná vlastní pole. no_results_content_text: Vytvořit nové vlastní pole @@ -1115,14 +1115,14 @@ cs: community: "OpenProject komunita" upsale: become_hero: "Staň se hrdinou!" - title: "Upgrade to Enterprise Edition" + title: "Upgradovat na Enterprise Edition" description: "What are the benefits?" more_info: "Více informací" additional_features: "Additional powerful premium features" professional_support: "Professional support from the OpenProject experts" you_contribute: "Developers need to pay their bills, too. With Enterprise Edition you substantially contribute to this Open-Source community effort." links: - upgrade_enterprise_edition: "Upgrade to Enterprise Edition" + upgrade_enterprise_edition: "Upgradovat na Enterprise Edition" postgres_migration: "Migrating your installation to PostgreSQL" user_guides: "Uživatelské příručky" faq: "FAQ - často kladené dotazy" diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index 5f21d1df43..f97e781765 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -445,8 +445,8 @@ fr: confirmation: "ne correspond pas à %{attribute}." could_not_be_copied: "n'a pas pu être copié (entièrement)." does_not_exist: "n'existe pas." - error_unauthorized: "may not be accessed." - error_readonly: "is not writable." + error_unauthorized: "est interdit d'accès." + error_readonly: "est en lecture seule." empty: "ne peut pas être vide." even: "doit être pair." exclusion: "est réservé." diff --git a/config/locales/crowdin/js-cs.yml b/config/locales/crowdin/js-cs.yml index 927f7e59aa..ce2912b308 100644 --- a/config/locales/crowdin/js-cs.yml +++ b/config/locales/crowdin/js-cs.yml @@ -43,7 +43,7 @@ cs: button_close: "Zavřít" button_change_project: "Change project" button_check_all: "Zaškrtnout vše" - button_configure-form: "Configure form" + button_configure-form: "Nastavení formuláře" button_confirm: "Potvrdit" button_continue: "Pokračovat" button_copy: "Kopírovat" @@ -54,13 +54,13 @@ cs: button_duplicate: "Kopie" button_edit: "Upravit" button_filter: "Filtr" - button_advanced_filter: "Advanced filter" + button_advanced_filter: "Pokročilý filtr" button_list_view: "Zobrazit jako seznam" button_show_view: "Celá obrazovka" button_log_time: "Čas protokolu" button_more: "Více" - button_open_details: "Open details view" - button_close_details: "Close details view" + button_open_details: "Otevřít zobrazení podrobností" + button_close_details: "Zavřít zobrazení podrobností" button_open_fullscreen: "Otevřít celoobrazovkový pohled" button_show_cards: "Show card view" button_show_list: "Show list view" @@ -106,16 +106,16 @@ cs: description_selected_columns: "Vybrané sloupce" description_subwork_package: "Potomek pracovního balíčku #%{id}" editor: - preview: 'Toggle preview mode' - source_code: 'Toggle Markdown source mode' - error_saving_failed: 'Saving the document failed with the following error: %{error}' + preview: 'Přepnout režim náhledu' + source_code: 'Přepnout zdrojový mód Markdown' + error_saving_failed: 'Uložení dokumentu se nezdařilo s následující chybou: %{error}' error_initialization_failed: 'Nepodařilo se inicializovat CKEditor!' mode: manual: 'Přepněte do Markdown zdroje' - wysiwyg: 'Switch to WYSIWYG editor' + wysiwyg: 'Přepnout na WYSIWYG Editor' macro: child_pages: - button: 'Links to child pages' + button: 'Odkazy na podřízené stránky' include_parent: 'Include parent' text: '[Placeholder] Links to child pages of' page: 'Stránka wiki' @@ -419,7 +419,7 @@ cs: reset_title: "Reset form configuration" confirm_reset: > Warning: Are you sure you want to reset the form configuration? This will reset the attributes to their default group and disable ALL custom fields. - upgrade_to_ee: "Upgrade to Enterprise Edition" + upgrade_to_ee: "Upgradovat na Enterprise Edition" upgrade_to_ee_text: "Wow! If you need this feature you are a super pro! Would you mind supporting us OpenSource developers by becoming an Enterprise Edition client?" more_information: "Více informací" nevermind: "Nevermind" diff --git a/config/locales/crowdin/tr.yml b/config/locales/crowdin/tr.yml index 1bbee319a5..c6816e26cc 100644 --- a/config/locales/crowdin/tr.yml +++ b/config/locales/crowdin/tr.yml @@ -2576,7 +2576,7 @@ tr: redirect_uri_html: > İzin verilen URL'ler yetkili kullanıcılar adresine yönlendirilebilir. Her satıra bir giriş.
Bir masaüstü uygulamasına kaydoluyorsanız, aşağıdaki URL'yi kullanın. confidential: "Uygulamanın, müşteri sırrının gizli tutulabileceği yerlerde kullanılıp kullanılmayacağını kontrol edin. Yerel mobil uygulamalar ve Tek Sayfa Uygulamaları gizli değildir." - scopes: "Check the scopes you want the application to grant access to. If no scope is checked, api_v3 is assumed." + scopes: "Uygulamanın erişmesine izin vermek istediğiniz kapsamları kontrol edin. Kapsam işaretli değilse, api_v3 olduğu kabul edilir." client_credential_user_id: "İstemciler bu uygulamayı kullandıklarında kimliğe bürünmek için isteğe bağlı kullanıcı kimliği. Yalnızca genel erişime izin vermek için boş bırakın" register_intro: "OpenProject için bir OAuth API istemci uygulaması geliştiriyorsanız, tüm kullanıcıların kullanması için bu formu kullanarak kayıt olabilirsiniz." default_scopes: "" @@ -2591,8 +2591,8 @@ tr: wants_to_access_html: > Bu uygulama OpenProject hesabınıza erişim istiyor.
Aşağıdaki izinleri istedi: scopes: - api_v3: "Full API v3 access" - api_v3_text: "Application will receive full read & write access to the OpenProject API v3 to perform actions on your behalf." + api_v3: "Tam API v3 erişimi" + api_v3_text: "Uygulama, sizin adınıza işlem yapmak için OpenProject API v3'e tam okuma ve yazma erişimi alacaktır." grants: created_date: "Tarihinde onaylandı" scopes: "İzinler" diff --git a/config/locales/en.yml b/config/locales/en.yml index 4ea70c7863..12d6a8a1ef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -497,7 +497,7 @@ en: could_not_be_copied: "could not be (fully) copied." does_not_exist: "does not exist." error_unauthorized: "may not be accessed." - error_readonly: "is not writable." + error_readonly: "was attempted to be written but is not writable." empty: "can't be empty." even: "must be even." exclusion: "is reserved." @@ -2652,7 +2652,6 @@ en: estimated_hours: "Estimated hours cannot be set on parent work packages." invalid_user_assigned_to_work_package: "The chosen user is not allowed to be '%{property}' for this work package." start_date: "Start date cannot be set on parent work packages." - writing_read_only_attributes: "You must not write a read-only attribute." resources: schema: 'Schema' diff --git a/db/migrate/20191115141154_add_content_orientation_to_custom_fields.rb b/db/migrate/20191115141154_add_content_orientation_to_custom_fields.rb new file mode 100644 index 0000000000..7fbfb7b5fd --- /dev/null +++ b/db/migrate/20191115141154_add_content_orientation_to_custom_fields.rb @@ -0,0 +1,5 @@ +class AddContentOrientationToCustomFields < ActiveRecord::Migration[6.0] + def change + add_column :custom_fields, :content_right_to_left, :boolean, default: false + end +end diff --git a/frontend/src/app/components/wp-card-view/event-handler/click-handler.ts b/frontend/src/app/components/wp-card-view/event-handler/click-handler.ts index c7825ddf14..c5e1a5cfc5 100644 --- a/frontend/src/app/components/wp-card-view/event-handler/click-handler.ts +++ b/frontend/src/app/components/wp-card-view/event-handler/click-handler.ts @@ -3,12 +3,15 @@ import {CardEventHandler} from "core-components/wp-card-view/event-handler/card- import {WorkPackageCardViewComponent} from "core-components/wp-card-view/wp-card-view.component"; import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service"; import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service"; - import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service"; +import {StateService} from "@uirouter/core"; +import {DeviceService} from "core-app/modules/common/browser/device.service"; export class CardClickHandler implements CardEventHandler { // Injections + public deviceService:DeviceService = this.injector.get(DeviceService); + public $state:StateService = this.injector.get(StateService); public wpTableSelection:WorkPackageViewSelectionService = this.injector.get(WorkPackageViewSelectionService); public wpTableFocus:WorkPackageViewFocusService = this.injector.get(WorkPackageViewFocusService); public wpCardView:WorkPackageCardViewService = this.injector.get(WorkPackageCardViewService); @@ -67,7 +70,15 @@ export class CardClickHandler implements CardEventHandler { // not matter what other card are (de-)selected below. // Thus save that card for the details view button. this.wpTableFocus.updateFocus(wpId); + + // open work package on mobile after first click + if (this.deviceService.isMobile) { + this.$state.go( + 'work-packages.show', + {workPackageId: wpId} + ); + } + return false; } } - diff --git a/frontend/src/app/components/wp-card-view/wp-card-view.component.ts b/frontend/src/app/components/wp-card-view/wp-card-view.component.ts index a49233eaa3..f5ab59471b 100644 --- a/frontend/src/app/components/wp-card-view/wp-card-view.component.ts +++ b/frontend/src/app/components/wp-card-view/wp-card-view.component.ts @@ -33,6 +33,7 @@ import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handle import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service"; import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service"; import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; +import {DeviceService} from "core-app/modules/common/browser/device.service"; export type CardViewOrientation = 'horizontal'|'vertical'; @@ -100,7 +101,8 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit { readonly wpTableSelection:WorkPackageViewSelectionService, readonly wpViewOrder:WorkPackageViewOrderService, readonly cardView:WorkPackageCardViewService, - readonly cardDragDrop:WorkPackageCardDragAndDropService) { + readonly cardDragDrop:WorkPackageCardDragAndDropService, + readonly deviceService:DeviceService) { } ngOnInit() { @@ -140,9 +142,12 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - // Register Drag & Drop this.cardDragDrop.init(this); - this.cardDragDrop.registerDragAndDrop(); + + // Register Drag & Drop only on desktop + if (!this.deviceService.isMobile) { + this.cardDragDrop.registerDragAndDrop(); + } // Register event handlers for the cards new CardViewHandlerRegistry(this.injector).attachTo(this); diff --git a/frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.html b/frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.html index a68dc90346..202ffe10cb 100644 --- a/frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.html +++ b/frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.html @@ -5,7 +5,7 @@ [ngClass]="cardHighlightingClass(workPackage)"> -
+
{ } const field = this.getField(resource, fieldSchema, schemaName, change); - field.render(span, this.getText(field, fieldSchema, placeholder)); + field.render(span, this.getText(field, fieldSchema, placeholder), fieldSchema.options); const title = field.title; if (title) { diff --git a/frontend/src/app/modules/common/ckeditor/ckeditor-setup.service.ts b/frontend/src/app/modules/common/ckeditor/ckeditor-setup.service.ts index 2ba81d0869..fcd8b96f9d 100644 --- a/frontend/src/app/modules/common/ckeditor/ckeditor-setup.service.ts +++ b/frontend/src/app/modules/common/ckeditor/ckeditor-setup.service.ts @@ -28,6 +28,10 @@ export interface ICKEditorContext { removePlugins?:string[]; // Set of enabled macro plugins or false to disable all macros?:'none'|'wp'|'full'|boolean|string[]; + // Additional options like the text orientation of the editors content + options?:{ + rtl?:boolean; + }; // context link to append on preview requests previewContext?:string; } @@ -61,10 +65,15 @@ export class CKEditorSetupService { const toolbarWrapper = wrapper.querySelector('.document-editor__toolbar') as HTMLElement; const contentWrapper = wrapper.querySelector('.document-editor__editable') as HTMLElement; + var contentLanguage = context.options && context.options.rtl ? 'ar' : 'en'; + return editor .createCustomized(contentWrapper, { openProject: this.createConfig(context), - initialData: initialData + initialData: initialData, + language: { + content: contentLanguage + } }) .then((editor) => { // Add decoupled toolbar diff --git a/frontend/src/app/modules/fields/display/display-field.module.ts b/frontend/src/app/modules/fields/display/display-field.module.ts index b88458706c..89f0ccee53 100644 --- a/frontend/src/app/modules/fields/display/display-field.module.ts +++ b/frontend/src/app/modules/fields/display/display-field.module.ts @@ -118,7 +118,7 @@ export class DisplayField extends Field { return this.valueString; } - public render(element:HTMLElement, displayText:string):void { + public render(element:HTMLElement, displayText:string, options:any = {}):void { element.textContent = displayText; } diff --git a/frontend/src/app/modules/fields/display/field-types/formattable-display-field.module.ts b/frontend/src/app/modules/fields/display/field-types/formattable-display-field.module.ts index 6eb1a2d9bb..d189383be5 100644 --- a/frontend/src/app/modules/fields/display/field-types/formattable-display-field.module.ts +++ b/frontend/src/app/modules/fields/display/field-types/formattable-display-field.module.ts @@ -35,9 +35,14 @@ export class FormattableDisplayField extends DisplayField { private readonly appRef = this.$injector.get(ApplicationRef); - public render(element:HTMLElement, displayText:string):void { + public render(element:HTMLElement, displayText:string, options:any = {}):void { let div = document.createElement('div'); + div.classList.add('read-value--html', 'wiki', 'highlight', '-multiline'); + if (options.rtl) { + div.classList.add('-rtl'); + } + div.innerHTML = displayText; element.innerHTML = ''; diff --git a/frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts index ccc9dbac67..acfea0ec8b 100644 --- a/frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts @@ -116,7 +116,8 @@ export class FormattableEditFieldComponent extends EditFieldComponent implements return { resource: this.resource, macros: 'none' as 'none', - previewContext: this.previewContext + previewContext: this.previewContext, + options: { rtl: this.schema.options && this.schema.options.rtl } }; } diff --git a/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts index fcb55c94b2..e1761f23fc 100644 --- a/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/multi-select-edit-field.component.ts @@ -42,7 +42,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements @ViewChild(NgSelectComponent, { static: true }) public ngSelectComponent:NgSelectComponent; readonly I18n:I18nService = this.injector.get(I18nService); - public options:any[] = []; + public availableOptions:any[] = []; public valueOptions:ValueOption[]; public text = { requiredPlaceholder: this.I18n.t('js.placeholders.selection'), @@ -70,7 +70,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements untilComponentDestroyed(this), ) .subscribe(() => { - this.requestFocus = this.options.length === 0; + this.requestFocus = this.availableOptions.length === 0; // If we already have all values loaded, open now. if (!this.requestFocus) { @@ -108,7 +108,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements public set selectedOption(val:ValueOption[]) { this._selectedOption = val; let mapper = (val:ValueOption) => { - let option = _.find(this.options, o => o.$href === val.$href) || this.nullOption; + let option = _.find(this.availableOptions, o => o.$href === val.$href) || this.nullOption; // Special case 'null' value, which angular // only understands in ng-options as an empty string. @@ -166,14 +166,14 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements }); } - this.options = availableValues || []; - this.valueOptions = this.options.map(el => { + this.availableOptions = availableValues || []; + this.valueOptions = this.availableOptions.map(el => { return { name: el.name, $href: el.$href }; }); this._selectedOption = this.buildSelectedOption(); this.checkCurrentValueValidity(); - if (this.options.length > 0 && this.requestFocus) { + if (this.availableOptions.length > 0 && this.requestFocus) { this.openAutocompleteSelectField(); this.requestFocus = false; } @@ -209,7 +209,7 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements // (If value AND) // MultiSelect AND there is no value which href is not in the options hrefs (!_.some(this.value, (value:HalResource) => { - return _.some(this.options, (option) => (option.$href === value.$href)) + return _.some(this.availableOptions, (option) => (option.$href === value.$href)) })) ); } diff --git a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts index 4fa19648f9..54d27824b7 100644 --- a/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/select-edit-field.component.ts @@ -48,7 +48,7 @@ export interface ValueOption { export class SelectEditFieldComponent extends EditFieldComponent implements OnInit { public selectAutocompleterRegister = this.injector.get(SelectAutocompleterRegisterService); - public options:any[]; + public availableOptions:any[]; public valueOptions:ValueOption[]; public text:{ requiredPlaceholder:string, placeholder:string }; @@ -104,7 +104,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn } public set selectedOption(val:ValueOption) { - let option = _.find(this.options, o => o.$href === val.$href); + let option = _.find(this.availableOptions, o => o.$href === val.$href); // Special case 'null' value, which angular // only understands in ng-options as an empty string. @@ -116,9 +116,9 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn } private setValues(availableValues:HalResource[]) { - this.options = this.halSorting.sort(availableValues); + this.availableOptions = this.halSorting.sort(availableValues); this.addEmptyOption(); - this.valueOptions = this.options.map(el => { + this.valueOptions = this.availableOptions.map(el => { return {name: el.name, $href: el.$href}; }); } @@ -138,13 +138,13 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn } private addValue(val:HalResource) { - this.options.push(val); + this.availableOptions.push(val); this.valueOptions.push({name: val.name, $href: val.$href}); } public get currentValueInvalid():boolean { return !!( - (this.value && !_.some(this.options, (option:HalResource) => (option.$href === this.value.$href))) + (this.value && !_.some(this.availableOptions, (option:HalResource) => (option.$href === this.value.$href))) || (!this.value && this.schema.required) ); @@ -191,7 +191,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn // the option if one is returned / exists already. const emptyOption = this.getEmptyOption(); if (emptyOption === undefined) { - this.options.unshift({ + this.availableOptions.unshift({ name: this.text.placeholder, $href: '' }); @@ -199,6 +199,6 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn } private getEmptyOption():ValueOption|undefined { - return _.find(this.options, el => el.name === this.text.placeholder); + return _.find(this.availableOptions, el => el.name === this.text.placeholder); } } diff --git a/frontend/src/app/modules/fields/field.base.ts b/frontend/src/app/modules/fields/field.base.ts index cb5e7b1743..a2b796ea54 100644 --- a/frontend/src/app/modules/fields/field.base.ts +++ b/frontend/src/app/modules/fields/field.base.ts @@ -36,6 +36,7 @@ export interface IFieldSchema { required?:boolean; hasDefault:boolean; name?:string; + options?:any; } export class Field { @@ -69,6 +70,10 @@ export class Field { return this.schema.hasDefault; } + public get options():boolean { + return this.schema.options; + } + public isEmpty():boolean { return !this.value; } diff --git a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts index a231e6946a..415e25071d 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts @@ -123,7 +123,7 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O this.currentQuery = query; // Update the visible representation - if (this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation) { + if (this.deviceService.isMobile || this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation) { this.showListView = false; } else { this.showListView = true; diff --git a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts index c03ac6d8f8..cce0d18c1e 100644 --- a/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts +++ b/frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts @@ -55,6 +55,7 @@ import {WorkPackageViewDisplayRepresentationService} from "core-app/modules/work import {HalEvent, HalEventsService} from "core-app/modules/hal/services/hal-events.service"; import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; +import {DeviceService} from "core-app/modules/common/browser/device.service"; export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { @@ -83,6 +84,7 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy { readonly cdRef:ChangeDetectorRef = this.injector.get(ChangeDetectorRef); readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService = this.injector.get(WorkPackageViewDisplayRepresentationService); readonly halEvents:HalEventsService = this.injector.get(HalEventsService); + readonly deviceService:DeviceService = this.injector.get(DeviceService); constructor(protected injector:Injector) { diff --git a/lib/api/decorators/allowed_values_by_collection_representer.rb b/lib/api/decorators/allowed_values_by_collection_representer.rb index f9de49f1ae..256749e776 100644 --- a/lib/api/decorators/allowed_values_by_collection_representer.rb +++ b/lib/api/decorators/allowed_values_by_collection_representer.rb @@ -46,7 +46,6 @@ module API required: true, has_default: false, writable: true, - visibility: nil, attribute_group: nil, current_user: nil, allowed_values_getter: nil) @@ -59,7 +58,6 @@ module API required: required, has_default: has_default, writable: writable, - visibility: visibility, attribute_group: attribute_group, current_user: current_user) end diff --git a/lib/api/decorators/property_schema_representer.rb b/lib/api/decorators/property_schema_representer.rb index 388f2235e4..2cd8d9ed22 100644 --- a/lib/api/decorators/property_schema_representer.rb +++ b/lib/api/decorators/property_schema_representer.rb @@ -35,18 +35,13 @@ module API class PropertySchemaRepresenter < ::API::Decorators::Single def initialize( type:, name:, required: true, has_default: false, writable: true, - visibility: nil, attribute_group: nil, current_user: nil + attribute_group: nil, current_user: nil ) @type = type @name = name @required = required @has_default = has_default @writable = writable - @visibility = if visibility == false - nil - else - visibility || 'default' - end @attribute_group = attribute_group super(nil, current_user: current_user) @@ -57,22 +52,22 @@ module API :required, :has_default, :writable, - :visibility, :attribute_group, :min_length, :max_length, - :regular_expression + :regular_expression, + :options property :type, exec_context: :decorator property :name, exec_context: :decorator property :required, exec_context: :decorator property :has_default, exec_context: :decorator property :writable, exec_context: :decorator - property :visibility, exec_context: :decorator property :attribute_group, exec_context: :decorator property :min_length, exec_context: :decorator property :max_length, exec_context: :decorator property :regular_expression, exec_context: :decorator + property :options, exec_context: :decorator private diff --git a/lib/api/decorators/schema_representer.rb b/lib/api/decorators/schema_representer.rb index 1b95e1a4ae..206eef1626 100644 --- a/lib/api/decorators/schema_representer.rb +++ b/lib/api/decorators/schema_representer.rb @@ -58,11 +58,11 @@ module API required: true, has_default: false, writable: default_writable_property(property), - visibility: nil, attribute_group: nil, min_length: nil, max_length: nil, regular_expression: nil, + options: {}, show_if: true) getter = ->(*) do schema_property_getter(type, @@ -70,11 +70,11 @@ module API required, has_default, writable, - visibility, attribute_group, min_length, max_length, - regular_expression) + regular_expression, + options) end schema_property(property, @@ -94,7 +94,6 @@ module API required: true, has_default: false, writable: default_writable_property(property), - visibility: nil, attribute_group: nil, show_if: true) getter = ->(*) do @@ -103,7 +102,6 @@ module API required, has_default, writable, - visibility, attribute_group, href_callback) end @@ -129,7 +127,6 @@ module API required: true, has_default: false, writable: default_writable_property(property), - visibility: nil, attribute_group: nil, show_if: true) @@ -142,7 +139,6 @@ module API required, has_default, writable, - visibility, attribute_group, values_callback, nil) @@ -167,7 +163,6 @@ module API required: true, has_default: false, writable: default_writable_property(property), - visibility: nil, attribute_group: nil, show_if: true) getter = ->(*) do @@ -179,7 +174,6 @@ module API required, has_default, writable, - visibility, attribute_group, values_callback, ->(*) { @@ -282,11 +276,11 @@ module API required, has_default, writable, - visibility, attribute_group, min_length, max_length, - regular_expression) + regular_expression, + options) name = call_or_translate(name_source) schema = ::API::Decorators::PropertySchemaRepresenter .new(type: call_or_use(type), @@ -294,11 +288,11 @@ module API required: call_or_use(required), has_default: call_or_use(has_default), writable: call_or_use(writable), - visibility: call_or_use(visibility), attribute_group: call_or_use(attribute_group)) schema.min_length = min_length schema.max_length = max_length schema.regular_expression = regular_expression + schema.options = options schema end @@ -308,7 +302,6 @@ module API required, has_default, writable, - visibility, attribute_group, href_callback) representer = ::API::Decorators::AllowedValuesByLinkRepresenter @@ -317,7 +310,6 @@ module API required: call_or_use(required), has_default: call_or_use(has_default), writable: call_or_use(writable), - visibility: call_or_use(visibility), attribute_group: call_or_use(attribute_group)) if form_embedded @@ -335,7 +327,6 @@ module API required, has_default, writable, - visibility, attribute_group, values_callback, allowed_values_getter) @@ -354,7 +345,6 @@ module API required: call_or_use(required), has_default: call_or_use(has_default), writable: call_or_use(writable), - visibility: call_or_use(visibility), attribute_group: call_or_use(attribute_group) } attributes[:allowed_values_getter] = allowed_values_getter if allowed_values_getter diff --git a/lib/api/errors/error_base.rb b/lib/api/errors/error_base.rb index 404cc2e6a1..5827ec5946 100644 --- a/lib/api/errors/error_base.rb +++ b/lib/api/errors/error_base.rb @@ -89,7 +89,7 @@ module API api_attribute_name = ::API::Utilities::PropertyNameConverter.from_ar_name(attribute) errors.symbols_and_messages_for(attribute).each do |symbol, full_message, _| api_errors << if symbol == :error_readonly - ::API::Errors::UnwritableProperty.new(api_attribute_name) + ::API::Errors::UnwritableProperty.new(api_attribute_name, full_message) else ::API::Errors::Validation.new(api_attribute_name, full_message) end diff --git a/lib/api/errors/unwritable_property.rb b/lib/api/errors/unwritable_property.rb index ea90f2737f..21a423d33a 100644 --- a/lib/api/errors/unwritable_property.rb +++ b/lib/api/errors/unwritable_property.rb @@ -33,8 +33,8 @@ module API identifier 'urn:openproject-org:api:v3:errors:PropertyIsReadOnly' code 422 - def initialize(property) - super I18n.t('api_v3.errors.writing_read_only_attributes') + def initialize(property, message) + super message @property = property @details = { attribute: property } diff --git a/lib/api/utilities/endpoints/modify.rb b/lib/api/utilities/endpoints/modify.rb index 2d410854ad..ce6f1a3fb3 100644 --- a/lib/api/utilities/endpoints/modify.rb +++ b/lib/api/utilities/endpoints/modify.rb @@ -43,12 +43,16 @@ module API end def present_error(call) - errors = call.errors - errors = merge_dependent_errors call if errors.empty? + api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(postprocess_errors(call))] - api_errors = [::API::Errors::ErrorBase.create_and_merge_errors(errors)] + fail(::API::Errors::MultipleErrors + .create_if_many(api_errors)) + end - fail ::API::Errors::MultipleErrors.create_if_many(api_errors) + def postprocess_errors(call) + errors = call.errors + errors = merge_dependent_errors call if errors.empty? + errors end def merge_dependent_errors(call) diff --git a/lib/api/v3/memberships/schemas/membership_schema_representer.rb b/lib/api/v3/memberships/schemas/membership_schema_representer.rb index dd58666fc9..2fb6783574 100644 --- a/lib/api/v3/memberships/schemas/membership_schema_representer.rb +++ b/lib/api/v3/memberships/schemas/membership_schema_representer.rb @@ -39,17 +39,14 @@ module API end schema :id, - type: 'Integer', - visibility: false + type: 'Integer' schema :created_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema_with_allowed_link :project, has_default: false, required: true, - visibility: false, href_callback: ->(*) { allowed_projects_href } @@ -57,7 +54,6 @@ module API schema_with_allowed_link :principal, has_default: false, required: true, - visibility: false, href_callback: ->(*) { allowed_principal_href } @@ -67,7 +63,6 @@ module API name_source: :role, has_default: false, required: true, - visibility: false, href_callback: ->(*) { api_v3_paths.path_for(:roles, filters: [{ unit: { operator: '=', values: ['project'] } }]) } diff --git a/lib/api/v3/projects/schemas/project_schema_representer.rb b/lib/api/v3/projects/schemas/project_schema_representer.rb index 8ecde8b072..0c04bf2dd8 100644 --- a/lib/api/v3/projects/schemas/project_schema_representer.rb +++ b/lib/api/v3/projects/schemas/project_schema_representer.rb @@ -37,45 +37,37 @@ module API custom_field_injector type: :schema_representer schema :id, - type: 'Integer', - visibility: false + type: 'Integer' schema :name, type: 'String', - visibility: false, min_length: 1, max_length: 255 schema :identifier, type: 'String', - visibility: false, min_length: 1, max_length: 100 schema :description, type: 'Formattable', - visibility: false, required: false schema :public, - type: 'Boolean', - visibility: false + type: 'Boolean' schema :active, - type: 'Boolean', - visibility: false + type: 'Boolean' schema :status, type: 'ProjectStatus', name_source: ->(*) { I18n.t('activerecord.attributes.project/status.code') }, - visibility: false, required: false, writable: ->(*) { represented.writable?(:status) } schema :status_explanation, type: 'Formattable', name_source: ->(*) { I18n.t('activerecord.attributes.project/status.explanation') }, - visibility: false, required: false, writable: ->(*) { represented.writable?(:status) } @@ -93,12 +85,10 @@ module API } schema :created_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :updated_at, - type: 'DateTime', - visibility: false + type: 'DateTime' def self.represented_class ::Project diff --git a/lib/api/v3/queries/schemas/custom_option_filter_dependency_representer.rb b/lib/api/v3/queries/schemas/custom_option_filter_dependency_representer.rb index 0542cc7be7..203fabf7f4 100644 --- a/lib/api/v3/queries/schemas/custom_option_filter_dependency_representer.rb +++ b/lib/api/v3/queries/schemas/custom_option_filter_dependency_representer.rb @@ -40,7 +40,6 @@ module API writable: true, has_default: false, required: true, - visibility: false, values_callback: ->(*) { represented.custom_field.custom_options }, diff --git a/lib/api/v3/queries/schemas/filter_dependency_representer.rb b/lib/api/v3/queries/schemas/filter_dependency_representer.rb index a61c489968..a565a88202 100644 --- a/lib/api/v3/queries/schemas/filter_dependency_representer.rb +++ b/lib/api/v3/queries/schemas/filter_dependency_representer.rb @@ -46,7 +46,6 @@ module API writable: true, has_default: false, required: true, - visibility: false, href_callback: ->(*) { href_callback }, diff --git a/lib/api/v3/queries/schemas/query_filter_instance_schema_representer.rb b/lib/api/v3/queries/schemas/query_filter_instance_schema_representer.rb index 9aad277a79..186e159dad 100644 --- a/lib/api/v3/queries/schemas/query_filter_instance_schema_representer.rb +++ b/lib/api/v3/queries/schemas/query_filter_instance_schema_representer.rb @@ -41,8 +41,7 @@ module API type: 'String', writable: false, has_default: true, - required: true, - visibility: false + required: true def self.filter_representer ::API::V3::Queries::Filters::QueryFilterRepresenter @@ -61,7 +60,6 @@ module API type: 'QueryFilter', required: true, writable: true, - visibility: false, values_callback: -> { [filter] }, @@ -86,7 +84,6 @@ module API writable: true, has_default: false, required: true, - visibility: false, values_callback: -> { filter.available_operators }, diff --git a/lib/api/v3/queries/schemas/query_schema_representer.rb b/lib/api/v3/queries/schemas/query_schema_representer.rb index 4cdaff48ae..70a7413bb8 100644 --- a/lib/api/v3/queries/schemas/query_schema_representer.rb +++ b/lib/api/v3/queries/schemas/query_schema_representer.rb @@ -61,34 +61,28 @@ module API end schema :id, - type: 'Integer', - visibility: false + type: 'Integer' schema :name, type: 'String', writable: true, min_length: 1, - max_length: 255, - visibility: false + max_length: 255 schema :created_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :updated_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :user, type: 'User', - has_default: true, - visibility: false + has_default: true schema_with_allowed_link :project, type: 'Project', required: false, writable: true, - visibility: false, href_callback: ->(*) { api_v3_paths.query_available_projects } @@ -100,85 +94,73 @@ module API represented.project, global: represented.project.nil?) end, - has_default: true, - visibility: false + has_default: true schema :sums, type: 'Boolean', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :timeline_visible, type: 'Boolean', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :timeline_zoom_level, type: 'String', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :timeline_labels, type: 'QueryTimelineLabels', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :highlighting_mode, type: 'String', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :display_representation, type: 'String', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :show_hierarchies, type: 'Boolean', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema :starred, type: 'Boolean', required: false, writable: false, - has_default: true, - visibility: false + has_default: true schema :hidden, type: 'Boolean', required: true, writable: true, - has_default: true, - visibility: false + has_default: true schema :ordered_work_packages, type: 'QueryOrder', required: false, writable: true, - has_default: true, - visibility: false + has_default: true schema_with_allowed_collection :columns, type: '[]QueryColumn', required: false, writable: true, has_default: true, - visibility: false, values_callback: -> { represented.available_columns }, value_representer: ->(column) { Columns::QueryColumnsFactory.representer(column) @@ -204,7 +186,6 @@ module API type: '[]QueryGroupBy', required: false, writable: true, - visibility: false, values_callback: -> { represented.groupable_columns }, value_representer: GroupBys::QueryGroupByRepresenter, link_factory: ->(column) { @@ -221,7 +202,6 @@ module API required: false, writable: true, has_default: true, - visibility: false, values_callback: -> { represented.available_highlighting_columns }, value_representer: ->(column) { Columns::QueryColumnsFactory.representer(column) @@ -241,7 +221,6 @@ module API required: false, writable: true, has_default: true, - visibility: false, values_callback: -> do values = represented.sortable_columns.map do |column| [SortBys::SortByDecorator.new(column, 'asc'), @@ -263,8 +242,7 @@ module API schema :results, type: 'WorkPackageCollection', required: false, - writable: false, - visibility: false + writable: false property :filters_schemas, embedded: true, diff --git a/lib/api/v3/utilities/custom_field_injector.rb b/lib/api/v3/utilities/custom_field_injector.rb index 1ce81b4f35..374d655b51 100644 --- a/lib/api/v3/utilities/custom_field_injector.rb +++ b/lib/api/v3/utilities/custom_field_injector.rb @@ -188,7 +188,8 @@ module API writable: writable, min_length: cf_min_length(custom_field), max_length: cf_max_length(custom_field), - regular_expression: cf_regexp(custom_field) + regular_expression: cf_regexp(custom_field), + options: cf_options(custom_field) end def path_method_for(custom_field) @@ -321,6 +322,12 @@ module API custom_field.regexp unless custom_field.regexp.blank? end + def cf_options(custom_field) + { + rtl: ("true" if custom_field.content_right_to_left) + } + end + def list_schemas_values_callback(custom_field) ->(*) { represented.assignable_custom_field_values(custom_field) } end diff --git a/lib/api/v3/versions/schemas/version_schema_representer.rb b/lib/api/v3/versions/schemas/version_schema_representer.rb index af889f922a..af99e25b31 100644 --- a/lib/api/v3/versions/schemas/version_schema_representer.rb +++ b/lib/api/v3/versions/schemas/version_schema_representer.rb @@ -45,30 +45,25 @@ module API end schema :id, - type: 'Integer', - visibility: false + type: 'Integer' schema :name, type: 'String', min_length: 1, - max_length: 60, - visibility: false + max_length: 60 schema :description, type: 'Formattable', - required: false, - visibility: false + required: false schema :start_date, type: 'Date', - required: false, - visibility: false + required: false schema :due_date, as: 'endDate', type: 'Date', - required: false, - visibility: false + required: false schema_with_allowed_string_collection :status, type: 'String' @@ -80,7 +75,6 @@ module API as: :definingProject, has_default: false, required: true, - visibility: false, href_callback: ->(*) { next unless represented.new_record? @@ -88,12 +82,10 @@ module API } schema :created_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :updated_at, - type: 'DateTime', - visibility: false + type: 'DateTime' def self.represented_class Version diff --git a/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb b/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb index 2e21973abe..9f49270e6c 100644 --- a/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb +++ b/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb @@ -45,7 +45,6 @@ module API writable: true, has_default: false, required: true, - visibility: false, values_callback: ->(*) { represented.allowed_values }, diff --git a/modules/bcf/app/contracts/bcf/issues/base_contract.rb b/modules/bcf/app/contracts/bcf/issues/base_contract.rb new file mode 100644 index 0000000000..76a0e696de --- /dev/null +++ b/modules/bcf/app/contracts/bcf/issues/base_contract.rb @@ -0,0 +1,43 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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. +#++ + +module Bcf::Issues + class BaseContract < ::ModelContract + attribute :uuid + attribute :work_package + attribute :index + + def validate + validate_user_allowed_to_manage + + super + end + end +end diff --git a/modules/bcf/app/contracts/bcf/issues/create_contract.rb b/modules/bcf/app/contracts/bcf/issues/create_contract.rb new file mode 100644 index 0000000000..dc46229c0b --- /dev/null +++ b/modules/bcf/app/contracts/bcf/issues/create_contract.rb @@ -0,0 +1,39 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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. +#++ + +module Bcf::Issues + class CreateContract < BaseContract + private + + def validate_user_allowed_to_manage + unless model.project && user.allowed_to?(:manage_bcf, model.project) + errors.add :base, :error_unauthorized + end + end + end +end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/create.rb b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/create.rb new file mode 100644 index 0000000000..4aab66915f --- /dev/null +++ b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/create.rb @@ -0,0 +1,50 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1::Endpoints + class Create < API::Utilities::Endpoints::Create + include ModifyMixin + + def present_success(_current_user, call) + render_representer + .new(call.result) + end + + def postprocess_errors(call) + Bcf::API::V2_1::Errors::ErrorMapper.map(super) + end + + private + + def deduce_process_service + "::Bcf::#{deduce_backend_namespace}::#{update_or_create}Service".constantize + end + end +end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/modify_mixin.rb b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/modify_mixin.rb new file mode 100644 index 0000000000..7d5fd4fdd1 --- /dev/null +++ b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/modify_mixin.rb @@ -0,0 +1,46 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1::Endpoints + module ModifyMixin + private + + def deduce_parse_service + Bcf::API::V2_1::ParseResourceParamsService + end + + def deduce_in_and_out_representer + "::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize + end + + alias_method :deduce_parse_representer, :deduce_in_and_out_representer + alias_method :deduce_render_representer, :deduce_in_and_out_representer + end +end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb index 537270d949..dff9c2e14f 100644 --- a/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb +++ b/modules/bcf/app/controllers/bcf/api/v2_1/endpoints/update.rb @@ -30,22 +30,11 @@ module Bcf::API::V2_1::Endpoints class Update < API::Utilities::Endpoints::Update + include ModifyMixin + def present_success(_current_user, call) render_representer .new(call.result) end - - private - - def deduce_parse_service - Bcf::API::V2_1::ParseResourceParamsService - end - - def deduce_in_and_out_representer - "::Bcf::API::V2_1::#{deduce_api_namespace}::SingleRepresenter".constantize - end - - alias_method :deduce_parse_representer, :deduce_in_and_out_representer - alias_method :deduce_render_representer, :deduce_in_and_out_representer end end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb b/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb new file mode 100644 index 0000000000..f33a20811d --- /dev/null +++ b/modules/bcf/app/controllers/bcf/api/v2_1/project_extensions/api.rb @@ -0,0 +1,41 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1 + module ProjectExtensions + class API < ::API::OpenProjectAPI + + get :extensions do + mapper = Definitions.new(project: @project, user: current_user) + Representer.new(mapper) + end + end + end +end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb b/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb index 06336eb7eb..17d4e65b38 100644 --- a/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb +++ b/modules/bcf/app/controllers/bcf/api/v2_1/projects_api.rb @@ -53,6 +53,7 @@ module Bcf::API::V2_1 put &::Bcf::API::V2_1::Endpoints::Update.new(model: Project).mount mount Bcf::API::V2_1::TopicsAPI + mount Bcf::API::V2_1::ProjectExtensions::API end end end diff --git a/modules/bcf/app/controllers/bcf/api/v2_1/topics_api.rb b/modules/bcf/app/controllers/bcf/api/v2_1/topics_api.rb index 6bff40eef3..c5c39b230f 100644 --- a/modules/bcf/app/controllers/bcf/api/v2_1/topics_api.rb +++ b/modules/bcf/app/controllers/bcf/api/v2_1/topics_api.rb @@ -47,6 +47,25 @@ module Bcf::API::V2_1 scope: -> { topics }) .mount + post &::Bcf::API::V2_1::Endpoints::Create + .new(model: Bcf::Issue, + api_name: 'Topics', + params_modifier: ->(attributes) { + attributes[:project_id] = @project.id + + wp_attributes = Bcf::Issues::TransformAttributesService + .new + .call(attributes) + .result + + attributes + .slice(:stage, + :index, + :labels) + .merge(wp_attributes) + }) + .mount + route_param :topic_uuid, regexp: /\A[a-f0-9\-]+\z/ do after_validation do @issue = topics.find_by_uuid!(params[:topic_uuid]) diff --git a/modules/bcf/app/controllers/bcf/issues_controller.rb b/modules/bcf/app/controllers/bcf/issues_controller.rb index 66b5693d17..b1e5d9e968 100644 --- a/modules/bcf/app/controllers/bcf/issues_controller.rb +++ b/modules/bcf/app/controllers/bcf/issues_controller.rb @@ -141,9 +141,9 @@ module ::Bcf raise(StandardError.new(I18n.t('bcf.exceptions.file_invalid'))) end - @issues = ::Bcf::Issue.with_markup - .includes(work_package: %i[status priority assigned_to]) - .where(uuid: @listing.map { |e| e[:uuid] }, project: @project) + @issues = ::Bcf::Issue + .includes(work_package: %i[status priority assigned_to]) + .where(uuid: @listing.map { |e| e[:uuid] }, project: @project) render 'bcf/issues/diff_on_work_packages' end diff --git a/modules/bcf/app/models/bcf/issue.rb b/modules/bcf/app/models/bcf/issue.rb index c7493e1f11..0bb8c0b4dd 100644 --- a/modules/bcf/app/models/bcf/issue.rb +++ b/modules/bcf/app/models/bcf/issue.rb @@ -9,120 +9,14 @@ module Bcf after_update :invalidate_markup_cache - class << self - def with_markup - select '*', - extract_first_node(title_path, 'title'), - extract_first_node(description_path, 'description'), - extract_first_node(priority_text_path, 'priority_text'), - extract_first_node(status_text_path, 'status_text'), - extract_first_node(type_text_path, 'type_text'), - extract_first_node(assignee_text_path, 'assignee_text'), - extract_first_node(due_date_text_path, 'due_date_text'), - extract_first_node(creation_date_text_path, 'creation_date_text'), - extract_first_node(creation_author_text_path, 'creation_author_text'), - extract_first_node(modified_date_text_path, 'modified_date_text'), - extract_first_node(modified_author_text_path, 'modified_author_text'), - extract_first_node(index_text_path, 'index_text'), - extract_first_node(stage_text_path, 'stage_text'), - extract_nodes(labels_path, 'labels') - end + validates :work_package, presence: true + class << self def of_project(project) includes(:work_package) .references(:work_packages) .merge(WorkPackage.for_projects(project)) end - - protected - - def title_path - '/Markup/Topic/Title/text()' - end - - def description_path - '/Markup/Topic/Description/text()' - end - - def priority_text_path - '/Markup/Topic/Priority/text()' - end - - def status_text_path - '/Markup/Topic/@TopicStatus' - end - - def type_text_path - '/Markup/Topic/@TopicType' - end - - def assignee_text_path - '/Markup/Topic/AssignedTo/text()' - end - - def due_date_text_path - '/Markup/Topic/DueDate/text()' - end - - def stage_text_path - '/Markup/Topic/Stage/text()' - end - - def creation_date_text_path - '/Markup/Topic/CreationDate/text()' - end - - def creation_author_text_path - '/Markup/Topic/CreationAuthor/text()' - end - - def modified_date_text_path - '/Markup/Topic/ModifiedDate/text()' - end - - def modified_author_text_path - '/Markup/Topic/ModifiedAuthor/text()' - end - - def index_text_path - '/Markup/Topic/Index/text()' - end - - def labels_path - '/Markup/Topic/Labels/text()' - end - - private - - def extract_first_node(path, as) - "(xpath('#{path}', markup))[1] AS #{as}" - end - - def extract_nodes(path, as) - "(xpath('#{path}', markup)) AS #{as}" - end - end - - %i[title - description - priority_text - status_text - type_text - assignee_text - due_date_text - creation_date_text - creation_author_text - modified_date_text - modified_author_text - stage_text - index_text].each do |name| - define_method name do - from_attributes_or_doc name - end - end - - def labels - from_attributes_or_doc :labels, multiple: true end def markup_doc @@ -132,21 +26,5 @@ module Bcf def invalidate_markup_cache @markup_doc = nil end - - private - - def from_attributes_or_doc(key, multiple: false) - if attributes.keys.include? key.to_s - self[key] - else - path = markup_doc.xpath(self.class.send("#{key}_path")) - - if multiple - path.map(&:to_s) - else - path.first.to_s.presence - end - end - end end end diff --git a/modules/bcf/app/representers/bcf/api/v2_1/errors/error_mapper.rb b/modules/bcf/app/representers/bcf/api/v2_1/errors/error_mapper.rb new file mode 100644 index 0000000000..40be1524eb --- /dev/null +++ b/modules/bcf/app/representers/bcf/api/v2_1/errors/error_mapper.rb @@ -0,0 +1,64 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1::Errors + class ErrorMapper + extend ActiveModel::Naming + extend ActiveModel::Translation + + def read_attribute_for_validation(_attr) + nil + end + + def self.lookup_ancestors + [::Bcf::Issue] + end + + def self.map(original_errors) + mapped_errors = ActiveModel::Errors.new(new) + + original_errors.send(:error_symbols).each do |key, errors| + errors.map(&:first).each do |error| + mapped_errors.add(error_key_mapper(key), error) + end + end + + mapped_errors + end + + def self.i18n_scope + :activerecord + end + + def self.error_key_mapper(key) + { subject: :title }[key] || key + end + end +end diff --git a/modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb b/modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb index fed4d917d7..605b309d6a 100644 --- a/modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb +++ b/modules/bcf/app/representers/bcf/api/v2_1/errors/error_representer.rb @@ -31,7 +31,9 @@ module Bcf::API::V2_1::Errors class ErrorRepresenter < BaseRepresenter property :message, - getter: ->(*) { message }, + getter: ->(*) { + [message].concat(Array(errors).map(&:message)).compact.join(' ') + }, render_nil: true end end diff --git a/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb new file mode 100644 index 0000000000..fb514aef30 --- /dev/null +++ b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/definitions.rb @@ -0,0 +1,110 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1 + module ProjectExtensions + class Definitions + def initialize(project:, user:) + @project = project + @user = user + end + + def topic_type + project.types.pluck(:name) + end + + ## + # We only return the default status for now + # since that can always be set to a new issue + def topic_status + Status + .where_default + .pluck(:name) + end + + def priority + OpenProject::Cache.fetch(IssuePriority.all.cache_key, 'names') do + IssuePriority.all.pluck(:name) + end + end + + def user_id_type + if allowed?(:view_members) + project.possible_assignees.pluck(:mail) + else + [] + end + end + + # TODO: Labels do not yet exist + def topic_label + [] + end + + # TODO: Stage do not yet exist + def stage + [] + end + + # TODO: Snippet types do not exist + def snippet_type + [] + end + + def project_actions + [].tap do |actions| + actions << 'update' if allowed?(:edit_project) + actions << 'createTopic' if allowed?(:manage_bcf) + end + end + + def topic_actions + if allowed?(:manage_bcf) + %w[update updateRelatedTopics updateFiles createViewpoint] + else + [] + end + end + + def comment_actions + [] + end + + private + + attr_reader :project, :user + + def allowed?(permission) + user.allowed_to?(permission, project) + end + end + end +end + diff --git a/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/representer.rb b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/representer.rb new file mode 100644 index 0000000000..f3461f4de9 --- /dev/null +++ b/modules/bcf/app/representers/bcf/api/v2_1/project_extensions/representer.rb @@ -0,0 +1,44 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Bcf::API::V2_1 + class ProjectExtensions::Representer < BaseRepresenter + property :topic_type + property :topic_status + property :topic_label + property :snippet_type + property :priority + property :user_id_type + property :stage + property :project_actions + property :topic_actions + property :comment_actions + end +end diff --git a/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb b/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb index 11e1051609..18353e0d2e 100644 --- a/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb +++ b/modules/bcf/app/representers/bcf/api/v2_1/topics/single_representer.rb @@ -35,45 +35,114 @@ module Bcf::API::V2_1 property :uuid, as: :guid - property :type_text, - as: :topic_type + property :type, + as: :topic_type, + getter: ->(*) { + work_package + .type + .name + } - property :status_text, - as: :topic_status + property :status, + as: :topic_status, + getter: ->(*) { + work_package + .status + .name + } + + property :priority, + as: :priority, + getter: ->(*) { + work_package + .priority + .name + } property :reference_links, getter: ->(decorator:, **) { [decorator.api_v3_paths.work_package(work_package.id)] } - property :title + property :title, + getter: ->(*) { + work_package.subject + } - property :index_text, - as: :index + property :index property :labels - property :creation_date_text, - as: :creation_date + property :creation_date, + getter: ->(decorator:, **) { + decorator + .formatted_date_time(:created_at) + } - property :creation_author_text, - as: :creation_author + property :creation_author, + getter: ->(*) { + work_package + .author + .mail + } - property :modified_date_text, - as: :modified_date + property :modified_date, + getter: ->(decorator:, **) { + decorator + .formatted_date_time(:updated_at) + } - property :modified_author_text, - as: :modified_author + property :modified_author, + getter: ->(*) { + work_package + .journals + .max_by(&:version) + .user + .mail + } + + property :assignee, + as: :assigned_to, + getter: ->(decorator:, **) { + decorator + .assigned_to + &.mail + } + + property :stage - property :assignee_text, - as: :assigned_to + property :description, + getter: ->(*) { + work_package.description + } + + property :due_date, + getter: ->(decorator:, **) { + decorator.datetime_formatter.format_date(work_package.due_date, allow_nil: true) + }, + setter: ->(fragment:, decorator:, **) { + date = decorator + .datetime_formatter + .parse_date(fragment, + due_date, + allow_nil: true) + + self.due_date = date + } - property :stage_text, - as: :stage + def datetime_formatter + ::API::V3::Utilities::DateTimeFormatter + end - property :description + def formatted_date_time(method) + datetime_formatter + .format_datetime(represented.work_package.send(method), allow_nil: true) + end - property :due_date_text, - as: :due_date + def assigned_to + represented + .work_package + .assigned_to + end end end diff --git a/modules/bcf/app/services/bcf/issues/create_service.rb b/modules/bcf/app/services/bcf/issues/create_service.rb new file mode 100644 index 0000000000..cd5abb21a3 --- /dev/null +++ b/modules/bcf/app/services/bcf/issues/create_service.rb @@ -0,0 +1,51 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 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-2017 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. +#++ + +module Bcf::Issues + class CreateService < ::BaseServices::Create + private + + def before_perform(params) + wp_call = WorkPackages::CreateService + .new(user: user) + .call(params) + + if wp_call.success? + issue_params = { + work_package: wp_call.result + }.merge(params.slice(:stage, :labels, :index)) + + super(issue_params) + else + wp_call + end + end + end +end diff --git a/modules/bcf/app/services/bcf/issues/set_attributes_service.rb b/modules/bcf/app/services/bcf/issues/set_attributes_service.rb new file mode 100644 index 0000000000..db50aa742d --- /dev/null +++ b/modules/bcf/app/services/bcf/issues/set_attributes_service.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 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-2017 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. +#++ + +module Bcf::Issues + class SetAttributesService < ::BaseServices::SetAttributes + end +end diff --git a/modules/bcf/app/services/bcf/issues/transform_attributes_service.rb b/modules/bcf/app/services/bcf/issues/transform_attributes_service.rb new file mode 100644 index 0000000000..76ac8cda68 --- /dev/null +++ b/modules/bcf/app/services/bcf/issues/transform_attributes_service.rb @@ -0,0 +1,162 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 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-2017 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. +#++ + +module Bcf::Issues + class TransformAttributesService + def call(attributes) + ServiceResult.new success: true, + result: work_package_attributes(attributes) + end + + private + + ## + # BCF issues might have empty titles. OP needs one. + def title(attributes) + if attributes[:title] + attributes[:title] + elsif attributes[:import_options] + '(Imported BCF issue contained no title)' + end + end + + def author(project, attributes) + find_user_in_project(project, attributes[:author]) || User.system + end + + def assignee(project, attributes) + assignee = find_user_in_project(project, attributes[:assignee]) + + return assignee if assignee.present? + + missing_assignee(attributes[:assignee], attributes[:import_options] || {}) + end + + ## + # Try to find the given user by mail in the project + def find_user_in_project(project, mail) + project.users.find_by(mail: mail) + end + + def type(project, attributes) + type_name = attributes[:type] + type = project.types.find_by(name: type_name) + + return type if type.present? + + missing_type(project, type_name, attributes[:import_options] || {}) + end + + ## + # Handle unknown statuses during import + def status(attributes) + status_name = attributes[:status] + status = ::Status.find_by(name: status_name) + + return status if status.present? + + missing_status(status_name, attributes[:import_options] || {}) + end + + ## + # Handle unknown priorities during import + def priority(attributes) + priority_name = attributes[:priority] + priority = ::IssuePriority.find_by(name: priority_name) + + return priority if priority.present? + + missing_priority(priority_name, attributes[:import_options] || {}) + end + + ## + # Get mapped and raw attributes from MarkupExtractor + # and return all values that are non-nil + def work_package_attributes(attributes) + project = Project.find(attributes[:project_id]) + + { + # Fixed attributes we know + project: project, + type: type(project, attributes), + + # Native attributes from the extractor + subject: title(attributes), + description: attributes[:description], + due_date: attributes[:due_date], + start_date: attributes[:start_date], + + # Mapped attributes + assigned_to: assignee(project, attributes), + status: status(attributes), + priority: priority(attributes) + }.compact + end + + def missing_status(status_name, import_options) + if import_options[:unknown_statuses_action] == 'use_default' + ::Status.default + elsif import_options[:unknown_statuses_action] == 'chose' && + import_options[:unknown_statuses_chose_ids].any? + ::Status.find_by(id: import_options[:unknown_statuses_chose_ids].first) + elsif status_name + Status::InexistentStatus.new + end + end + + def missing_priority(priority_name, import_options) + if import_options[:unknown_priorities_action] == 'use_default' + # NOP The 'use_default' case gets already covered by OP. + elsif import_options[:unknown_priorities_action] == 'chose' && + import_options[:unknown_priorities_chose_ids].any? + ::IssuePriority.find_by(id: import_options[:unknown_priorities_chose_ids].first) + elsif priority_name + Priority::InexistentPriority.new + end + end + + def missing_type(project, type_name, import_options) + if import_options[:unknown_types_action] == 'use_default' + project.types.default&.first + elsif import_options[:unknown_types_action] == 'chose' && + import_options[:unknown_types_chose_ids].any? + project.types.find_by(id: import_options[:unknown_types_chose_ids].first) + elsif type_name + Type::InexistentType.new + end + end + + def missing_assignee(assignee_name, import_options) + if import_options[:invalid_people_action] != 'anonymize' && assignee_name + User::InexistentUser.new + end + end + end +end diff --git a/modules/bcf/config/locales/crowdin/tr.yml b/modules/bcf/config/locales/crowdin/tr.yml index 483f5a96cf..631bf96438 100644 --- a/modules/bcf/config/locales/crowdin/tr.yml +++ b/modules/bcf/config/locales/crowdin/tr.yml @@ -63,5 +63,5 @@ tr: permission_manage_bcf: "BCF sorunlarını içe aktarın ve yönetin" oauth: scopes: - bcf_v2_1: "Full access to the BCF v2.1 API" - bcf_v2_1_text: "Application will receive full read & write access to the OpenProject BCF API v2.1 to perform actions on your behalf." + bcf_v2_1: "BCF v2.1 API'sine tam erişim" + bcf_v2_1_text: "Uygulama, sizin adınıza işlem yapmak için OpenProject BCF API v2.1'e tam okuma ve yazma erişimi alacaktır." diff --git a/modules/bcf/db/migrate/20191119144123_add_issue_columns.rb b/modules/bcf/db/migrate/20191119144123_add_issue_columns.rb new file mode 100644 index 0000000000..c7d50647fb --- /dev/null +++ b/modules/bcf/db/migrate/20191119144123_add_issue_columns.rb @@ -0,0 +1,9 @@ +class AddIssueColumns < ActiveRecord::Migration[6.0] + def change + change_table :bcf_issues do |i| + i.string :stage + i.integer :index + i.text :labels, array: true, default: [] + end + end +end diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb index 252b6d6268..f96d785436 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/importer.rb @@ -155,8 +155,7 @@ module OpenProject::Bcf::BcfXml zip, entry, current_user: current_user, - import_options: import_options, - aggregations: aggregations).extract! + import_options: import_options).extract! if issue.errors.blank? issue.save diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb index d6fbe388ae..d7cf56f71b 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_reader.rb @@ -6,10 +6,10 @@ require_relative 'file_entry' module OpenProject::Bcf::BcfXml class IssueReader - attr_reader :zip, :entry, :issue, :extractor, :project, :user, :import_options, :aggregations + attr_reader :zip, :entry, :issue, :extractor, :project, :user, :import_options attr_accessor :wp_last_updated_at, :is_update - def initialize(project, zip, entry, current_user:, import_options:, aggregations:) + def initialize(project, zip, entry, current_user:, import_options:) @zip = zip @entry = entry @project = project @@ -17,23 +17,13 @@ module OpenProject::Bcf::BcfXml @issue = find_or_initialize_issue @extractor = MarkupExtractor.new(entry) @import_options = import_options - @aggregations = aggregations - @doc = nil @wp_last_updated_at = nil @is_update = false end def extract! - @doc = extractor.doc + markup = extractor.doc.to_xml(indent: 2) - treat_empty_titles - treat_unknown_types - treat_unknown_statuses - treat_unknown_priorities - - extractor.doc = @doc - - markup = @doc.to_xml(indent: 2) issue.markup = markup extractor.markup = markup @@ -51,82 +41,6 @@ module OpenProject::Bcf::BcfXml private - ## - # BCF issues might have empty titles. OP needs one. - def treat_empty_titles - title_node = @doc.xpath('/Markup/Topic/Title').first - return if title_node&.content&.present? - - title_node.content = "(Imported BCF issue contained no title)" - end - - ## - # Handle unknown types during import - def treat_unknown_types - if aggregations.unknown_types.present? - if import_options[:unknown_types_action] == 'use_default' - replace_type_with(::Type.default.first&.name) - elsif import_options[:unknown_types_action] == 'chose' && import_options[:unknown_types_chose_ids].any? - replace_type_with(::Type.find_by(id: import_options[:unknown_types_chose_ids].first)&.name) - else - raise StandardError.new 'Unknown topic type found in import. Use an existing type name.' - end - end - end - - def replace_type_with(new_type_name) - raise StandardError.new "New type name can't be blank." unless new_type_name.present? - - @doc.xpath('/Markup/Topic').first.set_attribute('TopicType', new_type_name) - end - - ## - # Handle unknown statuses during import - def treat_unknown_statuses - if aggregations.unknown_statuses.present? - if import_options[:unknown_statuses_action] == 'use_default' - replace_status_with(::Status.default&.name) - elsif import_options[:unknown_statuses_action] == 'chose' && import_options[:unknown_statuses_chose_ids].any? - replace_status_with(::Status.find_by(id: import_options[:unknown_statuses_chose_ids].first)&.name) - else - raise StandardError.new 'Unknown topic status found in import. Use an existing status name.' - end - end - end - - def replace_status_with(new_status_name) - raise StandardError.new "New status name can't be blank." unless new_status_name.present? - - @doc.xpath('/Markup/Topic').first.set_attribute('TopicStatus', new_status_name) - end - - ## - # Handle unknown priorities during import - def treat_unknown_priorities - if aggregations.unknown_priorities.present? - if import_options[:unknown_priorities_action] == 'use_default' - # NOP The 'use_default' case gets already covered by OP. - elsif import_options[:unknown_priorities_action] == 'chose' && import_options[:unknown_priorities_chose_ids].any? - replace_priorities_with(::IssuePriority.find_by(id: import_options[:unknown_priorities_chose_ids].first)&.name) - else - raise StandardError.new 'Unknown topic priority found in import. Use an existing priority name.' - end - end - end - - def replace_priorities_with(new_priority_name) - raise StandardError.new "New priority name can't be blank." unless new_priority_name.present? - - priority_node = @doc.xpath('/Markup/Topic/Priority').first - if priority_node - priority_node.content = new_priority_name - else - # Valid BCF XML Topics must have a Title node. So we can add the Priority node just behind it and thus, - # maintain the schema's sequence compliance. - @doc.at('/Markup/Topic/Title').after("#{new_priority_name}") - end - end - def synchronize_with_work_package self.is_update = issue.work_package.present? self.wp_last_updated_at = issue.work_package&.updated_at @@ -152,13 +66,9 @@ module OpenProject::Bcf::BcfXml end def create_work_package - call = WorkPackages::CreateService.new(user: user).call(work_package_attributes - .merge(send_notifications: false) - .symbolize_keys) + call = WorkPackages::CreateService.new(user: user).call(work_package_attributes) - if call.success? - force_overwrite(call.result) - end + force_overwrite(call.result) if call.success? call end @@ -167,62 +77,42 @@ module OpenProject::Bcf::BcfXml find_user_in_project(extractor.author) || User.system end - def assignee - find_user_in_project(extractor.assignee) - end - - def type - type_name = extractor.type - type = ::Type.find_by(name: type_name) - - return type if type.present? - - return ::Type.default&.first if import_options[:unknown_types_action] == 'default' - - if import_options[:unknown_types_action] == 'chose' && - import_options[:unknown_types_chose_ids].any? - return ::Type.find_by(id: import_options[:unknown_types_chose_ids].first) - else - ServiceResult.new success: false, - errors: issue.errors, - result: issue - end - end - - def start_date - extractor.creation_date.to_date unless is_update - end - def update_work_package if import_is_newer? WorkPackages::UpdateService .new(user: user, model: issue.work_package) - .call(work_package_attributes.merge(send_notifications: false).symbolize_keys) + .call(work_package_attributes) else import_is_outdated(issue) end end - ## - # Get mapped and raw attributes from MarkupExtractor - # and return all values that are non-nil + ### + ## Get mapped and raw attributes from MarkupExtractor + ## and return all values that are non-nil def work_package_attributes - { - # Fixed attributes we know - project: project, - type: type, - - # Native attributes from the extractor - subject: extractor.title, - description: extractor.description, - due_date: extractor.due_date, - start_date: start_date, - - # Mapped attributes - assigned_to: assignee, - status_id: statuses.fetch(extractor.status, statuses[:default]), - priority_id: priorities.fetch(extractor.priority, priorities[:default]) - }.compact + attributes = ::Bcf::Issues::TransformAttributesService + .new + .call(extractor_attributes.merge(import_options: import_options)) + .result + .merge(send_notifications: false) + .symbolize_keys + + attributes[:start_date] = extractor.creation_date.to_date unless is_update + + attributes + end + + def extractor_attributes + attributes = { + project_id: project.id + } + + %i(type title description due_date assignee status priority).each do |key| + attributes[key] = extractor.send(key) + end + + attributes end ## @@ -400,22 +290,10 @@ module OpenProject::Bcf::BcfXml def update_journal_attributes(bcf_comment, comment_data) bcf_comment.journal.update(notes: comment_data[:comment], - created_at: comment_data[:modified_date]) + created_at: comment_data[:modified_date]) bcf_comment.journal.save end - ## - # Keep a hash map of current status ids for faster lookup - def statuses - @statuses ||= Hash[Status.pluck(:name, :id)].merge(default: Status.default.id) - end - - ## - # Keep a hash map of current status ids for faster lookup - def priorities - @priorities ||= Hash[IssuePriority.pluck(:name, :id)].merge(default: IssuePriority.default.try(:id)) - end - def import_is_outdated(issue) issue.errors.add :base, :conflict, diff --git a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb index 63bfd94ee1..1304f64267 100644 --- a/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb +++ b/modules/bcf/lib/open_project/bcf/bcf_xml/issue_writer.rb @@ -250,7 +250,7 @@ module OpenProject::Bcf::BcfXml def find_or_initialize_issue ::Bcf::Issue.find_or_initialize_by(work_package: work_package) end - + def to_bcf_datetime(date_time) date_time.utc.iso8601 end diff --git a/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb b/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb index c3f0a528a7..1df66a322f 100644 --- a/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb +++ b/modules/bcf/spec/api/v3/work_packages/work_package_representer_spec.rb @@ -31,91 +31,12 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do member_in_project: project, member_through_role: role) end - let(:markup) do - <<-MARKUP - -
- - IfcPile_01.ifc - 2014-10-27T16:27:27Z - ../IfcPile_01.ifc - -
- - https://bim--it.net - Maximum Content - High - 0 - Structural - IT Development - 2015-06-21T12:00:00Z - mike@example.com - 2015-06-21T14:22:47Z - mike@example.com - andy@example.com - This is a topic with all information present. - - JsonElement.json - http://json-schema.org - - - https://github.com/BuildingSMART/BCF-XML - GitHub BCF Specification - - - ../markup.xsd - Markup.xsd Schema - - - - - 2015-08-31T12:40:17Z - mike@example.com - This is an unmodified topic at the uppermost hierarchical level. - All times in the XML are marked as UTC times. - - - 2015-08-31T14:00:01Z - mike@example.com - This comment was a reply to the first comment in BCF v2.0. This is a no longer supported functionality and therefore is to be treated as a regular comment in v2.1. - - - 2015-08-31T13:07:11Z - mike@example.com - This comment again is in the highest hierarchy level. - It references a viewpoint. - - - - 2015-08-31T15:42:58Z - mike@example.com - This comment contained some spllng errs. - Hopefully, the modifier did catch them all. - 2015-08-31T16:07:11Z - mike@example.com - - - Viewpoint_8dc86298-9737-40b4-a448-98a9e953293a.bcfv - Snapshot_8dc86298-9737-40b4-a448-98a9e953293a.png - - - Viewpoint_21dd4807-e9af-439e-a980-04d913a6b1ce.bcfv - Snapshot_21dd4807-e9af-439e-a980-04d913a6b1ce.png - - - Viewpoint_81daa431-bf01-4a49-80a2-1ab07c177717.bcfv - Snapshot_81daa431-bf01-4a49-80a2-1ab07c177717.png - -
- MARKUP - end - let(:bcf_issue) do - FactoryBot.create(:bcf_issue_with_comment, markup: markup) + let!(:bcf_issue) do + FactoryBot.create(:bcf_issue_with_comment, work_package: work_package) end let(:work_package) do FactoryBot.create(:work_package, - project_id: project.id, - bcf_issue: bcf_issue) + project_id: project.id) end let(:representer) do described_class.new(work_package, diff --git a/modules/bcf/spec/bcf/bcf_xml/importer_spec.rb b/modules/bcf/spec/bcf/bcf_xml/importer_spec.rb index 6a7d453d3e..0782024674 100644 --- a/modules/bcf/spec/bcf/bcf_xml/importer_spec.rb +++ b/modules/bcf/spec/bcf/bcf_xml/importer_spec.rb @@ -63,7 +63,7 @@ describe ::OpenProject::Bcf::BcfXml::Importer do workflow priority bcf_manager_member - allow(User).to receive(:current).and_return(bcf_manager) + login_as(bcf_manager) end describe '#to_listing' do diff --git a/modules/bcf/spec/bcf/bcf_xml/issue_reader_spec.rb b/modules/bcf/spec/bcf/bcf_xml/issue_reader_spec.rb index 3c0eb79296..4c2264abf8 100644 --- a/modules/bcf/spec/bcf/bcf_xml/issue_reader_spec.rb +++ b/modules/bcf/spec/bcf/bcf_xml/issue_reader_spec.rb @@ -88,15 +88,13 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do end let(:entry_stream) { StringIO.new(markup) } let(:import_options) { OpenProject::Bcf::BcfXml::Importer::DEFAULT_IMPORT_OPTIONS } - let(:aggregations) { OpenProject::Bcf::BcfXml::Aggregations.new([], project) } subject do described_class.new(project, nil, entry, current_user: bcf_manager, - import_options: import_options, - aggregations: aggregations) + import_options: import_options) end before do @@ -134,12 +132,6 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do end context 'with no import options provided' do - let(:aggregations) do - Struct - .new(:unknown_statuses, :unknown_types, :unknown_priorities) - .new([nil], [nil], [nil]) - end - let(:bcf_issue) { subject.extract! } it 'sets a status' do @@ -160,7 +152,8 @@ describe ::OpenProject::Bcf::BcfXml::IssueReader do context 'on updating import' do context '#update_comment' do - let!(:bcf_issue) { FactoryBot.create :bcf_issue_with_comment } + let(:work_package) { FactoryBot.create(:work_package) } + let!(:bcf_issue) { FactoryBot.create :bcf_issue_with_comment, work_package: work_package } before do allow(subject).to receive(:issue).and_return(bcf_issue) diff --git a/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb b/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb index 51e5b2ad13..df07bf7fac 100644 --- a/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb +++ b/modules/bcf/spec/bcf/bcf_xml/issue_writer_spec.rb @@ -84,16 +84,16 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do end let(:bcf_issue) do FactoryBot.create(:bcf_issue_with_comment, + work_package: work_package, markup: markup) end let(:priority) { FactoryBot.create :priority_low } let(:current_user) { FactoryBot.create(:user) } let(:due_date) { DateTime.now } - let(:type) { FactoryBot.create :type, name: 'Issue'} + let(:type) { FactoryBot.create :type, name: 'Issue' } let(:work_package) do FactoryBot.create(:work_package, project_id: project.id, - bcf_issue: bcf_issue, priority: priority, author: current_user, assigned_to: current_user, @@ -104,7 +104,7 @@ describe ::OpenProject::Bcf::BcfXml::IssueWriter do before do allow(User).to receive(:current).and_return current_user - work_package.bcf_issue.comments.first.journal.update_attribute('journable_id', work_package.id) + bcf_issue.comments.first.journal.update_attribute('journable_id', work_package.id) FactoryBot.create(:work_package_journal, notes: "Some note created in OP.", journable_id: work_package.id) work_package.reload end diff --git a/modules/bcf/spec/contracts/bcf/issues/create_contract_spec.rb b/modules/bcf/spec/contracts/bcf/issues/create_contract_spec.rb new file mode 100644 index 0000000000..a5d70aeb9f --- /dev/null +++ b/modules/bcf/spec/contracts/bcf/issues/create_contract_spec.rb @@ -0,0 +1,45 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './shared_contract_examples' + +describe Bcf::Issues::CreateContract do + it_behaves_like 'issues contract' do + let(:issue) do + Bcf::Issue.new(uuid: issue_uuid, + work_package: issue_work_package, + stage: issue_stage, + index: issue_index, + labels: issue_labels) + end + let(:permissions) { [:manage_bcf] } + + subject(:contract) { described_class.new(issue, current_user) } + end +end diff --git a/modules/bcf/spec/contracts/bcf/issues/shared_contract_examples.rb b/modules/bcf/spec/contracts/bcf/issues/shared_contract_examples.rb new file mode 100644 index 0000000000..d44560ac17 --- /dev/null +++ b/modules/bcf/spec/contracts/bcf/issues/shared_contract_examples.rb @@ -0,0 +1,129 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +shared_examples_for 'issues contract' do + let(:current_user) do + FactoryBot.build_stubbed(:user) + end + let!(:allowed_to) do + allow(current_user) + .to receive(:allowed_to?) do |permission, permission_project| + permissions.include?(permission) && project == permission_project + end + end + let(:issue_uuid) { 'issue uuid' } + let(:project) { FactoryBot.build_stubbed(:project) } + let(:issue_work_package) { FactoryBot.build_stubbed(:stubbed_work_package, project: project) } + let(:issue_work_package_id) do + id = 5 + + allow(WorkPackage) + .to receive(:find) + .with(id) + .and_return(issue_work_package) + + id + end + let(:issue_stage) { nil } + let(:issue_labels) { [] } + let(:issue_index) { 8 } + + before do + allow(issue) + .to receive(:project) + .and_return(project) + end + + def expect_valid(valid, symbols = {}) + expect(contract.validate).to eq(valid) + + symbols.each do |key, arr| + expect(contract.errors.symbols_for(key)).to match_array arr + end + end + + shared_examples 'is valid' do + it 'is valid' do + expect_valid(true) + end + end + + it_behaves_like 'is valid' + + context 'if the uuid is nil' do + let(:issue_uuid) { nil } + + it_behaves_like 'is valid' # as the uuid will be set + end + + context 'if the work_package_id is nil' do + let(:issue_work_package) { nil } + + it 'is invalid' do + expect_valid(false, work_package: %i(blank)) + end + end + + context 'if the user lacks permission' do + let(:permissions) { [] } + + it 'is invalid' do + expect_valid(false, base: %i(error_unauthorized)) + end + end + + context 'if the stage is nil' do + let(:issue_stage) { nil } + + it_behaves_like 'is valid' + end + + context 'if the stage is written' do + let(:issue_stage) { 'some stage' } + + it 'is invalid' do + expect_valid(false, stage: %i(error_readonly)) + end + end + + context 'if labels is written' do + let(:issue_labels) { %w(some labels) } + + it 'is invalid' do + expect_valid(false, labels: %i(error_readonly)) + end + end + + context 'if index is nil' do + let(:issue_index) { nil } + + it_behaves_like 'is valid' + end +end diff --git a/modules/bcf/spec/factories/bcf_issue_factory.rb b/modules/bcf/spec/factories/bcf_issue_factory.rb index bf07fda9bb..094993505a 100644 --- a/modules/bcf/spec/factories/bcf_issue_factory.rb +++ b/modules/bcf/spec/factories/bcf_issue_factory.rb @@ -1,20 +1,15 @@ +#-- encoding: UTF-8 + #-- copyright -# OpenProject Backlogs Plugin -# -# Copyright (C)2013-2014 the OpenProject Foundation (OPF) -# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda -# Copyright (C)2010-2011 friflaj -# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns -# Copyright (C)2009-2010 Mark Maglana -# Copyright (C)2009 Joe Heck, Nate Lowrie +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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. +# 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 Backlogs is a derivative work based on ChiliProject Backlogs. -# The copyright follows: -# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj -# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 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 @@ -30,7 +25,7 @@ # 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. +# See docs/COPYRIGHT.rdoc for more details. #++ FactoryBot.define do @@ -114,6 +109,9 @@ FactoryBot.define do MARKUP end + stage { 'Construction stop' } + labels { ['Structural', 'IT Development', 'Huge'] } + sequence(:index) { |n| n } factory :bcf_issue_with_viewpoint do after(:create) do |issue| diff --git a/modules/bcf/spec/models/bcf/issue_spec.rb b/modules/bcf/spec/models/bcf/issue_spec.rb index 28963e900c..a94a2bd6bc 100644 --- a/modules/bcf/spec/models/bcf/issue_spec.rb +++ b/modules/bcf/spec/models/bcf/issue_spec.rb @@ -33,31 +33,6 @@ describe ::Bcf::Issue, type: :model do let(:work_package) { FactoryBot.create :work_package, type: type } let(:issue) { FactoryBot.create :bcf_issue, work_package: work_package } - shared_examples_for 'provides attributes' do - it "provides attributes" do - expect(subject.title).to be_eql 'Maximum Content' - expect(subject.description).to be_eql 'This is a topic with all information present.' - expect(subject.priority_text).to be_eql 'High' - expect(subject.status_text).to be_eql 'Open' - expect(subject.type_text).to be_eql 'Structural' - expect(subject.assignee_text).to be_eql 'andy@example.com' - expect(subject.index_text).to be_eql '0' - expect(subject.labels).to contain_exactly 'Structural', 'IT Development' - expect(subject.due_date_text).to be_nil - expect(subject.creation_date_text).to eql "2015-06-21T12:00:00Z" - expect(subject.creation_author_text).to eql "mike@example.com" - expect(subject.modified_date_text).to eql "2015-06-21T14:22:47Z" - expect(subject.modified_author_text).to eql "michelle@example.com" - expect(subject.stage_text).to eql "Construction start" - end - end - - context '#self.with_markup' do - subject { ::Bcf::Issue.with_markup.find_by id: issue.id } - - it_behaves_like 'provides attributes' - end - context '#markup_doc' do subject { issue } @@ -76,8 +51,6 @@ describe ::Bcf::Issue, type: :model do subject.save expect(subject.markup_doc).to_not be_eql(first_fetched_doc) end - - it_behaves_like 'provides attributes' end describe '.of_project' do diff --git a/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb b/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb new file mode 100644 index 0000000000..aea287a06d --- /dev/null +++ b/modules/bcf/spec/representers/bcf/api/v2_1/project_extensions/definitions_spec.rb @@ -0,0 +1,135 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +require_relative '../shared_examples' + +describe Bcf::API::V2_1::ProjectExtensions::Definitions, 'rendering' do + shared_let(:type_task) { FactoryBot.create :type_task, name: 'My BCF type' } + shared_let(:project) { FactoryBot.create(:project, types: [type_task]) } + let(:user) { FactoryBot.build_stubbed(:user) } + let(:instance) { described_class.new(project: project, user: user) } + + describe '#topic_type' do + subject { instance.topic_type } + + it 'returns the project type names' do + expect(subject).to eq ['My BCF type'] + end + end + + describe '#topic_status' do + let!(:default_status) { FactoryBot.create :default_status } + let!(:status) { FactoryBot.create :status } + subject { instance.topic_status } + + it 'returns default status only' do + expect(subject).to eq [default_status.name] + end + end + + describe '#priority' do + let!(:priority) { FactoryBot.create :default_priority } + subject { instance.priority } + + it 'returns statuses for the available types' do + expect(subject).to eq [priority.name] + end + end + + describe '#user_id_type' do + let!(:other_user) do + FactoryBot.create(:user, + member_in_project: project, + member_with_permissions: [:view_work_packages]) + end + subject { instance.user_id_type } + + before do + allow(user) + .to receive(:allowed_to?).with(:view_members, project) + .and_return is_permitted + end + + context 'with permissions' do + let(:is_permitted) { true } + + it 'returns the user as assignee' do + expect(subject).to eq [other_user.mail] + end + end + + context 'with no permissions' do + let(:is_permitted) { false } + + it 'returns nothing' do + expect(subject).to eq [] + end + end + end + + describe '#project_actions' do + subject { instance.project_actions } + + it 'includes nothing if not permitted' do + allow(user).to receive(:allowed_to?).and_return false + expect(subject).to be_empty + end + + it 'includes `update` if edit_project permission' do + allow(user).to receive(:allowed_to?).and_return false + allow(user).to receive(:allowed_to?).with(:edit_project, project).and_return true + + expect(subject).to include 'update' + end + + it 'includes `createTopic` if edit_project permission' do + allow(user).to receive(:allowed_to?).and_return false + allow(user).to receive(:allowed_to?).with(:manage_bcf, project).and_return true + + expect(subject).to include 'createTopic' + end + end + + describe '#topic_actions' do + subject { instance.topic_actions } + + it 'includes nothing if not permitted' do + allow(user).to receive(:allowed_to?).and_return false + expect(subject).to be_empty + end + + it 'includes `update` if manage_bcf permission' do + allow(user).to receive(:allowed_to?).and_return false + allow(user).to receive(:allowed_to?).with(:manage_bcf, project).and_return true + + expect(subject).to match_array %w[update updateRelatedTopics updateFiles createViewpoint] + end + end +end diff --git a/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb b/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb index 44fbd11bac..a097ac48cb 100644 --- a/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb +++ b/modules/bcf/spec/representers/bcf/api/v2_1/topics/single_representer_rendering_spec.rb @@ -33,7 +33,27 @@ require_relative '../shared_examples' describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do include API::V3::Utilities::PathHelper - let(:work_package) { FactoryBot.build_stubbed(:stubbed_work_package, type: FactoryBot.build_stubbed(:type)) } + let(:assignee) { FactoryBot.build_stubbed(:user) } + let(:creator) { FactoryBot.build_stubbed(:user) } + let(:modifier) { FactoryBot.build_stubbed(:user) } + let(:first_journal) { FactoryBot.build_stubbed(:journal, version: 1, user: creator) } + let(:last_journal) { FactoryBot.build_stubbed(:journal, version: 2, user: modifier) } + let(:journals) { [first_journal, last_journal] } + let(:type) { FactoryBot.build_stubbed(:type) } + let(:status) { FactoryBot.build_stubbed(:status) } + let(:priority) { FactoryBot.build_stubbed(:priority) } + let(:work_package) do + FactoryBot.build_stubbed(:stubbed_work_package, + assigned_to: assignee, + due_date: Date.today, + status: status, + priority: priority, + type: type).tap do |wp| + allow(wp) + .to receive(:journals) + .and_return(journals) + end + end let(:issue) { FactoryBot.build_stubbed(:bcf_issue, work_package: work_package) } let(:instance) { described_class.new(issue) } @@ -50,18 +70,25 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do context 'topic_type' do it_behaves_like 'attribute' do - let(:value) { issue.type_text } + let(:value) { type.name } let(:path) { 'topic_type' } end end context 'topic_status' do it_behaves_like 'attribute' do - let(:value) { issue.status_text } + let(:value) { status.name } let(:path) { 'topic_status' } end end + context 'priority' do + it_behaves_like 'attribute' do + let(:value) { priority.name } + let(:path) { 'priority' } + end + end + context 'reference_links' do it_behaves_like 'attribute' do let(:value) { [api_v3_paths.work_package(work_package.id)] } @@ -71,14 +98,14 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do context 'title' do it_behaves_like 'attribute' do - let(:value) { issue.title } + let(:value) { work_package.subject } let(:path) { 'title' } end end context 'index' do it_behaves_like 'attribute' do - let(:value) { issue.index_text } + let(:value) { issue.index } let(:path) { 'index' } end end @@ -92,56 +119,56 @@ describe Bcf::API::V2_1::Topics::SingleRepresenter, 'rendering' do context 'creation_date' do it_behaves_like 'attribute' do - let(:value) { issue.creation_date_text } + let(:value) { work_package.created_at.iso8601 } let(:path) { 'creation_date' } end end context 'creation_author' do it_behaves_like 'attribute' do - let(:value) { issue.creation_author_text } + let(:value) { work_package.author.mail } let(:path) { 'creation_author' } end end context 'modified_date' do it_behaves_like 'attribute' do - let(:value) { issue.modified_date_text } + let(:value) { work_package.updated_at.iso8601 } let(:path) { 'modified_date' } end end context 'modified_author' do it_behaves_like 'attribute' do - let(:value) { issue.modified_author_text } + let(:value) { modifier.mail } let(:path) { 'modified_author' } end end context 'description' do it_behaves_like 'attribute' do - let(:value) { issue.description } + let(:value) { work_package.description } let(:path) { 'description' } end end context 'due_date' do it_behaves_like 'attribute' do - let(:value) { issue.due_date_text } + let(:value) { work_package.due_date.iso8601 } let(:path) { 'due_date' } end end context 'assigned_to' do it_behaves_like 'attribute' do - let(:value) { issue.assignee_text } + let(:value) { work_package.assigned_to.mail } let(:path) { 'assigned_to' } end end context 'stage' do it_behaves_like 'attribute' do - let(:value) { issue.stage_text } + let(:value) { issue.stage } let(:path) { 'stage' } end end diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb new file mode 100644 index 0000000000..263aa9e00e --- /dev/null +++ b/modules/bcf/spec/requests/api/bcf/v2_1/project_extensions_api_spec.rb @@ -0,0 +1,116 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2019 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require 'rack/test' + +require_relative './shared_responses' + +describe 'BCF 2.1 project extensions resource', type: :request, content_type: :json do + include Rack::Test::Methods + shared_let(:type_task) { FactoryBot.create :type_task } + shared_let(:status) { FactoryBot.create :default_status } + shared_let(:priority) { FactoryBot.create :default_priority } + shared_let(:project) { FactoryBot.create(:project, enabled_module_names: [:bcf], types: [type_task]) } + subject(:response) { last_response } + + let(:path) { "/api/bcf/2.1/projects/#{project.id}/extensions" } + + context 'with only view_project permissions' do + let(:current_user) do + FactoryBot.create(:user, + member_in_project: project, + member_with_permissions: [:view_project]) + end + + before do + login_as(current_user) + get path + end + + it_behaves_like 'bcf api successful response' do + let(:expected_body) do + { + topic_type: [type_task.name], + topic_status: [status.name], + priority: [priority.name], + snippet_type: [], + stage: [], + topic_label: [], + user_id_type: [], + project_actions: [], + topic_actions: [], + comment_actions: [] + } + end + end + end + + context 'with edit permissions in project' do + let(:current_user) do + FactoryBot.create(:user, + member_in_project: project, + member_with_permissions: [:view_project, :edit_project, :manage_bcf, :view_members]) + end + + let(:other_user) { + FactoryBot.create(:user, + member_in_project: project, + member_with_permissions: [:view_project]) + } + + before do + other_user + login_as(current_user) + get path + end + + it_behaves_like 'bcf api successful response expectation' do + let(:expectations) do + ->(body) { + hash = JSON.parse(body) + + expect(hash.keys).to match_array %w[ + topic_type topic_status user_id_type project_actions topic_actions comment_actions + stage snippet_type priority topic_label + ] + + expect(hash['topic_type']).to include type_task.name + expect(hash['topic_status']).to include status.name + + expect(hash['user_id_type']).to include(other_user.mail, current_user.mail) + + expect(hash['project_actions']).to eq %w[update createTopic] + + expect(hash['topic_actions']).to eq %w[update updateRelatedTopics updateFiles createViewpoint] + expect(hash['comment_actions']).to eq [] + } + end + end + end +end diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb index 405fe34061..57ae5e2edc 100644 --- a/modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb +++ b/modules/bcf/spec/requests/api/bcf/v2_1/projects_api_spec.rb @@ -119,7 +119,7 @@ describe 'BCF 2.1 projects resource', type: :request, content_type: :json do end it_behaves_like 'bcf api unprocessable response' do - let(:message) { 'You must not write a read-only attribute.' } + let(:message) { 'ID was attempted to be written but is not writable.' } end end end diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb b/modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb index 44bbd1a759..5d4041e793 100644 --- a/modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb +++ b/modules/bcf/spec/requests/api/bcf/v2_1/shared_responses.rb @@ -27,82 +27,57 @@ #++ shared_examples_for 'bcf api successful response' do - it 'responds 200 OK' do + it 'responds correctly with the expected body', :aggregate_failures do expect(subject.status) - .to eql 200 + .to eql(defined?(expected_status) ? expected_status : 200) + expect(subject.body).to be_json_eql(expected_body.to_json) + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' end +end - it 'returns the resource' do - expect(subject.body) - .to be_json_eql(expected_body.to_json) - end +shared_examples_for 'bcf api successful response expectation' do + it 'responds correctly with the expected body', :aggregate_failures do + expect(subject.status).to eq 200 + + instance_exec(subject.body, &expectations) - it 'is has a json content type header' do - expect(subject.headers['Content-Type']) - .to eql 'application/json; charset=utf-8' + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' end end shared_examples_for 'bcf api not found response' do - it 'responds 404 NOT FOUND' do - expect(subject.status) - .to eql 404 + let(:expect_404) do + { message: 'The requested resource could not be found.' } end - it 'states a NOT FOUND message' do - expected = { - message: 'The requested resource could not be found.' - } - - expect(subject.body) - .to be_json_eql(expected.to_json) - end - - it 'is has a json content type header' do - expect(subject.headers['Content-Type']) - .to eql 'application/json; charset=utf-8' + it 'responds 404 NOT FOUND', :aggregate_failures do + expect(subject.status).to eq 404 + expect(subject.body).to be_json_eql(expect_404.to_json) + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' end end shared_examples_for 'bcf api not allowed response' do - it 'responds 403 NOT ALLOWED' do - expect(subject.status) - .to eql 403 + let(:expect_403) do + { message: 'You are not authorized to access this resource.' } end - it 'states a NOT ALLOWED message' do - expected = { - message: 'You are not authorized to access this resource.' - } - - expect(subject.body) - .to be_json_eql(expected.to_json) - end - - it 'is has a json content type header' do - expect(subject.headers['Content-Type']) - .to eql 'application/json; charset=utf-8' + it 'responds 403 NOT ALLOWED', :aggregate_failures do + expect(subject.status).to eq 403 + expect(subject.body).to be_json_eql(expect_403.to_json) + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' end end shared_examples_for 'bcf api unprocessable response' do - it 'responds 403 NOT ALLOWED' do - expect(subject.status) - .to eql 422 + let(:expect_422) do + { message: message } end - it 'states a reason message' do - expected = { - message: message - } - - expect(subject.body) - .to be_json_eql(expected.to_json) - end - - it 'is has a json content type header' do - expect(subject.headers['Content-Type']) - .to eql 'application/json; charset=utf-8' + it 'responds 422 UNPROCESSABLE ENTITY', :aggregate_failures do + expect(subject.status).to eq 422 + expect(subject.body).to be_json_eql(expect_422.to_json) + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' end end diff --git a/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb b/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb index 5d79bdf1c2..f949708d8d 100644 --- a/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb +++ b/modules/bcf/spec/requests/api/bcf/v2_1/topics_api_spec.rb @@ -38,22 +38,32 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma let(:view_only_user) do FactoryBot.create(:user, member_in_project: project, - member_with_permissions: [:view_linked_issues]) + member_with_permissions: %i[view_linked_issues view_work_packages]) end let(:only_member_user) do FactoryBot.create(:user, member_in_project: project, member_with_permissions: []) end + let(:edit_member_user) do + FactoryBot.create(:user, + member_in_project: project, + member_with_permissions: %i[manage_bcf add_work_packages view_linked_issues]) + end let(:non_member_user) do FactoryBot.create(:user) end let(:project) do FactoryBot.create(:project, - enabled_module_names: [:bcf]) + enabled_module_names: %i[bcf work_package_tracking]) + end + let(:assignee) { FactoryBot.create(:user) } + let(:work_package) do + FactoryBot.create(:work_package, + assigned_to: assignee, + project: project) end - let(:work_package) { FactoryBot.create(:work_package, project: project) } let(:bcf_issue) { FactoryBot.create(:bcf_issue, work_package: work_package) } subject(:response) { last_response } @@ -72,26 +82,24 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma let(:expected_body) do [ { - "assigned_to": "andy@example.com", - "creation_author": "mike@example.com", - "creation_date": "2015-06-21T12:00:00Z", - "description": "This is a topic with all information present.", + "assigned_to": assignee.mail, + "creation_author": work_package.author.mail, + "creation_date": work_package.created_at.iso8601, + "description": work_package.description, "due_date": nil, - guid: bcf_issue.uuid, - "index": "0", - "labels": [ - "Structural", - "IT Development" - ], - "modified_author": "michelle@example.com", - "modified_date": "2015-06-21T14:22:47Z", + "guid": bcf_issue.uuid, + "index": bcf_issue.index, + "labels": bcf_issue.labels, + "priority": work_package.priority.name, + "modified_author": current_user.mail, + "modified_date": work_package.updated_at.iso8601, "reference_links": [ api_v3_paths.work_package(work_package.id) ], - "stage": "Construction start", - "title": "Maximum Content", - "topic_status": "Open", - "topic_type": "Structural" + "stage": bcf_issue.stage, + "title": work_package.subject, + "topic_status": work_package.status.name, + "topic_type": work_package.type.name } ] end @@ -123,26 +131,24 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma it_behaves_like 'bcf api successful response' do let(:expected_body) do { - "assigned_to": "andy@example.com", - "creation_author": "mike@example.com", - "creation_date": "2015-06-21T12:00:00Z", - "description": "This is a topic with all information present.", + "assigned_to": assignee.mail, + "creation_author": work_package.author.mail, + "creation_date": work_package.created_at.iso8601, + "description": work_package.description, "due_date": nil, - guid: bcf_issue.uuid, - "index": "0", - "labels": [ - "Structural", - "IT Development" - ], - "modified_author": "michelle@example.com", - "modified_date": "2015-06-21T14:22:47Z", + "guid": bcf_issue.uuid, + "index": bcf_issue.index, + "labels": bcf_issue.labels, + "priority": work_package.priority.name, + "modified_author": current_user.mail, + "modified_date": work_package.updated_at.iso8601, "reference_links": [ api_v3_paths.work_package(work_package.id) ], - "stage": "Construction start", - "title": "Maximum Content", - "topic_status": "Open", - "topic_type": "Structural" + "stage": bcf_issue.stage, + "title": work_package.subject, + "topic_status": work_package.status.name, + "topic_type": work_package.type.name } end end @@ -165,4 +171,240 @@ describe 'BCF 2.1 topics resource', type: :request, content_type: :json, with_ma it_behaves_like 'bcf api not allowed response' end end + + describe 'POST /api/bcf/2.1/projects/:project_id/topics' do + let(:path) { "/api/bcf/2.1/projects/#{project.id}/topics" } + let(:current_user) { edit_member_user } + let(:type) do + FactoryBot.create(:type).tap do |t| + project.types << t + end + end + let(:status) do + FactoryBot.create(:status) + end + let!(:default_status) do + FactoryBot.create(:default_status) + end + let!(:default_type) do + FactoryBot.create(:type, is_default: true) + end + let!(:standard_type) do + FactoryBot.create(:type_standard) + end + let!(:priority) do + FactoryBot.create(:priority) + end + let!(:default_priority) do + FactoryBot.create(:default_priority) + end + let(:description) { 'some description' } + let(:stage) { nil } + let(:labels) { [] } + let(:index) { 5 } + let(:params) do + { + topic_type: type.name, + topic_status: status.name, + priority: priority.name, + title: 'BCF topic 101', + labels: labels, + stage: stage, + index: index, + due_date: Date.today.iso8601, + assigned_to: view_only_user.mail, + description: description + } + end + + before do + login_as(current_user) + post path, params.to_json + end + + it_behaves_like 'bcf api successful response' do + let(:expected_status) { 201 } + let(:expected_body) do + issue = Bcf::Issue.last + work_package = WorkPackage.last + + { + guid: issue&.uuid, + topic_type: type.name, + topic_status: status.name, + priority: priority.name, + title: 'BCF topic 101', + labels: labels, + index: index, + reference_links: [ + api_v3_paths.work_package(work_package&.id) + ], + assigned_to: view_only_user.mail, + due_date: Date.today.iso8601, + stage: stage, + creation_author: edit_member_user.mail, + creation_date: work_package&.created_at&.iso8601, + modified_author: edit_member_user.mail, + modified_date: work_package&.updated_at&.iso8601, + description: description + } + end + end + + context 'with minimal parameters' do + let(:params) do + { + title: 'BCF topic 101' + } + end + + it_behaves_like 'bcf api successful response' do + let(:expected_status) { 201 } + let(:expected_body) do + issue = Bcf::Issue.last + work_package = WorkPackage.last + + { + guid: issue&.uuid, + topic_type: standard_type.name, + topic_status: default_status.name, + priority: default_priority.name, + title: 'BCF topic 101', + labels: [], + index: nil, + reference_links: [ + api_v3_paths.work_package(work_package&.id) + ], + assigned_to: nil, + due_date: nil, + stage: nil, + creation_author: edit_member_user.mail, + creation_date: work_package&.created_at&.iso8601, + modified_author: edit_member_user.mail, + modified_date: work_package&.updated_at&.iso8601, + description: nil + } + end + end + end + + context 'without a title' do + let(:params) do + { + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Title can't be blank." + end + end + end + + context 'with an inexistent status' do + let(:params) do + { + title: 'Some title', + topic_status: 'Some non existing status' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Status does not exist." + end + end + end + + context 'with an inexistent priority' do + let(:params) do + { + title: 'Some title', + priority: 'Some non existing priority' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Priority does not exist." + end + end + end + + context 'with an inexistent type' do + let(:params) do + { + title: 'Some title', + topic_type: 'Some non existing type' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Type does not exist." + end + end + end + + context 'with an inexistent assigned_to' do + let(:params) do + { + title: 'Some title', + assigned_to: 'Some non existing assignee' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Assignee does not exist." + end + end + end + + context 'with two inexistent related resources' do + let(:params) do + { + title: 'Some title', + assigned_to: 'Some non existing assignee', + topic_type: 'Some non existing type' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Multiple field constraints have been violated. Type does not exist. Assignee does not exist." + end + end + end + + context 'with a label' do + let(:params) do + { + title: 'Some title', + labels: ['some label'] + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Labels was attempted to be written but is not writable." + end + end + end + + context 'with a stage' do + let(:params) do + { + title: 'Some title', + stage: 'some stage' + } + end + + it_behaves_like 'bcf api unprocessable response' do + let(:message) do + "Stage was attempted to be written but is not writable." + end + end + end + end end diff --git a/modules/bcf/spec/services/bcf/issues/create_service_spec.rb b/modules/bcf/spec/services/bcf/issues/create_service_spec.rb new file mode 100644 index 0000000000..c30f707943 --- /dev/null +++ b/modules/bcf/spec/services/bcf/issues/create_service_spec.rb @@ -0,0 +1,173 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe Bcf::Issues::CreateService, type: :model do + let(:user) { FactoryBot.build_stubbed(:user) } + let(:contract_class) do + double('contract_class', '<=': true) + end + let(:issue_valid) { true } + let(:instance) do + described_class.new(user: user, + contract_class: contract_class) + end + let(:call_attributes) { { subject: 'Some name' } } + let(:set_attributes_success) do + true + end + let(:set_attributes_errors) do + double('set_attributes_errors') + end + let(:set_attributes_result) do + ServiceResult.new result: created_issue, + success: set_attributes_success, + errors: set_attributes_errors + end + let!(:created_work_package) do + FactoryBot.build_stubbed(:work_package) + end + let(:wp_create_errors) do + double('wp_create_errors') + end + let(:wp_create_result) do + ServiceResult.new result: created_work_package, + success: true, + errors: wp_create_errors + end + let!(:wp_create_service) do + wp_service = double('wp create service') + + allow(WorkPackages::CreateService) + .to receive(:new) + .with(user: user) + .and_return(wp_service) + + allow(wp_service) + .to receive(:call) + .and_return(wp_create_result) + + wp_service + end + let!(:created_issue) do + issue = FactoryBot.build_stubbed(:bcf_issue) + + allow(Bcf::Issue) + .to receive(:new) + .and_return(issue) + + allow(issue) + .to receive(:save) + .and_return(issue_valid) + + issue + end + let!(:set_attributes_service) do + service = double('set_attributes_service_instance') + + allow(Bcf::Issues::SetAttributesService) + .to receive(:new) + .with(user: user, + model: created_issue, + contract_class: contract_class) + .and_return(service) + + allow(service) + .to receive(:call) + .and_return(set_attributes_result) + end + + describe '#call' do + subject { instance.call(call_attributes) } + + it 'is successful' do + expect(subject.success?).to be_truthy + end + + it 'returns the result of the SetAttributesService' do + expect(subject) + .to eql set_attributes_result + end + + it 'persists the issue' do + expect(created_issue) + .to receive(:save) + .and_return(issue_valid) + + subject + end + + it 'creates a issue' do + expect(subject.result) + .to eql created_issue + end + + context 'if the SetAttributeService is unsuccessful' do + let(:set_attributes_success) { false } + + it 'is unsuccessful' do + expect(subject.success?).to be_falsey + end + + it 'returns the result of the SetAttributesService' do + expect(subject) + .to eql set_attributes_result + end + + it 'does not persist the changes' do + expect(created_issue) + .to_not receive(:save) + + subject + end + + it "exposes the contract's errors" do + subject + + expect(subject.errors).to eql set_attributes_errors + end + end + + context 'when the issue is invalid' do + let(:issue_valid) { false } + + it 'is unsuccessful' do + expect(subject.success?).to be_falsey + end + + it "exposes the issue's errors" do + subject + + expect(subject.errors).to eql created_issue.errors + end + end + end +end diff --git a/modules/boards/spec/requests/api/v3/grids/grids_resource_spec.rb b/modules/boards/spec/requests/api/v3/grids/grids_resource_spec.rb index c453a2e7a1..1d9d0567e0 100644 --- a/modules/boards/spec/requests/api/v3/grids/grids_resource_spec.rb +++ b/modules/boards/spec/requests/api/v3/grids/grids_resource_spec.rb @@ -288,21 +288,7 @@ describe 'API v3 Grids resource for Board Grids', type: :request, content_type: }.with_indifferent_access end - it 'responds with 422 and mentions the error' do - expect(subject.status).to eq 422 - - expect(subject.body) - .to be_json_eql('Error'.to_json) - .at_path('_type') - - expect(subject.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) - .at_path('message') - - expect(subject.body) - .to be_json_eql("scope".to_json) - .at_path('_embedded/details/attribute') - end + it_behaves_like 'read-only violation', 'scope', Boards::Grid end context 'with the grid not existing' do diff --git a/modules/boards/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb b/modules/boards/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb index acb3870450..52d9d576ef 100644 --- a/modules/boards/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb +++ b/modules/boards/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb @@ -121,7 +121,7 @@ describe "PATCH /api/v3/grids/:id/form for Board Grids", type: :request, content it 'has a validation error on scope as the value is not writeable' do expect(subject.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) + .to be_json_eql("Scope was attempted to be written but is not writable.".to_json) .at_path('_embedded/validationErrors/scope/message') end end diff --git a/modules/dashboards/config/locales/crowdin/tr.yml b/modules/dashboards/config/locales/crowdin/tr.yml index c4263fcf8e..20c317720b 100644 --- a/modules/dashboards/config/locales/crowdin/tr.yml +++ b/modules/dashboards/config/locales/crowdin/tr.yml @@ -1,5 +1,5 @@ tr: dashboards: - label: 'Gösterge tabloları' + label: 'Gösterge panosu' menu_badge: 'Alfa' - project_module_dashboards: 'Gösterge tabloları' + project_module_dashboards: 'Gösterge panosu' diff --git a/modules/grids/app/representers/api/v3/grids/schemas/grid_schema_representer.rb b/modules/grids/app/representers/api/v3/grids/schemas/grid_schema_representer.rb index d7af33537f..2bc14f592e 100644 --- a/modules/grids/app/representers/api/v3/grids/schemas/grid_schema_representer.rb +++ b/modules/grids/app/representers/api/v3/grids/schemas/grid_schema_representer.rb @@ -41,38 +41,30 @@ module API end schema :id, - type: 'Integer', - visibility: false + type: 'Integer' schema :created_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :updated_at, - type: 'DateTime', - visibility: false + type: 'DateTime' schema :row_count, - type: 'Integer', - visibility: false + type: 'Integer' schema :column_count, - type: 'Integer', - visibility: false + type: 'Integer' schema :name, - type: 'String', - visibility: false + type: 'String' schema :options, - type: 'JSON', - visibility: false + type: 'JSON' schema_with_allowed_collection :scope, type: 'Href', required: true, has_default: false, - visibility: false, value_representer: false, link_factory: ->(path) { { @@ -84,7 +76,6 @@ module API type: '[]GridWidget', required: true, has_default: false, - visibility: false, values_callback: -> do represented.assignable_widgets.map do |identifier| OpenStruct.new(identifier: identifier, grid: represented.model, options: {}) diff --git a/modules/grids/spec/lib/api/v3/grids/schemas/grid_schema_representer_spec.rb b/modules/grids/spec/lib/api/v3/grids/schemas/grid_schema_representer_spec.rb index 98344482cc..e4c2f7e232 100644 --- a/modules/grids/spec/lib/api/v3/grids/schemas/grid_schema_representer_spec.rb +++ b/modules/grids/spec/lib/api/v3/grids/schemas/grid_schema_representer_spec.rb @@ -94,8 +94,6 @@ describe ::API::V3::Grids::Schemas::GridSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'rowCount' do @@ -107,8 +105,6 @@ describe ::API::V3::Grids::Schemas::GridSchemaRepresenter do let(:required) { true } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'columnCount' do @@ -120,8 +116,6 @@ describe ::API::V3::Grids::Schemas::GridSchemaRepresenter do let(:required) { true } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'createdAt' do @@ -133,8 +127,6 @@ describe ::API::V3::Grids::Schemas::GridSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'updatedAt' do @@ -146,8 +138,6 @@ describe ::API::V3::Grids::Schemas::GridSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'widgets' do diff --git a/modules/my_page/spec/requests/api/v3/grids/grids_resource_spec.rb b/modules/my_page/spec/requests/api/v3/grids/grids_resource_spec.rb index 959e55b5a8..5948901d06 100644 --- a/modules/my_page/spec/requests/api/v3/grids/grids_resource_spec.rb +++ b/modules/my_page/spec/requests/api/v3/grids/grids_resource_spec.rb @@ -277,21 +277,7 @@ describe 'API v3 Grids resource', type: :request, content_type: :json do }.with_indifferent_access end - it 'responds with 422 and mentions the error' do - expect(subject.status).to eq 422 - - expect(subject.body) - .to be_json_eql('Error'.to_json) - .at_path('_type') - - expect(subject.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) - .at_path('message') - - expect(subject.body) - .to be_json_eql("scope".to_json) - .at_path('_embedded/details/attribute') - end + it_behaves_like 'read-only violation', 'scope', Grids::Grid end context 'with the page not existing' do diff --git a/modules/my_page/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb b/modules/my_page/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb index 7311008a47..28a6128735 100644 --- a/modules/my_page/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb +++ b/modules/my_page/spec/requests/api/v3/grids/grids_update_form_resource_spec.rb @@ -132,7 +132,7 @@ describe "PATCH /api/v3/grids/:id/form", type: :request, content_type: :json do it 'has a validation error on scope as the value is not writeable' do expect(subject.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) + .to be_json_eql("Scope was attempted to be written but is not writable.".to_json) .at_path('_embedded/validationErrors/scope/message') end end diff --git a/spec/contracts/work_packages/base_contract_spec.rb b/spec/contracts/work_packages/base_contract_spec.rb index 8c5e010034..a5c100f7e6 100644 --- a/spec/contracts/work_packages/base_contract_spec.rb +++ b/spec/contracts/work_packages/base_contract_spec.rb @@ -174,7 +174,7 @@ describe WorkPackages::BaseContract do before do allow(work_package) .to receive(:status_id_change) - .and_return [1,2] + .and_return [1, 2] end it 'is writable' do @@ -182,6 +182,19 @@ describe WorkPackages::BaseContract do end end end + + context 'is an inexistent status' do + before do + work_package.status = Status::InexistentStatus.new + end + + it 'is invalid' do + contract.validate + + expect(subject.errors.symbols_for(:status)) + .to match_array [:does_not_exist] + end + end end describe 'estimated hours' do @@ -521,6 +534,34 @@ describe WorkPackages::BaseContract do end end end + + context 'inexistent type' do + before do + work_package.type = Type::InexistentType.new + + contract.validate + end + + it 'is invalid' do + expect(contract.errors.symbols_for(:type)) + .to match_array [:does_not_exist] + end + end + end + + context 'assigned_to' do + context 'inexistent user' do + before do + work_package.assigned_to = User::InexistentUser.new + + contract.validate + end + + it 'is invalid' do + expect(contract.errors.symbols_for(:assigned_to)) + .to match_array [:does_not_exist] + end + end end describe 'category' do @@ -630,6 +671,19 @@ describe WorkPackages::BaseContract do .to be_empty end end + + context 'inexistent priority' do + before do + work_package.priority = Priority::InexistentPriority.new + + contract.validate + end + + it 'is invalid' do + expect(contract.errors.symbols_for(:priority)) + .to match_array [:does_not_exist] + end + end end describe 'status' do diff --git a/spec/features/work_packages/display_representations/switch_display_representations_spec.rb b/spec/features/work_packages/display_representations/switch_display_representations_spec.rb index f807531c4f..6c7a14da39 100644 --- a/spec/features/work_packages/display_representations/switch_display_representations_spec.rb +++ b/spec/features/work_packages/display_representations/switch_display_representations_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' -describe 'Work package timeline navigation', +describe 'Switching work package view', with_ee: %i[conditional_highlighting], js: true do let(:user) { FactoryBot.create(:admin) } @@ -69,8 +69,7 @@ describe 'Work package timeline navigation', before do # Enable card representation display_representation.switch_to_card_layout - expect(page).to have_selector("wp-single-card[data-work-package-id='#{wp_1.id}']") - expect(page).to have_selector("wp-single-card[data-work-package-id='#{wp_2.id}']") + cards.expect_work_package_listed wp_1, wp_2 end it 'can switch the representations and keep the configuration settings' do @@ -107,8 +106,48 @@ describe 'Work package timeline navigation', it 'saves the representation in the query' do # After refresh the WP are still disaplyed as cards page.driver.browser.navigate.refresh - expect(page).to have_selector("wp-single-card[data-work-package-id='#{wp_1.id}']") - expect(page).to have_selector("wp-single-card[data-work-package-id='#{wp_2.id}']") + cards.expect_work_package_listed wp_1, wp_2 + end + end + + context 'switching to mobile card view' do + let!(:height_before) do + page.driver.browser.manage.window.size.height + end + let!(:width_before) do + page.driver.browser.manage.window.size.width + end + + after do + page.driver.browser.manage.window.resize_to(width_before, height_before) + end + + it 'can switch the representation automatically on mobile after a refresh' do + # Change browser size to mobile + page.driver.browser.manage.window.resize_to(679, 1080) + + # Expect the representation to switch to card on mobile + page.driver.browser.navigate.refresh + + # It shows the elements as cards + cards.expect_work_package_listed wp_1, wp_2 + + # A single click leads to the full view + cards.select_work_package(wp_1) + expect(page).to have_selector('.work-packages--details--subject', + text: wp_1.subject) + page.find('.work-packages-back-button').click + + # The query is however unchanged + expect(page).not_to have_selector('.editable-toolbar-title--save') + url = URI.parse(page.current_url).query + expect(url).not_to match(/query_props=.+/) + + # Since the query is unchanged, the WPs will be displayed as list on larger screens again + page.driver.browser.manage.window.resize_to(680, 1080) + page.driver.browser.navigate.refresh + wp_table.expect_work_package_listed wp_1, wp_2 + wp_table.expect_work_package_order wp_1, wp_2 end end diff --git a/spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb b/spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb index 60f28ac8bd..bda4882cbf 100644 --- a/spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb +++ b/spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb @@ -96,8 +96,6 @@ describe ::API::V3::Memberships::Schemas::MembershipSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'createdAt' do @@ -109,8 +107,6 @@ describe ::API::V3::Memberships::Schemas::MembershipSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'project' do diff --git a/spec/lib/api/v3/projects/schemas/project_schema_representer_spec.rb b/spec/lib/api/v3/projects/schemas/project_schema_representer_spec.rb index 12928d33bb..bd6a101a87 100644 --- a/spec/lib/api/v3/projects/schemas/project_schema_representer_spec.rb +++ b/spec/lib/api/v3/projects/schemas/project_schema_representer_spec.rb @@ -100,8 +100,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'name' do @@ -118,8 +116,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:min_length) { 1 } let(:max_length) { 255 } end - - it_behaves_like 'has no visibility property' end describe 'identifier' do @@ -136,8 +132,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:min_length) { 1 } let(:max_length) { 100 } end - - it_behaves_like 'has no visibility property' end describe 'description' do @@ -149,8 +143,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { false } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'public' do @@ -162,8 +154,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { true } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'active' do @@ -175,8 +165,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { true } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'statusExplanation' do @@ -222,8 +210,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'updatedAt' do @@ -235,8 +221,6 @@ describe ::API::V3::Projects::Schemas::ProjectSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'int custom field' do diff --git a/spec/lib/api/v3/queries/schemas/query_filter_instance_schema_representer_spec.rb b/spec/lib/api/v3/queries/schemas/query_filter_instance_schema_representer_spec.rb index bf5f6bb7fc..d106f086b5 100644 --- a/spec/lib/api/v3/queries/schemas/query_filter_instance_schema_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/query_filter_instance_schema_representer_spec.rb @@ -105,8 +105,6 @@ describe ::API::V3::Queries::Schemas::QueryFilterInstanceSchemaRepresenter, clea let(:writable) { false } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'filter' do @@ -119,8 +117,6 @@ describe ::API::V3::Queries::Schemas::QueryFilterInstanceSchemaRepresenter, clea let(:writable) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -150,8 +146,6 @@ describe ::API::V3::Queries::Schemas::QueryFilterInstanceSchemaRepresenter, clea let(:writable) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do diff --git a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb index 310d6b887b..3861be64b0 100644 --- a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb @@ -112,8 +112,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'name' do @@ -130,8 +128,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:min_length) { 1 } let(:max_length) { 255 } end - - it_behaves_like 'has no visibility property' end describe 'createdAt' do @@ -143,8 +139,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'updatedAt' do @@ -156,8 +150,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'user' do @@ -170,8 +162,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { false } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'project' do @@ -184,8 +174,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -208,8 +196,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:has_default) { true } end - it_behaves_like 'has no visibility property' - context 'when having the :manage_public_queries permission' do before do allow(user) @@ -248,8 +234,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'timelineVisible' do @@ -262,8 +246,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'timelineZoomLevel' do @@ -276,8 +258,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'timelineLabels' do @@ -290,8 +270,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'show hierarchies' do @@ -304,8 +282,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'starred' do @@ -318,8 +294,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { false } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'highlighting_mode' do @@ -332,8 +306,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'display_representation' do @@ -346,8 +318,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } let(:has_default) { true } end - - it_behaves_like 'has no visibility property' end describe 'columns' do @@ -361,8 +331,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:has_default) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -441,8 +409,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:has_default) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when global query' do @@ -477,8 +443,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:writable) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -511,8 +475,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:has_default) { true } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -553,8 +515,6 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:required) { false } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end end diff --git a/spec/lib/api/v3/support/api_v3_filter_dependency.rb b/spec/lib/api/v3/support/api_v3_filter_dependency.rb index 7ae6f87f75..2bffd0ab59 100644 --- a/spec/lib/api/v3/support/api_v3_filter_dependency.rb +++ b/spec/lib/api/v3/support/api_v3_filter_dependency.rb @@ -34,8 +34,6 @@ shared_examples_for 'filter dependency' do let(:has_default) { false } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -53,8 +51,6 @@ shared_examples_for 'filter dependency with allowed link' do let(:has_default) { false } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do @@ -72,8 +68,6 @@ shared_examples_for 'filter dependency with allowed value link collection' do let(:has_default) { false } end - it_behaves_like 'has no visibility property' - it_behaves_like 'does not link to allowed values' context 'when embedding' do diff --git a/spec/lib/api/v3/support/schema_examples.rb b/spec/lib/api/v3/support/schema_examples.rb index 4eefa4ca47..04d01e1913 100644 --- a/spec/lib/api/v3/support/schema_examples.rb +++ b/spec/lib/api/v3/support/schema_examples.rb @@ -86,13 +86,6 @@ shared_examples_for 'indicates length requirements' do end end -shared_examples_for 'has no visibility property' do - it 'has no path' do - is_expected - .not_to have_json_path("#{path}/visibility") - end -end - shared_examples_for 'links to allowed values directly' do it 'has the expected number of links' do is_expected.to have_json_size(hrefs.size).at_path("#{path}/_links/allowedValues") diff --git a/spec/lib/api/v3/versions/schemas/version_schema_representer_spec.rb b/spec/lib/api/v3/versions/schemas/version_schema_representer_spec.rb index 63c241de21..68be2ec027 100644 --- a/spec/lib/api/v3/versions/schemas/version_schema_representer_spec.rb +++ b/spec/lib/api/v3/versions/schemas/version_schema_representer_spec.rb @@ -98,8 +98,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'createdAt' do @@ -111,8 +109,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'updatedAt' do @@ -124,8 +120,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { true } let(:writable) { false } end - - it_behaves_like 'has no visibility property' end describe 'name' do @@ -142,8 +136,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:min_length) { 1 } let(:max_length) { 60 } end - - it_behaves_like 'has no visibility property' end describe 'description' do @@ -155,8 +147,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { false } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'int custom field' do @@ -179,8 +169,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { false } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'endDate' do @@ -192,8 +180,6 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do let(:required) { false } let(:writable) { true } end - - it_behaves_like 'has no visibility property' end describe 'definingProject' do diff --git a/spec/lib/api/v3/work_packages/schema/work_package_sums_schema_representer_spec.rb b/spec/lib/api/v3/work_packages/schema/work_package_sums_schema_representer_spec.rb index b76d2185c1..6fa4013e9e 100644 --- a/spec/lib/api/v3/work_packages/schema/work_package_sums_schema_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/schema/work_package_sums_schema_representer_spec.rb @@ -65,10 +65,10 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do it 'is represented' do expected = { 'type': 'Duration', 'name': 'Estimated time', - 'visibility': 'default', 'required': false, 'hasDefault': false, - 'writable': false } + 'writable': false, + 'options': {} } expect(subject).to be_json_eql(expected.to_json).at_path('estimatedTime') end end @@ -90,10 +90,10 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSumsSchemaRepresenter do it 'is represented' do expected = { 'type': 'Integer', 'name': custom_field.name, - 'visibility': 'default', 'required': false, 'hasDefault': false, - 'writable': false } + 'writable': false, + 'options': {} } expect(subject).to be_json_eql(expected.to_json).at_path("customField#{custom_field.id}") end end diff --git a/spec/models/mail_handler_spec.rb b/spec/models/mail_handler_spec.rb index 74724e2399..9855bef4f4 100644 --- a/spec/models/mail_handler_spec.rb +++ b/spec/models/mail_handler_spec.rb @@ -276,18 +276,19 @@ describe MailHandler, type: :model do end it 'rejects if unknown_user=accept and permission check is present' do - expected = 'MailHandler: work_package could not be created by Anonymous due to ' \ - '#["may not be accessed.", "Type is not writable.", "Project is not writable.", ' \ - '"Subject is not writable.", "Description is not writable."]' + '#["may not be accessed.", "Type was attempted to be written but is not writable.", ' \ + '"Project was attempted to be written but is not writable.", ' \ + '"Subject was attempted to be written but is not writable.", ' \ + '"Description was attempted to be written but is not writable."]' expect(Rails.logger) - .to(receive(:error)) + .to receive(:error) .with(expected) result = submit_email 'ticket_by_unknown_user.eml', - issue: {project: project.identifier}, + issue: { project: project.identifier }, unknown_user: 'accept' expect(result).to eq false @@ -295,7 +296,7 @@ describe MailHandler, type: :model do it 'accepts if unknown_user=accept and no_permission_check' do work_package = submit_email 'ticket_by_unknown_user.eml', - issue: {project: project.identifier}, + issue: { project: project.identifier }, unknown_user: 'accept', no_permission_check: 1 diff --git a/spec/requests/api/v3/membership_resources_spec.rb b/spec/requests/api/v3/membership_resources_spec.rb index 8308783cc4..100c767cb3 100644 --- a/spec/requests/api/v3/membership_resources_spec.rb +++ b/spec/requests/api/v3/membership_resources_spec.rb @@ -576,14 +576,7 @@ describe 'API v3 memberhips resource', type: :request, content_type: :json do }.to_json end - it 'returns 422' do - expect(last_response.status) - .to eql(422) - - expect(last_response.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) - .at_path('message') - end + it_behaves_like 'read-only violation', 'project', Member end context 'if attempting to switch the principal' do @@ -602,18 +595,7 @@ describe 'API v3 memberhips resource', type: :request, content_type: :json do }.to_json end - it 'returns 422' do - expect(last_response.status) - .to eql(422) - - expect(last_response.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) - .at_path('message') - - expect(last_response.body) - .to be_json_eql("user".to_json) - .at_path('_embedded/details/attribute') - end + it_behaves_like 'read-only violation', 'user', Member end context 'if lacking the manage permissions' do diff --git a/spec/requests/api/v3/relations/relations_api_spec.rb b/spec/requests/api/v3/relations/relations_api_spec.rb index b572c5575b..6e191ce996 100644 --- a/spec/requests/api/v3/relations/relations_api_spec.rb +++ b/spec/requests/api/v3/relations/relations_api_spec.rb @@ -302,7 +302,7 @@ describe 'API v3 Relation resource', type: :request, content_type: :json do it "should let the user know the attribute is read-only" do msg = JSON.parse(last_response.body)["message"] - expect(msg).to include 'read-only' + expect(msg).to include "Work package an existing relation's `from` link is immutable" end end end diff --git a/spec/requests/api/v3/support/response_examples.rb b/spec/requests/api/v3/support/response_examples.rb index ea7026db44..d01b13c1f3 100644 --- a/spec/requests/api/v3/support/response_examples.rb +++ b/spec/requests/api/v3/support/response_examples.rb @@ -150,7 +150,7 @@ shared_examples_for 'format error' do |message| message end -shared_examples_for 'read-only violation' do |attribute| +shared_examples_for 'read-only violation' do |attribute, model, attribute_message = nil| describe 'details' do subject { JSON.parse(last_response.body)['_embedded']['details'] } @@ -160,7 +160,7 @@ shared_examples_for 'read-only violation' do |attribute| it_behaves_like 'error response', 422, 'PropertyIsReadOnly', - I18n.t('api_v3.errors.writing_read_only_attributes') + "#{attribute_message || model.human_attribute_name(attribute)} #{I18n.t('activerecord.errors.messages.error_readonly')}" end shared_examples_for 'multiple errors' do |code, _message| diff --git a/spec/requests/api/v3/version_resource_spec.rb b/spec/requests/api/v3/version_resource_spec.rb index 541db82608..9a8f021ad6 100644 --- a/spec/requests/api/v3/version_resource_spec.rb +++ b/spec/requests/api/v3/version_resource_spec.rb @@ -236,14 +236,7 @@ describe 'API v3 Version resource', content_type: :json do }.to_json end - it 'returns 422' do - expect(last_response.status) - .to eql(422) - - expect(last_response.body) - .to be_json_eql("You must not write a read-only attribute.".to_json) - .at_path('message') - end + it_behaves_like 'read-only violation', 'project', Version end context 'if lacking the manage permissions' do diff --git a/spec/requests/api/v3/work_package_resource_spec.rb b/spec/requests/api/v3/work_package_resource_spec.rb index 2532f6a5db..69a5b4303b 100644 --- a/spec/requests/api/v3/work_package_resource_spec.rb +++ b/spec/requests/api/v3/work_package_resource_spec.rb @@ -856,13 +856,13 @@ describe 'API v3 Work package resource', context 'created_at' do let(:params) { valid_params.merge(createdAt: tomorrow) } - it_behaves_like 'read-only violation', 'createdAt' + it_behaves_like 'read-only violation', 'createdAt', WorkPackage, 'Created on' end context 'updated_at' do let(:params) { valid_params.merge(updatedAt: tomorrow) } - it_behaves_like 'read-only violation', 'updatedAt' + it_behaves_like 'read-only violation', 'updatedAt', WorkPackage, 'Updated on' end end end diff --git a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb index 571cf5be97..fd0a30e9d5 100644 --- a/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/work_packages_schemas_resource_spec.rb @@ -206,10 +206,10 @@ describe API::V3::WorkPackages::Schema::WorkPackageSchemasAPI, type: :request do it 'should return the schema for estimated_hours' do expected = { 'type': 'Duration', 'name': 'Estimated time', - 'visibility': 'default', 'required': false, 'hasDefault': false, - 'writable': false } + 'writable': false, + 'options': {} } expect(subject.body).to be_json_eql(expected.to_json).at_path('estimatedTime') end end diff --git a/spec/services/projects/set_attributes_service_spec.rb b/spec/services/projects/set_attributes_service_spec.rb index 9e1574c6d4..fa51b5ebe9 100644 --- a/spec/services/projects/set_attributes_service_spec.rb +++ b/spec/services/projects/set_attributes_service_spec.rb @@ -69,11 +69,11 @@ describe Projects::SetAttributesService, type: :model do before do allow(project) .to receive(:valid?) - .and_return(project_valid) + .and_return(project_valid) expect(contract_instance) .to receive(:validate) - .and_return(contract_valid) + .and_return(contract_valid) end subject { instance.call(call_attributes) } diff --git a/spec/support/matchers/be_html_eql.rb b/spec/support/matchers/be_html_eql.rb index b8004b8c6d..f0d4af5c98 100644 --- a/spec/support/matchers/be_html_eql.rb +++ b/spec/support/matchers/be_html_eql.rb @@ -56,11 +56,11 @@ @path = path + ' > *' end - should_message = -> (actual) do + should_message = ->(actual) do ['expected:', expected.to_s, 'got:', actual.to_s].join("\n") end - should_not_message = -> (actual) do + should_not_message = ->(actual) do ['expected:', actual.to_s, 'not to be equivalent to:', expected.to_s].join("\n") end diff --git a/spec/support/pages/work_packages/work_package_cards.rb b/spec/support/pages/work_packages/work_package_cards.rb index fcd18d1a90..cdb5e95978 100644 --- a/spec/support/pages/work_packages/work_package_cards.rb +++ b/spec/support/pages/work_packages/work_package_cards.rb @@ -37,6 +37,12 @@ module Pages @project = project end + def expect_work_package_listed(*work_packages) + work_packages.each do |wp| + expect(page).to have_selector("wp-single-card[data-work-package-id='#{wp.id}']") + end + end + def expect_work_package_order(*ids) retry_block do rows = page.all 'wp-single-card'