)]
+ def get_descendants(item:, include_self: true)
+ if include_self
+ Success(item.self_and_descendants)
+ else
+ Success(item.descendants)
+ end
+ end
+
# Move an item/node to a new parent item/node
# @param item [CustomField::Hierarchy::Item] the parent of the node
# @param new_parent [CustomField::Hierarchy::Item] the new parent of the node
diff --git a/app/views/admin/settings/project_custom_fields/new.html.erb b/app/views/admin/settings/project_custom_fields/new.html.erb
index 1d1b501fd099..f95642bc2048 100644
--- a/app/views/admin/settings/project_custom_fields/new.html.erb
+++ b/app/views/admin/settings/project_custom_fields/new.html.erb
@@ -34,6 +34,11 @@ See COPYRIGHT and LICENSE files for more details.
<%= error_messages_for 'custom_field' %>
+<% content_controller "admin--custom-fields",
+ dynamic: true,
+ 'admin--custom-fields-format-config-value': OpenProject::CustomFieldFormatDependent.stimulus_config
+%>
+
<%= labelled_tabular_form_for @custom_field, as: :custom_field,
url: admin_settings_project_custom_fields_path,
html: { id: 'custom_field_form' } do |f| %>
diff --git a/app/views/custom_fields/_custom_options.html.erb b/app/views/custom_fields/_custom_options.html.erb
index f20f6934f719..3f8b60b1f70d 100644
--- a/app/views/custom_fields/_custom_options.html.erb
+++ b/app/views/custom_fields/_custom_options.html.erb
@@ -86,13 +86,17 @@ See COPYRIGHT and LICENSE files for more details.
>
- <%= co_f.hidden_field :id, class: 'custom-option-id' %>
+ <%= co_f.hidden_field :id,
+ disabled:true,
+ class: 'custom-option-id' %>
<%= co_f.text_field :value,
+ disabled: true,
container_class: 'custom-option-value',
no_label: true %>
|
<%= co_f.check_box :default_value,
+ disabled: true,
container_class: 'custom-option-default-value',
data: {
'admin--custom-fields-target': 'customOptionDefaults',
diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb
index b85a490efcdd..aa06e6d44711 100644
--- a/app/views/custom_fields/_form.html.erb
+++ b/app/views/custom_fields/_form.html.erb
@@ -32,9 +32,6 @@ See COPYRIGHT and LICENSE files for more details.
|
diff --git a/modules/costs/app/views/costlog/edit.html.erb b/modules/costs/app/views/costlog/edit.html.erb
index 8b41038d4e55..32a26bcab409 100644
--- a/modules/costs/app/views/costlog/edit.html.erb
+++ b/modules/costs/app/views/costlog/edit.html.erb
@@ -132,7 +132,7 @@ See COPYRIGHT and LICENSE files for more details.
value: @cost_entry.overridden_costs ? unitless_currency_number(@cost_entry.overridden_costs).strip : '',
placeholder: t(:label_example_placeholder, decimal: unitless_currency_number(1000.50)),
no_label: true,
- suffix: Setting.plugin_costs['costs_currency'],
+ suffix: Setting.costs_currency,
id: 'cost_entry_cost_value',
container_class: '-middle',
size: 7 %>
diff --git a/modules/costs/app/views/costs_settings/show.html.erb b/modules/costs/app/views/costs_settings/show.html.erb
new file mode 100644
index 000000000000..1f4be12f6635
--- /dev/null
+++ b/modules/costs/app/views/costs_settings/show.html.erb
@@ -0,0 +1,68 @@
+<%# -- copyright
+OpenProject is an open source project management software.
+Copyright (C) the OpenProject GmbH
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See COPYRIGHT and LICENSE files for more details.
+
+++#%>
+
+<% html_title t(:label_administration), t("plugin_costs.name") %>
+
+<%=
+ render(Primer::OpenProject::PageHeader.new) do |header|
+ header.with_title { t("plugin_costs.name") }
+ header.with_breadcrumbs([{ href: admin_index_path, text: t("label_administration") },
+ t("plugin_costs.name")])
+ end
+%>
+
+<%= styled_form_tag(
+ admin_costs_settings_path,
+ method: :patch,
+ class: 'op-costs-admin-settings'
+ ) do %>
+
+
+
+
+ <%= styled_button_tag t(:button_save), class: '-primary -with-icon icon-checkmark' %>
+<% end %>
diff --git a/modules/costs/app/views/hourly_rates/_rate.html.erb b/modules/costs/app/views/hourly_rates/_rate.html.erb
index fa6f9ab67c9e..63f6755b8868 100644
--- a/modules/costs/app/views/hourly_rates/_rate.html.erb
+++ b/modules/costs/app/views/hourly_rates/_rate.html.erb
@@ -61,7 +61,7 @@ See COPYRIGHT and LICENSE files for more details.
value: rate.rate ? unitless_currency_number(rate.rate.round(2)) : "",
required: true %>
- <%= Setting.plugin_costs['costs_currency'] %>
+ <%= Setting.costs_currency %>
diff --git a/modules/costs/app/views/settings/_costs.html.erb b/modules/costs/app/views/settings/_costs.html.erb
deleted file mode 100644
index d1683e430c7d..000000000000
--- a/modules/costs/app/views/settings/_costs.html.erb
+++ /dev/null
@@ -1,43 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-
-
-
-
diff --git a/modules/costs/config/locales/crowdin/af.yml b/modules/costs/config/locales/crowdin/af.yml
index c4a3658e8310..697b1dc9ccc9 100644
--- a/modules/costs/config/locales/crowdin/af.yml
+++ b/modules/costs/config/locales/crowdin/af.yml
@@ -85,8 +85,6 @@ af:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ af:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ af:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/ar.yml b/modules/costs/config/locales/crowdin/ar.yml
index a9ac1ee9cede..b50b099182d4 100644
--- a/modules/costs/config/locales/crowdin/ar.yml
+++ b/modules/costs/config/locales/crowdin/ar.yml
@@ -89,8 +89,6 @@ ar:
label_cost_type_plural: "أنواع التكلفة"
label_cost_type_specific: "نوع التكلفة #%{id}:%{name}"
label_costs_per_page: "التكاليف للصفحة"
- label_currency: "العملة"
- label_currency_format: "شكل العملة"
label_current_default_rate: "المعدل الافتراضي الحالي"
label_date_on: "على"
label_deleted_cost_types: "أنواع التكلفة المحذوفة"
@@ -124,7 +122,7 @@ ar:
notice_something_wrong: "حصل خطأ ما. من فضلك حاول مرة أخرى."
notice_successful_restore: "استعادة ناجحة."
notice_successful_lock: "أغلِقت بنجاح."
- notice_cost_logged_successfully: 'تمّ تسجيل تكلفة الوحدة بنجاح.'
+ notice_cost_logged_successfully: "تمّ تسجيل تكلفة الوحدة بنجاح."
permission_edit_cost_entries: "تحرير تكاليف الوحدة المحجوزة"
permission_edit_own_cost_entries: "تحرير تكاليف الوحدة المحجوزة الخاصة"
permission_edit_hourly_rates: "تحرير الأجور الساعيّة"
@@ -139,6 +137,10 @@ ar:
permission_view_own_hourly_rate: "عرض الأجور الساعيّة الخاصة"
permission_view_own_time_entries: "عرض الوقت الخاص الذي تم قضاؤه"
project_module_costs: "الوقت والتكاليف"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "تعيين الساعات والتكاليف التي تمّ الإبلاغ عنها للمشروع"
text_destroy_cost_entries_question: "%{cost_entries} تم الإبلاغ عنها في مجموعات العمل التي توشك أن تلغيها. ماذا تريد أن تفعل؟"
text_destroy_time_and_cost_entries: "إلغاء الساعات والتكاليف التي تم الإبلاغ عنها"
diff --git a/modules/costs/config/locales/crowdin/az.yml b/modules/costs/config/locales/crowdin/az.yml
index e4d95603fe9c..c2e9da30db92 100644
--- a/modules/costs/config/locales/crowdin/az.yml
+++ b/modules/costs/config/locales/crowdin/az.yml
@@ -85,8 +85,6 @@ az:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ az:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ az:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/be.yml b/modules/costs/config/locales/crowdin/be.yml
index e14a6e41c24f..e064b0fbf12d 100644
--- a/modules/costs/config/locales/crowdin/be.yml
+++ b/modules/costs/config/locales/crowdin/be.yml
@@ -87,8 +87,6 @@ be:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -122,7 +120,7 @@ be:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -137,6 +135,10 @@ be:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/bg.yml b/modules/costs/config/locales/crowdin/bg.yml
index e9392654613b..69b55b8c90e2 100644
--- a/modules/costs/config/locales/crowdin/bg.yml
+++ b/modules/costs/config/locales/crowdin/bg.yml
@@ -85,8 +85,6 @@ bg:
label_cost_type_plural: "Видове разходи"
label_cost_type_specific: "Тип на разходите #%{id}: %{name}"
label_costs_per_page: "Разходи за страница"
- label_currency: "Валута"
- label_currency_format: "Формат на валутата"
label_current_default_rate: "Текуща ставка по подразбиране"
label_date_on: "на"
label_deleted_cost_types: "Изтрити видове разходи"
@@ -120,7 +118,7 @@ bg:
notice_something_wrong: "Нещо се обърка. Моля, опитайте отново."
notice_successful_restore: "Успешно възстановяване."
notice_successful_lock: "Заключено успешно."
- notice_cost_logged_successfully: 'Единичната цена е регистрирана успешно.'
+ notice_cost_logged_successfully: "Единичната цена е регистрирана успешно."
permission_edit_cost_entries: "Редактирайте резервираните единични разходи"
permission_edit_own_cost_entries: "Редактирайте собствените резервирани единични разходи"
permission_edit_hourly_rates: "Редактирайте почасовите ставки"
@@ -135,6 +133,10 @@ bg:
permission_view_own_hourly_rate: "Вижте собствената почасова ставка"
permission_view_own_time_entries: "Вижте собственото си отработено време"
project_module_costs: "Време и разходи"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Присвойте отчетени часове и разходи на проекта"
text_destroy_cost_entries_question: "%{cost_entries} бяха отчетени в работните пакети, които ще изтриете. Какво искаш да правиш?"
text_destroy_time_and_cost_entries: "Изтрийте отчетените часове и разходи"
diff --git a/modules/costs/config/locales/crowdin/ca.yml b/modules/costs/config/locales/crowdin/ca.yml
index b83fd44e93a4..2fc6e6c2f3b4 100644
--- a/modules/costs/config/locales/crowdin/ca.yml
+++ b/modules/costs/config/locales/crowdin/ca.yml
@@ -85,8 +85,6 @@ ca:
label_cost_type_plural: "Estils de cost"
label_cost_type_specific: "Estil de cost #%{id}: %{name}"
label_costs_per_page: "Costs per pàgina"
- label_currency: "Divisa"
- label_currency_format: "Format de divisa"
label_current_default_rate: "Tarifa actual per defecte"
label_date_on: "en"
label_deleted_cost_types: "Estils de cost eliminats"
@@ -120,7 +118,7 @@ ca:
notice_something_wrong: "Alguna cosa ha anat malament. Si us plau, prova-ho de nou."
notice_successful_restore: "Restaurat correctament."
notice_successful_lock: "Fixat correctament."
- notice_cost_logged_successfully: 'Cost d''unitat registrat correctament.'
+ notice_cost_logged_successfully: "Cost d'unitat registrat correctament."
permission_edit_cost_entries: "Editar costs d'unitat registrats"
permission_edit_own_cost_entries: "Editar costs d'unitat registrats propis"
permission_edit_hourly_rates: "Editar tarifes per hora"
@@ -135,6 +133,10 @@ ca:
permission_view_own_hourly_rate: "Visualitza totes les tarifes per hora pròpies"
permission_view_own_time_entries: "Visualitzar el propi temps invertit"
project_module_costs: "Temps i costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assignar les hores i costs registrats al projecte"
text_destroy_cost_entries_question: "%{cost_entries} van ser reportades al paquet de treball que vols eliminar. Què vols fer?"
text_destroy_time_and_cost_entries: "Eliminar hores i costs reportats"
diff --git a/modules/costs/config/locales/crowdin/ckb-IR.yml b/modules/costs/config/locales/crowdin/ckb-IR.yml
index a66568467c4a..16643fa127cc 100644
--- a/modules/costs/config/locales/crowdin/ckb-IR.yml
+++ b/modules/costs/config/locales/crowdin/ckb-IR.yml
@@ -85,8 +85,6 @@ ckb-IR:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ ckb-IR:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ ckb-IR:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/cs.yml b/modules/costs/config/locales/crowdin/cs.yml
index b6fadbf03329..22fc70c3b354 100644
--- a/modules/costs/config/locales/crowdin/cs.yml
+++ b/modules/costs/config/locales/crowdin/cs.yml
@@ -87,8 +87,6 @@ cs:
label_cost_type_plural: "Typy nákladů"
label_cost_type_specific: "Typ nákladu #%{id}: %{name}"
label_costs_per_page: "Náklady na stránku"
- label_currency: "Měna"
- label_currency_format: "Formát měny"
label_current_default_rate: "Aktuální výchozí sazba"
label_date_on: "zapnuto"
label_deleted_cost_types: "Odstraněné typy nákladů"
@@ -122,7 +120,7 @@ cs:
notice_something_wrong: "Něco se pokazilo. Zkuste to prosím znovu."
notice_successful_restore: "Úspěšně obnoveno."
notice_successful_lock: "Úspěšně uzamčeno."
- notice_cost_logged_successfully: 'Jednotková cena byla úspěšně zaznamenána.'
+ notice_cost_logged_successfully: "Jednotková cena byla úspěšně zaznamenána."
permission_edit_cost_entries: "Upravit rezervované jednotkové náklady"
permission_edit_own_cost_entries: "Upravit vlastní rezervované jednotkové náklady"
permission_edit_hourly_rates: "Upravit hodinové sazby"
@@ -137,6 +135,10 @@ cs:
permission_view_own_hourly_rate: "Zobrazit vlastní hodinovou sazbu"
permission_view_own_time_entries: "Zobrazit vlastní strávený čas"
project_module_costs: "Čas a náklady"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Přiřadit nahlášené hodiny a náklady projektu"
text_destroy_cost_entries_question: "%{cost_entries} byl nahlášen k pracovním balíčkům, které se chystáte odstranit. Co chcete udělat?"
text_destroy_time_and_cost_entries: "Odstranit nahlášené hodiny a náklady"
diff --git a/modules/costs/config/locales/crowdin/da.yml b/modules/costs/config/locales/crowdin/da.yml
index a39c839f3b2a..3c64f0839ba4 100644
--- a/modules/costs/config/locales/crowdin/da.yml
+++ b/modules/costs/config/locales/crowdin/da.yml
@@ -85,8 +85,6 @@ da:
label_cost_type_plural: "Omkostningstyper"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Omkostninger pr. side"
- label_currency: "Valuta"
- label_currency_format: "Valutaformat"
label_current_default_rate: "Nuværende standardsats"
label_date_on: "d."
label_deleted_cost_types: "Slettede omkostningstyper"
@@ -120,7 +118,7 @@ da:
notice_something_wrong: "Noget gik galt. Forsøg venligst igen."
notice_successful_restore: "Gendannet."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Redigér reserverede enhedsomkostninger"
permission_edit_own_cost_entries: "Redigér egne reserverede enhedsomkostninger"
permission_edit_hourly_rates: "Redigér timesatser"
@@ -135,6 +133,10 @@ da:
permission_view_own_hourly_rate: "Se egen timestaser"
permission_view_own_time_entries: "Se eget tidsforbrug"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Tildel indberettede timer og omkostninger til projektet"
text_destroy_cost_entries_question: "%{cost_entries} blev indberettet for de arbejdspakker, som du er ved at slette. Hvad ønsker du at gøre?"
text_destroy_time_and_cost_entries: "Slet indberettede timer og omkostninger"
diff --git a/modules/costs/config/locales/crowdin/de.yml b/modules/costs/config/locales/crowdin/de.yml
index d9149edd43cb..add466c5965a 100644
--- a/modules/costs/config/locales/crowdin/de.yml
+++ b/modules/costs/config/locales/crowdin/de.yml
@@ -85,8 +85,6 @@ de:
label_cost_type_plural: "Kostentypen"
label_cost_type_specific: "Kostentyp #%{id}: %{name}"
label_costs_per_page: "Kosten dieser Seite"
- label_currency: "Währung"
- label_currency_format: "Währungsformat"
label_current_default_rate: "Aktueller Standardsatz"
label_date_on: "am"
label_deleted_cost_types: "Gelöschte Kostentypen"
@@ -120,7 +118,7 @@ de:
notice_something_wrong: "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
notice_successful_restore: "Erfolgreich wiederhergestellt."
notice_successful_lock: "Erfolgreich gesperrt."
- notice_cost_logged_successfully: 'Stückkosten erfolgreich gebucht.'
+ notice_cost_logged_successfully: "Stückkosten erfolgreich gebucht."
permission_edit_cost_entries: "Bearbeiten gebuchter Stückkosten"
permission_edit_own_cost_entries: "Bearbeiten eigener gebuchter Stückkosten"
permission_edit_hourly_rates: "Stundensätze bearbeiten"
@@ -135,6 +133,10 @@ de:
permission_view_own_hourly_rate: "Eigene Stundensätze ansehen"
permission_view_own_time_entries: "Eigene gebuchte Aufwände ansehen"
project_module_costs: "Zeit und Kosten"
+ setting_allow_tracking_start_and_end_times: "Nutzern erlauben Start- und Endzeiten bei Zeitbuchungen zu erfassen."
+ setting_costs_currency: "Währung"
+ setting_costs_currency_format: "Format der Währung"
+ setting_enforce_tracking_start_and_end_times: "Das Setzen von Start- und Endzeiten bei Zeitbuchungen erzwingen."
text_assign_time_and_cost_entries_to_project: "Gebuchte Aufwände dem Projekt zuweisen"
text_destroy_cost_entries_question: "Es wurden bereits %{cost_entries} auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?"
text_destroy_time_and_cost_entries: "Gebuchte Aufwände löschen"
diff --git a/modules/costs/config/locales/crowdin/el.yml b/modules/costs/config/locales/crowdin/el.yml
index 3e532be0665e..b498d43cbb02 100644
--- a/modules/costs/config/locales/crowdin/el.yml
+++ b/modules/costs/config/locales/crowdin/el.yml
@@ -85,8 +85,6 @@ el:
label_cost_type_plural: "Τύποι κόστους"
label_cost_type_specific: "Τύπος κόστους #%{id}: %{name}"
label_costs_per_page: "Κόστος ανά σελίδα"
- label_currency: "Νόμισμα"
- label_currency_format: "Μορφοποίηση του νομίσματος"
label_current_default_rate: "Τρέχουσα προεπιλεγμένη αμοιβή"
label_date_on: "σε"
label_deleted_cost_types: "Διαγραμμένοι τύποι κόστους"
@@ -120,7 +118,7 @@ el:
notice_something_wrong: "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά."
notice_successful_restore: "Επιτυχής επαναφορά."
notice_successful_lock: "Κλειδώθηκε επιτυχώς."
- notice_cost_logged_successfully: 'Η μονάδα κόστους καταγράφηκε επιτυχώς.'
+ notice_cost_logged_successfully: "Η μονάδα κόστους καταγράφηκε επιτυχώς."
permission_edit_cost_entries: "Επεξεργασία κλεισμένου κόστους μονάδας"
permission_edit_own_cost_entries: "Επεξεργασία δικού μου κλεισμένου κόστους μονάδας"
permission_edit_hourly_rates: "Επεξεργασία ωριαίων αμοιβών"
@@ -135,6 +133,10 @@ el:
permission_view_own_hourly_rate: "Εμφάνιση των δικών μου ωριαίων αμοιβών"
permission_view_own_time_entries: "Προβολή του δικού μου χρόνου που δαπανήθηκε"
project_module_costs: "Χρόνος και κόστος"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Ανάθεση αναφερόμενων ωρών και κόστους στο έργο"
text_destroy_cost_entries_question: "%{cost_entries} αναφέρθηκαν στα πακέτα εργασίας που πρόκειται να διαγράψετε. Τι θέλετε να κάνετε;"
text_destroy_time_and_cost_entries: "Διαγραφή αναφερόμενων ωρών και κόστους"
diff --git a/modules/costs/config/locales/crowdin/eo.yml b/modules/costs/config/locales/crowdin/eo.yml
index 75b7e65fa1da..bb6c809541d0 100644
--- a/modules/costs/config/locales/crowdin/eo.yml
+++ b/modules/costs/config/locales/crowdin/eo.yml
@@ -85,8 +85,6 @@ eo:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Speco de kosto #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "en"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ eo:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ eo:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Tempo kaj kostoj"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/es.yml b/modules/costs/config/locales/crowdin/es.yml
index 57df20a246ba..7327e6bfdfd2 100644
--- a/modules/costs/config/locales/crowdin/es.yml
+++ b/modules/costs/config/locales/crowdin/es.yml
@@ -85,8 +85,6 @@ es:
label_cost_type_plural: "Tipos de costos"
label_cost_type_specific: "Tipo de coste %{id}: %{name}"
label_costs_per_page: "Costos por página"
- label_currency: "Moneda"
- label_currency_format: "Formato de moneda"
label_current_default_rate: "tasa de morosidad actual"
label_date_on: "en"
label_deleted_cost_types: "Eliminar tipos de costo"
@@ -120,7 +118,7 @@ es:
notice_something_wrong: "Algo salió mal. Por favor intentelo nuevamente."
notice_successful_restore: "Restauración exitosa."
notice_successful_lock: "Bloqueado correctamente."
- notice_cost_logged_successfully: 'Costo unitario registrado correctamente.'
+ notice_cost_logged_successfully: "Costo unitario registrado correctamente."
permission_edit_cost_entries: "Editar los costos unitarios reservados"
permission_edit_own_cost_entries: "Editar los costos unitarios propios reservados"
permission_edit_hourly_rates: "Editar las tasas por hora"
@@ -135,6 +133,10 @@ es:
permission_view_own_hourly_rate: "Ver tarifa por hora propia"
permission_view_own_time_entries: "Ver tiempo propio invertido"
project_module_costs: "Tiempo y costos"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Asignar horas reportadas y los costos al proyecto"
text_destroy_cost_entries_question: "%{cost_entries} fueron reportados en los paquetes de trabajo que van a eliminar. ¿Qué quieres hacer?"
text_destroy_time_and_cost_entries: "Eliminar horas reportadas y costos"
diff --git a/modules/costs/config/locales/crowdin/et.yml b/modules/costs/config/locales/crowdin/et.yml
index cd89ff666094..52b8af64e1f5 100644
--- a/modules/costs/config/locales/crowdin/et.yml
+++ b/modules/costs/config/locales/crowdin/et.yml
@@ -85,8 +85,6 @@ et:
label_cost_type_plural: "Kulu liigid"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Valuuta vorming"
label_current_default_rate: "Praegune valuutakurss"
label_date_on: "on"
label_deleted_cost_types: "Kustutatud kululiigid"
@@ -120,7 +118,7 @@ et:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Lukustatud."
- notice_cost_logged_successfully: 'Ühiku kulu on logitud.'
+ notice_cost_logged_successfully: "Ühiku kulu on logitud."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ et:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/eu.yml b/modules/costs/config/locales/crowdin/eu.yml
index cce54fd2d8b9..3946b8109c95 100644
--- a/modules/costs/config/locales/crowdin/eu.yml
+++ b/modules/costs/config/locales/crowdin/eu.yml
@@ -85,8 +85,6 @@ eu:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ eu:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ eu:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/fa.yml b/modules/costs/config/locales/crowdin/fa.yml
index 81e94fe451bd..b4743735df0f 100644
--- a/modules/costs/config/locales/crowdin/fa.yml
+++ b/modules/costs/config/locales/crowdin/fa.yml
@@ -85,8 +85,6 @@ fa:
label_cost_type_plural: "انواع هزینه"
label_cost_type_specific: "نوع هزینه #%{id}: %{name}"
label_costs_per_page: "هزینهها در هر صفحه"
- label_currency: "واحد پول"
- label_currency_format: "فرمت واحد پول"
label_current_default_rate: "ضریب پرداخت پیشفرض فعلی"
label_date_on: "برخط"
label_deleted_cost_types: "انواع هزینه حذفشده"
@@ -120,7 +118,7 @@ fa:
notice_something_wrong: "مشکلی پیش آمد. لطفا دوباره تلاش کنید."
notice_successful_restore: "بازیابی موفق"
notice_successful_lock: "با موفقیت قفل شد."
- notice_cost_logged_successfully: 'هزینه واحد با موفقیت ثبت شد.'
+ notice_cost_logged_successfully: "هزینه واحد با موفقیت ثبت شد."
permission_edit_cost_entries: "ویرایش هزینههای واحد رزرو شده"
permission_edit_own_cost_entries: "ویرایش هزینههای واحد رزرو شده خود"
permission_edit_hourly_rates: "ویرایش نرخ (ضریب پرداختی) ساعتی"
@@ -135,6 +133,10 @@ fa:
permission_view_own_hourly_rate: "مشاهده نرخ ساعتی خود"
permission_view_own_time_entries: "مشاهده زمان صرفشده خود"
project_module_costs: "زمان و هزینهها"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "تخصیص ساعتها و هزینههای گزارش شده به پروژه"
text_destroy_cost_entries_question: "%{cost_entries} در بستههای کاری که میخواهید حذف کنید گزارش شده است. میخوای چیکار کنی؟"
text_destroy_time_and_cost_entries: "ساعت ها و هزینه های گزارش شده را حذف کنید"
diff --git a/modules/costs/config/locales/crowdin/fi.yml b/modules/costs/config/locales/crowdin/fi.yml
index 99232729dec9..377c4134a2c7 100644
--- a/modules/costs/config/locales/crowdin/fi.yml
+++ b/modules/costs/config/locales/crowdin/fi.yml
@@ -85,8 +85,6 @@ fi:
label_cost_type_plural: "Kustannusten tyypit"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Kustannuksia sivulla"
- label_currency: "Valuutta"
- label_currency_format: "Valuuttamuotoilu"
label_current_default_rate: "Nykyinen vakiotuntihinta"
label_date_on: "päälle"
label_deleted_cost_types: "Poistetut kustannuslajit"
@@ -120,7 +118,7 @@ fi:
notice_something_wrong: "Jotain meni pieleen. Yritä uudelleen."
notice_successful_restore: "Palautus onnistui."
notice_successful_lock: "Lukittu onnistuneesti."
- notice_cost_logged_successfully: 'Kustannus kirjattu onnistuneesti.'
+ notice_cost_logged_successfully: "Kustannus kirjattu onnistuneesti."
permission_edit_cost_entries: "Kirjattujen yksikkökustannusten muokkaus"
permission_edit_own_cost_entries: "Omien yksikkökustannuskirjausten muokkaus"
permission_edit_hourly_rates: "Tuntihintojen muokkaus"
@@ -135,6 +133,10 @@ fi:
permission_view_own_hourly_rate: "Oman tuntinnan tarkasteleminen"
permission_view_own_time_entries: "Oman aikakirjauksen tarkistelu"
project_module_costs: "Työaika ja kustannukset"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Raportoitujen tunti- ja kustannuskirjausten liittäminen projektiin"
text_destroy_cost_entries_question: "%{cost_entries} on raportoitu projektille, jota olet poistamassa. Mitä haluat tehdä?"
text_destroy_time_and_cost_entries: "Poista raportoidut tunnit ja kustannukset"
diff --git a/modules/costs/config/locales/crowdin/fil.yml b/modules/costs/config/locales/crowdin/fil.yml
index f880dc8e9978..85070c611d8b 100644
--- a/modules/costs/config/locales/crowdin/fil.yml
+++ b/modules/costs/config/locales/crowdin/fil.yml
@@ -85,8 +85,6 @@ fil:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "sa"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ fil:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ fil:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/fr.yml b/modules/costs/config/locales/crowdin/fr.yml
index e761c7e72601..b57fa3d525d1 100644
--- a/modules/costs/config/locales/crowdin/fr.yml
+++ b/modules/costs/config/locales/crowdin/fr.yml
@@ -85,8 +85,6 @@ fr:
label_cost_type_plural: "Types de coûts"
label_cost_type_specific: "Type de coût N°%{id} : %{name}"
label_costs_per_page: "Coûts par page"
- label_currency: "Devise"
- label_currency_format: "Format de devise"
label_current_default_rate: "Taux par défaut actuel"
label_date_on: "le"
label_deleted_cost_types: "Types de coût effacés"
@@ -120,7 +118,7 @@ fr:
notice_something_wrong: "Quelque chose s'est mal passé. Veuillez réessayer."
notice_successful_restore: "Restauration réussie."
notice_successful_lock: "Bloqué avec succès."
- notice_cost_logged_successfully: 'Coût unitaire enregistré avec succès.'
+ notice_cost_logged_successfully: "Coût unitaire enregistré avec succès."
permission_edit_cost_entries: "Éditer coûts unitaires réservés"
permission_edit_own_cost_entries: "Éditer coûts unitaires propres réservés"
permission_edit_hourly_rates: "Éditer les tarifs horaires"
@@ -135,6 +133,10 @@ fr:
permission_view_own_hourly_rate: "Voir son propre tarif horaire"
permission_view_own_time_entries: "Voir son propre temps passé"
project_module_costs: "Temps et coûts"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assigner les heures et coûts consignés au projet"
text_destroy_cost_entries_question: "%{cost_entries} ont été consignés sur les lots de travaux que vous êtes sur le point de supprimer. Que voulez-vous faire ?"
text_destroy_time_and_cost_entries: "Supprimer les heures et coûts consignés"
diff --git a/modules/costs/config/locales/crowdin/he.yml b/modules/costs/config/locales/crowdin/he.yml
index f716b2efea02..7336b306d83b 100644
--- a/modules/costs/config/locales/crowdin/he.yml
+++ b/modules/costs/config/locales/crowdin/he.yml
@@ -87,8 +87,6 @@ he:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "מס' עלות %{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "פועל"
label_deleted_cost_types: "Deleted cost types"
@@ -122,7 +120,7 @@ he:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -137,6 +135,10 @@ he:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "עלויות"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/hi.yml b/modules/costs/config/locales/crowdin/hi.yml
index 84a589d334fc..a1d4a3937593 100644
--- a/modules/costs/config/locales/crowdin/hi.yml
+++ b/modules/costs/config/locales/crowdin/hi.yml
@@ -85,8 +85,6 @@ hi:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "पर"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ hi:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ hi:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/hr.yml b/modules/costs/config/locales/crowdin/hr.yml
index 2d9aa164abea..b34657abc311 100644
--- a/modules/costs/config/locales/crowdin/hr.yml
+++ b/modules/costs/config/locales/crowdin/hr.yml
@@ -86,8 +86,6 @@ hr:
label_cost_type_plural: "Vrsta troška"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Troškova po stranici"
- label_currency: "Valuta"
- label_currency_format: "Oblik valute"
label_current_default_rate: "Trenutno zadana stopa"
label_date_on: "na"
label_deleted_cost_types: "Izbrisane tipovi troškova"
@@ -121,7 +119,7 @@ hr:
notice_something_wrong: "Nešto je pošlo po zlu. Molimo pokušajte ponovno."
notice_successful_restore: "Uspješno vraćanje."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Jedinični trošak uspješno zabilježen.'
+ notice_cost_logged_successfully: "Jedinični trošak uspješno zabilježen."
permission_edit_cost_entries: "Uredite zabilježene jedinične troškove"
permission_edit_own_cost_entries: "Uredite vlastite zabilježene jedinične troškove"
permission_edit_hourly_rates: "Uredite satnice"
@@ -136,6 +134,10 @@ hr:
permission_view_own_hourly_rate: "Prikaži vlastite satnice"
permission_view_own_time_entries: "Pogledaj vlastito utrošeno vrijeme"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Dodijeli prijavljene satnice i troškove na projektu"
text_destroy_cost_entries_question: "%{cost_entries} su prikazani na radnim paketima koje želite izbrisati. Što zapravo želite napraviti?"
text_destroy_time_and_cost_entries: "Izbriši prijavljene sate i troškove"
diff --git a/modules/costs/config/locales/crowdin/hu.yml b/modules/costs/config/locales/crowdin/hu.yml
index 8fffb9b2bac0..1a965580fe97 100644
--- a/modules/costs/config/locales/crowdin/hu.yml
+++ b/modules/costs/config/locales/crowdin/hu.yml
@@ -85,8 +85,6 @@ hu:
label_cost_type_plural: "Költségtípusok"
label_cost_type_specific: "Költség típus%{id}%{name}"
label_costs_per_page: "Költések oldalanként"
- label_currency: "Pénznem"
- label_currency_format: "Pénznem formátuma"
label_current_default_rate: "Jelenlegi alapértelmezett díj"
label_date_on: "mikor"
label_deleted_cost_types: "Törölt költségnemek"
@@ -120,7 +118,7 @@ hu:
notice_something_wrong: "Valami hiba történt. Kérlek próbáld újra."
notice_successful_restore: "Sikeres visszaállítás."
notice_successful_lock: "Sikeresen zárolt."
- notice_cost_logged_successfully: 'Egységköltség sikeresen naplózva.'
+ notice_cost_logged_successfully: "Egységköltség sikeresen naplózva."
permission_edit_cost_entries: "Könyvelt egységköltségek szerkesztése"
permission_edit_own_cost_entries: "Saját könyvelt egységköltségek szerkesztése"
permission_edit_hourly_rates: "Órabérek szerkesztése"
@@ -135,6 +133,10 @@ hu:
permission_view_own_hourly_rate: "Saját óradíj megtekintése"
permission_view_own_time_entries: "Az eltöltött idő megtekintése"
project_module_costs: "Idő és költség"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Idők és költségek projekthez rendelése"
text_destroy_cost_entries_question: "A work package-hez rögzített %{cost_entries} törlése. Mit szeretnél tenni?"
text_destroy_time_and_cost_entries: "Idők és költségek törlése"
diff --git a/modules/costs/config/locales/crowdin/id.yml b/modules/costs/config/locales/crowdin/id.yml
index f73f92a8fbb5..110d0cd70bc1 100644
--- a/modules/costs/config/locales/crowdin/id.yml
+++ b/modules/costs/config/locales/crowdin/id.yml
@@ -84,8 +84,6 @@ id:
label_cost_type_plural: "Jenis biaya"
label_cost_type_specific: "Jenis biaya #%{id}: %{name}"
label_costs_per_page: "Tampilan biaya perhalaman"
- label_currency: "Mata Uang"
- label_currency_format: "Format mata uang"
label_current_default_rate: "Rate saat ini"
label_date_on: "pada"
label_deleted_cost_types: "Jenis biaya terhapus"
@@ -119,7 +117,7 @@ id:
notice_something_wrong: "Terjadi suatu kesalahan. Silahkan coba lagi."
notice_successful_restore: "Restore berhasil."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Ubah booked biaya unit"
permission_edit_own_cost_entries: "Edit booked biaya unit sendiri"
permission_edit_hourly_rates: "Edit rate per jam"
@@ -134,6 +132,10 @@ id:
permission_view_own_hourly_rate: "Tampilkan rate per-jam diri sendiri"
permission_view_own_time_entries: "Tampilan jumlah waktu diri sendiri"
project_module_costs: "Waktu dan biaya"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Masukkan laporan per-jam dan laporan biaya ke proyek"
text_destroy_cost_entries_question: "%{cost_entries} digunakan pada work package yang akan dihapus. Keputusan anda?"
text_destroy_time_and_cost_entries: "Hapus jumlah jam dan biaya yang terlapor"
diff --git a/modules/costs/config/locales/crowdin/it.yml b/modules/costs/config/locales/crowdin/it.yml
index 471179ef7cef..035b5d58d35d 100644
--- a/modules/costs/config/locales/crowdin/it.yml
+++ b/modules/costs/config/locales/crowdin/it.yml
@@ -85,8 +85,6 @@ it:
label_cost_type_plural: "Tipo di costo"
label_cost_type_specific: "Tipo di costo #%{id}: %{name}"
label_costs_per_page: "Costi per pagina"
- label_currency: "Valuta"
- label_currency_format: "Formato della valuta"
label_current_default_rate: "Tariffa predefinita corrente"
label_date_on: "il"
label_deleted_cost_types: "Tipi di costo eliminati"
@@ -120,7 +118,7 @@ it:
notice_something_wrong: "Qualcosa è andato storto. Si prega di riprovare."
notice_successful_restore: "Ripristino eseguito con successo."
notice_successful_lock: "Bloccato con successo."
- notice_cost_logged_successfully: 'Costo unitario registrato con successo.'
+ notice_cost_logged_successfully: "Costo unitario registrato con successo."
permission_edit_cost_entries: "Modifica unità di costo riservate"
permission_edit_own_cost_entries: "Modifica le proprie unità di costo riservate"
permission_edit_hourly_rates: "Modifica tariffe orarie"
@@ -135,6 +133,10 @@ it:
permission_view_own_hourly_rate: "Visualizza propria tariffa oraria"
permission_view_own_time_entries: "Visualizzare il proprio tempo trascorso"
project_module_costs: "Tempi e costi"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assegna orari e costi segnalati al progetto"
text_destroy_cost_entries_question: "%{cost_entries} sono stati segnalati per la macro-attività (work package) che si sta per eliminare. Che cosa vuoi fare?"
text_destroy_time_and_cost_entries: "Elimina orari e costi riportati"
diff --git a/modules/costs/config/locales/crowdin/ja.yml b/modules/costs/config/locales/crowdin/ja.yml
index 8c3b4d4467be..bbb84f6dd205 100644
--- a/modules/costs/config/locales/crowdin/ja.yml
+++ b/modules/costs/config/locales/crowdin/ja.yml
@@ -84,8 +84,6 @@ ja:
label_cost_type_plural: "コスト種類"
label_cost_type_specific: "コスト種類 #%{id}: %{name}"
label_costs_per_page: "1ページあたりのコスト"
- label_currency: "通貨"
- label_currency_format: "通貨の形式"
label_current_default_rate: "現在のデフォルトレート"
label_date_on: "オン"
label_deleted_cost_types: "削除されたコストタイプ"
@@ -119,7 +117,7 @@ ja:
notice_something_wrong: "エラーが発生しました。再試行してください。"
notice_successful_restore: "復元処理が成功します。"
notice_successful_lock: "ロックに成功しました。"
- notice_cost_logged_successfully: '単価が正常に記録されました。'
+ notice_cost_logged_successfully: "単価が正常に記録されました。"
permission_edit_cost_entries: "予約単価の編集"
permission_edit_own_cost_entries: "自分が予約した単価の編集"
permission_edit_hourly_rates: "時給の編集"
@@ -134,6 +132,10 @@ ja:
permission_view_own_hourly_rate: "自分の時給の表示"
permission_view_own_time_entries: "自分の使用済み時間の表示"
project_module_costs: "時間とコスト"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "報告された時間とコストをプロジェクトに割り当てる"
text_destroy_cost_entries_question: "削除しようとしている作業項目が%{cost_entries} 件報告されました。どうしますか?"
text_destroy_time_and_cost_entries: "報告された時間とコストを削除する"
diff --git a/modules/costs/config/locales/crowdin/ka.yml b/modules/costs/config/locales/crowdin/ka.yml
index 3890d642074d..ba345258b083 100644
--- a/modules/costs/config/locales/crowdin/ka.yml
+++ b/modules/costs/config/locales/crowdin/ka.yml
@@ -85,8 +85,6 @@ ka:
label_cost_type_plural: "ღირებულების ტიპი"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "ღირებულებები თითოეული გვერდისთვის"
- label_currency: "ფული"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "ჩართული"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ ka:
notice_something_wrong: "მოხდა შეცდომა. სცადეთ თავიდან."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ ka:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "დრო და ღირებულებები"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/kk.yml b/modules/costs/config/locales/crowdin/kk.yml
index 4aee728a6ec9..19fc6a7da4d9 100644
--- a/modules/costs/config/locales/crowdin/kk.yml
+++ b/modules/costs/config/locales/crowdin/kk.yml
@@ -85,8 +85,6 @@ kk:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ kk:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ kk:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/ko.yml b/modules/costs/config/locales/crowdin/ko.yml
index 0b091b0d2586..75a5b614346b 100644
--- a/modules/costs/config/locales/crowdin/ko.yml
+++ b/modules/costs/config/locales/crowdin/ko.yml
@@ -84,8 +84,6 @@ ko:
label_cost_type_plural: "비용 유형"
label_cost_type_specific: "비용 유형 #%{id}: %{name}"
label_costs_per_page: "페이지당 비용"
- label_currency: "통화"
- label_currency_format: "통화 형식"
label_current_default_rate: "현재 기본 요금"
label_date_on: "-"
label_deleted_cost_types: "삭제된 비용 유형"
@@ -119,7 +117,7 @@ ko:
notice_something_wrong: "문제가 발생했습니다. 다시 시도해 주세요."
notice_successful_restore: "성공적으로 복원되었습니다."
notice_successful_lock: "잠겼습니다."
- notice_cost_logged_successfully: '단위 비용이 기록되었습니다.'
+ notice_cost_logged_successfully: "단위 비용이 기록되었습니다."
permission_edit_cost_entries: "예약된 단위 비용 편집"
permission_edit_own_cost_entries: "고유한 예약된 단위 비용 편집"
permission_edit_hourly_rates: "시간당 요금 편집"
@@ -134,6 +132,10 @@ ko:
permission_view_own_hourly_rate: "고유한 시간당 요금 보기"
permission_view_own_time_entries: "고유한 소비 시간 보기"
project_module_costs: "시간 및 비용"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "보고된 시간 및 비용을 프로젝트에 할당"
text_destroy_cost_entries_question: "%{cost_entries}은(는) 삭제하려는 작업 패키지에서 보고되었습니다. 어떻게 하시겠습니까?"
text_destroy_time_and_cost_entries: "보고된 시간 및 비용 삭제"
diff --git a/modules/costs/config/locales/crowdin/lt.yml b/modules/costs/config/locales/crowdin/lt.yml
index ac7a2e0820c0..69519573428a 100644
--- a/modules/costs/config/locales/crowdin/lt.yml
+++ b/modules/costs/config/locales/crowdin/lt.yml
@@ -87,8 +87,6 @@ lt:
label_cost_type_plural: "Kaštų tipai"
label_cost_type_specific: "Išlaidų tipas #%{id}: %{name}"
label_costs_per_page: "Kaštų puslapyje"
- label_currency: "Valiuta"
- label_currency_format: "Valiutos formatas"
label_current_default_rate: "Esamas numatytasis koeficientas"
label_date_on: "įjungta"
label_deleted_cost_types: "Panaikinti kaštų tipai"
@@ -122,7 +120,7 @@ lt:
notice_something_wrong: "Įvyko klaida. Bandykite dar kartą."
notice_successful_restore: "Sėkmingas atkūrimas."
notice_successful_lock: "Sėkmingai užrakinta."
- notice_cost_logged_successfully: 'Vieneto kaštai į žurnalą įrašyti sėkmingai.'
+ notice_cost_logged_successfully: "Vieneto kaštai į žurnalą įrašyti sėkmingai."
permission_edit_cost_entries: "Redaguoti įrašytų vienetų kaštus"
permission_edit_own_cost_entries: "Redaguoti paties įrašytų vienetų kaštus"
permission_edit_hourly_rates: "Redaguoti valandinius koeficientus"
@@ -137,6 +135,10 @@ lt:
permission_view_own_hourly_rate: "Peržiūrėti savo valandinį koeficientą"
permission_view_own_time_entries: "Peržiūrėti savo sugaištą laiką"
project_module_costs: "Laikas ir išlaidos"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Priskirti praleistas valandas ir kaštus projektui"
text_destroy_cost_entries_question: "%{cost_entries} buvo pranešta Jūsų ketinamiems ištrinti darbų paketams. Ką Jūs norite daryti?"
text_destroy_time_and_cost_entries: "Ištrinti praneštas valandas ir kaštus"
diff --git a/modules/costs/config/locales/crowdin/lv.yml b/modules/costs/config/locales/crowdin/lv.yml
index 551cd631825b..dc31a556a99a 100644
--- a/modules/costs/config/locales/crowdin/lv.yml
+++ b/modules/costs/config/locales/crowdin/lv.yml
@@ -57,8 +57,8 @@ lv:
nullify_is_not_valid_for_cost_entries: ""
attributes:
comment: "Komentârs"
- cost_type: "Cost type"
- costs: "Costs"
+ cost_type: "Izmaksu veids"
+ costs: "Izmaksas"
current_rate: "Current rate"
hours: "Stundas"
units: "Vienības"
@@ -86,8 +86,6 @@ lv:
label_cost_type_plural: "Izmaksu veids"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: " "
label_deleted_cost_types: "Deleted cost types"
@@ -112,7 +110,7 @@ lv:
label_rate: "Likme"
label_rate_plural: "Likmes"
label_status_finished: "Pabeigts"
- label_show: "Show"
+ label_show: "Rādīt"
label_units: "Izmaksu vienības"
label_user: "Lietotāji"
label_until: "līdz"
@@ -121,7 +119,7 @@ lv:
notice_something_wrong: "Kļūda. Mēģiniet vēlreiz."
notice_successful_restore: "Veiksmīga atjaunošana."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Sekmīgi reģistrētas vienības izmaksas.'
+ notice_cost_logged_successfully: "Sekmīgi reģistrētas vienības izmaksas."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -136,12 +134,16 @@ lv:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Laiks un izmaksas"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Valūta"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
text_destroy_time_and_cost_entries_question: "%{hours} hours, %{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_reassign_time_and_cost_entries: "Reassign reported hours and costs to this work package:"
text_warning_hidden_elements: "Some entries may have been excluded from the aggregation."
- week: "week"
+ week: "nedēļa"
js:
text_are_you_sure: "Vai esat pārliecināts?"
diff --git a/modules/costs/config/locales/crowdin/mn.yml b/modules/costs/config/locales/crowdin/mn.yml
index d6876c73668c..a775a95d3845 100644
--- a/modules/costs/config/locales/crowdin/mn.yml
+++ b/modules/costs/config/locales/crowdin/mn.yml
@@ -85,8 +85,6 @@ mn:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ mn:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ mn:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/ms.yml b/modules/costs/config/locales/crowdin/ms.yml
index 026330233933..b87c59e96392 100644
--- a/modules/costs/config/locales/crowdin/ms.yml
+++ b/modules/costs/config/locales/crowdin/ms.yml
@@ -84,8 +84,6 @@ ms:
label_cost_type_plural: "Jenis kos"
label_cost_type_specific: "Jenis kos #%{id}: %{name}"
label_costs_per_page: "Kos setiap halaman"
- label_currency: "Mata Wang"
- label_currency_format: "Format mata wang"
label_current_default_rate: "Kadar default semasa"
label_date_on: "pada"
label_deleted_cost_types: "Jenis kos yang dipadam"
@@ -119,7 +117,7 @@ ms:
notice_something_wrong: "Ada ralat berlaku. Sila cuba lagi."
notice_successful_restore: "Pemulihan berjaya."
notice_successful_lock: "Berjaya dikunci."
- notice_cost_logged_successfully: 'Unit kos berjaya dilog.'
+ notice_cost_logged_successfully: "Unit kos berjaya dilog."
permission_edit_cost_entries: "Edit kos unit yang ditempah"
permission_edit_own_cost_entries: "Edit kos unit yang ditempah sendiri"
permission_edit_hourly_rates: "Edit kadar jam"
@@ -134,6 +132,10 @@ ms:
permission_view_own_hourly_rate: "Lihat kadar jam sendiri"
permission_view_own_time_entries: "Lihat masa yang digunakan sendiri"
project_module_costs: "Masa dan kos"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Tetapkan jam dan kos yang dilaporkan ke projek"
text_destroy_cost_entries_question: "%{cost_entries} dilaporkan pada pakej kerja yang anda ingin padamkan. Apakah yang anda ingin lakukan ?"
text_destroy_time_and_cost_entries: "Padam jam dan kos yang dilaporkan"
diff --git a/modules/costs/config/locales/crowdin/ne.yml b/modules/costs/config/locales/crowdin/ne.yml
index 47cab5ec4431..dee5a682d593 100644
--- a/modules/costs/config/locales/crowdin/ne.yml
+++ b/modules/costs/config/locales/crowdin/ne.yml
@@ -85,8 +85,6 @@ ne:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ ne:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ ne:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/nl.yml b/modules/costs/config/locales/crowdin/nl.yml
index 1a9053527618..505efed26764 100644
--- a/modules/costs/config/locales/crowdin/nl.yml
+++ b/modules/costs/config/locales/crowdin/nl.yml
@@ -85,8 +85,6 @@ nl:
label_cost_type_plural: "Soort kosten"
label_cost_type_specific: "Soort kosten #%{id}: %{name}"
label_costs_per_page: "Kosten per pagina"
- label_currency: "Valuta"
- label_currency_format: "Formaat van valuta"
label_current_default_rate: "Huidige standaard tarief"
label_date_on: "op"
label_deleted_cost_types: "Verwijderde kostentypen"
@@ -111,7 +109,7 @@ nl:
label_rate: "Tarief"
label_rate_plural: "Tarieven"
label_status_finished: "Afgewerkt"
- label_show: "Show"
+ label_show: "Toon"
label_units: "Kosten eenheden"
label_user: "Gebruiker"
label_until: "tot"
@@ -120,7 +118,7 @@ nl:
notice_something_wrong: "Er is iets fout gegaan. Probeer het opnieuw."
notice_successful_restore: "Herstel geslaagd."
notice_successful_lock: "Vergrendeld met succes."
- notice_cost_logged_successfully: 'Kosten eenheid succesvol geregistreerd.'
+ notice_cost_logged_successfully: "Kosten eenheid succesvol geregistreerd."
permission_edit_cost_entries: "Geboekte kosten per eenheid bewerken"
permission_edit_own_cost_entries: "Eigen geboekte kosten per eenheid bewerken"
permission_edit_hourly_rates: "Bewerk uurtarieven"
@@ -135,6 +133,10 @@ nl:
permission_view_own_hourly_rate: "Bekijk eigen uurtarief"
permission_view_own_time_entries: "Bekijk eigen bestede tijd"
project_module_costs: "Tijd en kosten"
+ setting_allow_tracking_start_and_end_times: "Gebruikers toestaan om begin- en eindtijd bij te houden op tijdregistraties"
+ setting_costs_currency: "Valuta"
+ setting_costs_currency_format: "Formaat van valuta"
+ setting_enforce_tracking_start_and_end_times: "Forceer gebruikers om de start- en eindtijd op tijdrecords in te stellen"
text_assign_time_and_cost_entries_to_project: "Gerapporteerde uren en kosten aan het project toewijzen"
text_destroy_cost_entries_question: "%{cost_entries} werd gerapporteerd op de werkpakketten die je gaat verwijderen. Wat wil je doen?"
text_destroy_time_and_cost_entries: "Gerapporteerde uren en kosten verwijderen"
diff --git a/modules/costs/config/locales/crowdin/no.yml b/modules/costs/config/locales/crowdin/no.yml
index 1966725492c6..fe530123aca6 100644
--- a/modules/costs/config/locales/crowdin/no.yml
+++ b/modules/costs/config/locales/crowdin/no.yml
@@ -85,8 +85,6 @@
label_cost_type_plural: "Kostnadstyper"
label_cost_type_specific: "Kostnadstype #%{id}: %{name}"
label_costs_per_page: "Kostnader per side"
- label_currency: "Valuta"
- label_currency_format: "Formatet på valuta"
label_current_default_rate: "Nåværende standardsats"
label_date_on: "på"
label_deleted_cost_types: "Slettede kostnadstyper"
@@ -120,7 +118,7 @@
notice_something_wrong: "Noe gikk galt. Prøv på nytt."
notice_successful_restore: "Vellykket gjenoppretting."
notice_successful_lock: "Låst vellykket."
- notice_cost_logged_successfully: 'Enhetskostnaden er logget.'
+ notice_cost_logged_successfully: "Enhetskostnaden er logget."
permission_edit_cost_entries: "Rediger bokførte enhetskostnader"
permission_edit_own_cost_entries: "Rediger egne bokførte enhetskostnader"
permission_edit_hourly_rates: "Rediger timesatser"
@@ -135,6 +133,10 @@
permission_view_own_hourly_rate: "Vis egen timesats"
permission_view_own_time_entries: "Vis egen tidsbruk"
project_module_costs: "Tid og kostnader"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Overfør rapporterte timer og kostnader til prosjektet"
text_destroy_cost_entries_question: "%{cost_entries} ble rapportert på arbeidspakkene du er i ferd med å slette. Hva vil du gjøre ?"
text_destroy_time_and_cost_entries: "Slett rapporterte timer og kostnader"
diff --git a/modules/costs/config/locales/crowdin/pl.yml b/modules/costs/config/locales/crowdin/pl.yml
index 3baa261239c3..8a5db5ae8891 100644
--- a/modules/costs/config/locales/crowdin/pl.yml
+++ b/modules/costs/config/locales/crowdin/pl.yml
@@ -87,8 +87,6 @@ pl:
label_cost_type_plural: "Rodzaje kosztów"
label_cost_type_specific: "Typ kosztu #%{id}: %{name}"
label_costs_per_page: "Kosztów na stronę"
- label_currency: "Waluta"
- label_currency_format: "Format waluty"
label_current_default_rate: "Aktualne stawki domyślne"
label_date_on: "na"
label_deleted_cost_types: "Usunięte typy kosztów"
@@ -122,7 +120,7 @@ pl:
notice_something_wrong: "Coś poszło nie tak. Proszę spróbuj ponownie."
notice_successful_restore: "Przywracanie się powiodło."
notice_successful_lock: "Zablokowano."
- notice_cost_logged_successfully: 'Koszt jednostkowy zarejestrowano pomyślnie.'
+ notice_cost_logged_successfully: "Koszt jednostkowy zarejestrowano pomyślnie."
permission_edit_cost_entries: "Edytuj zapisane koszty jednostkowe"
permission_edit_own_cost_entries: "Edytuj zarejestrowane własne koszty jednostkowe"
permission_edit_hourly_rates: "Edycja stawek godzinowych"
@@ -137,6 +135,10 @@ pl:
permission_view_own_hourly_rate: "Zobacz własne stawki godzinowe"
permission_view_own_time_entries: "Zobacz swój poświęcony czas"
project_module_costs: "Czas i koszty"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Przypisz zgłoszone godziny i koszty do projektu"
text_destroy_cost_entries_question: "%{cost_entries} zostały zgłoszone do Zestawu zadań, który chcesz usunąć. Co chcesz zrobić?"
text_destroy_time_and_cost_entries: "Usuń zgłoszone godziny i koszty"
diff --git a/modules/costs/config/locales/crowdin/pt-BR.yml b/modules/costs/config/locales/crowdin/pt-BR.yml
index 226b125e25ad..13ea16d2c347 100644
--- a/modules/costs/config/locales/crowdin/pt-BR.yml
+++ b/modules/costs/config/locales/crowdin/pt-BR.yml
@@ -85,8 +85,6 @@ pt-BR:
label_cost_type_plural: "Tipos de custo"
label_cost_type_specific: "Tipos de custo #%{id}: %{name}"
label_costs_per_page: "Custos por página"
- label_currency: "Moeda"
- label_currency_format: "Formato de moeda"
label_current_default_rate: "Atual taxa padrão"
label_date_on: "em"
label_deleted_cost_types: "Tipos de custos excluídos"
@@ -120,7 +118,7 @@ pt-BR:
notice_something_wrong: "Algo deu errado. Por favor, tente novamente."
notice_successful_restore: "Restauração bem-sucedida."
notice_successful_lock: "Bloqueado com sucesso."
- notice_cost_logged_successfully: 'Custo unitário registrado com sucesso.'
+ notice_cost_logged_successfully: "Custo unitário registrado com sucesso."
permission_edit_cost_entries: "Editar custos unitários reservados"
permission_edit_own_cost_entries: "Editar custos unitários próprios reservados"
permission_edit_hourly_rates: "Editar taxas horárias"
@@ -135,6 +133,10 @@ pt-BR:
permission_view_own_hourly_rate: "Ver sua própria taxa horária"
permission_view_own_time_entries: "Ver o próprio tempo gasto"
project_module_costs: "Tempo e custos"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Atribuir horas relatadas e custos ao projeto"
text_destroy_cost_entries_question: "%{cost_entries} foram informados sobre os pacotes de trabalho que você está prestes a excluir. O que você quer fazer?"
text_destroy_time_and_cost_entries: "Excluir horas e custos informados"
diff --git a/modules/costs/config/locales/crowdin/pt-PT.yml b/modules/costs/config/locales/crowdin/pt-PT.yml
index ec400acdc328..8d5e5de51112 100644
--- a/modules/costs/config/locales/crowdin/pt-PT.yml
+++ b/modules/costs/config/locales/crowdin/pt-PT.yml
@@ -85,8 +85,6 @@ pt-PT:
label_cost_type_plural: "Tipos de custos"
label_cost_type_specific: "Tipos de custo #%{id}: %{name}"
label_costs_per_page: "Custos por página"
- label_currency: "Moeda"
- label_currency_format: "Formato de moeda"
label_current_default_rate: "Taxa atual por defeito"
label_date_on: "em"
label_deleted_cost_types: "Tipos de custos apagados"
@@ -120,7 +118,7 @@ pt-PT:
notice_something_wrong: "Ocorreu um erro. Por favor, tente novamente."
notice_successful_restore: "Restauração bem-sucedida."
notice_successful_lock: "Bloqueado com êxito."
- notice_cost_logged_successfully: 'Custo unitário registado com êxito.'
+ notice_cost_logged_successfully: "Custo unitário registado com êxito."
permission_edit_cost_entries: "Editar custos unitários marcados"
permission_edit_own_cost_entries: "Editar custos unitários próprios marcados"
permission_edit_hourly_rates: "Editar taxas horárias"
@@ -135,6 +133,10 @@ pt-PT:
permission_view_own_hourly_rate: "Ver taxa horária própria"
permission_view_own_time_entries: "Ver tempo próprio gasto"
project_module_costs: "Tempo e custos"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Atribuir horas e custos reportados ao projeto"
text_destroy_cost_entries_question: "%{cost_entries} foram reportados nos pacotes de trabalho que está prestes a apagar. Tem a certeza que pretende apagar?"
text_destroy_time_and_cost_entries: "Apagar horas e custos reportados"
diff --git a/modules/costs/config/locales/crowdin/ro.yml b/modules/costs/config/locales/crowdin/ro.yml
index 6a5e9308a991..010f3a7134f8 100644
--- a/modules/costs/config/locales/crowdin/ro.yml
+++ b/modules/costs/config/locales/crowdin/ro.yml
@@ -86,8 +86,6 @@ ro:
label_cost_type_plural: "Tipuri de costuri"
label_cost_type_specific: "Tipul de cost #%{id}: %{name}"
label_costs_per_page: "Costuri pe pagină"
- label_currency: "Moneda"
- label_currency_format: "Formatul monedei"
label_current_default_rate: "Tarif implicit actual"
label_date_on: "pe"
label_deleted_cost_types: "Tipuri de costuri șterse"
@@ -121,7 +119,7 @@ ro:
notice_something_wrong: "Ceva n-a mers bine. Vă rugăm să încercați din nou."
notice_successful_restore: "Restaurare reușită."
notice_successful_lock: "Blocat cu succes."
- notice_cost_logged_successfully: 'Costul unitar a fost înregistrat cu succes.'
+ notice_cost_logged_successfully: "Costul unitar a fost înregistrat cu succes."
permission_edit_cost_entries: "Editarea costurilor unitare înregistrate"
permission_edit_own_cost_entries: "Editați propriile costuri unitare contabilizate"
permission_edit_hourly_rates: "Editați tarifele orare"
@@ -136,6 +134,10 @@ ro:
permission_view_own_hourly_rate: "Vezi propriul tarif orar"
permission_view_own_time_entries: "Vizualizează propriul timp consumat"
project_module_costs: "Timp și costuri"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Atribuiți orele și costurile raportate la proiect"
text_destroy_cost_entries_question: "%{cost_entries} au fost raportate pe pachetele de lucru pe care urmează să le ștergeți. Ce doriți să faceți?"
text_destroy_time_and_cost_entries: "Ștergeți orele și costurile raportate"
diff --git a/modules/costs/config/locales/crowdin/ru.yml b/modules/costs/config/locales/crowdin/ru.yml
index 3ba449eabe1e..0978d24827af 100644
--- a/modules/costs/config/locales/crowdin/ru.yml
+++ b/modules/costs/config/locales/crowdin/ru.yml
@@ -87,8 +87,6 @@ ru:
label_cost_type_plural: "Типы затрат"
label_cost_type_specific: "Затраты типа #%{id}: %{name}"
label_costs_per_page: "Расходов на странице"
- label_currency: "Валюта"
- label_currency_format: "Формат валюты"
label_current_default_rate: "Текущий тариф по умолчанию"
label_date_on: "на"
label_deleted_cost_types: "Типы удаленных расходов"
@@ -122,7 +120,7 @@ ru:
notice_something_wrong: "Произошла ошибка. Пожалуйста, попробуйте снова."
notice_successful_restore: "Успешное восстановление."
notice_successful_lock: "Успешно заблокировано."
- notice_cost_logged_successfully: 'Стоимость единицы успешно зажурналировано '
+ notice_cost_logged_successfully: "Стоимость единицы успешно зажурналировано "
permission_edit_cost_entries: "Править зарегестрированные количественные затраты"
permission_edit_own_cost_entries: "Править собственные зарегистрированные количественные затраты"
permission_edit_hourly_rates: "Править почасовые тарифы"
@@ -137,6 +135,10 @@ ru:
permission_view_own_hourly_rate: "Просмотр своей почасовой ставки"
permission_view_own_time_entries: "Просмотр своего отработанного времени"
project_module_costs: "Время и затраты"
+ setting_allow_tracking_start_and_end_times: "Разрешить пользователям отслеживать время начала и окончания учёта рабочего времени"
+ setting_costs_currency: "Валюта"
+ setting_costs_currency_format: "Формат валюты"
+ setting_enforce_tracking_start_and_end_times: "Обязательное время начала и окончания учёта рабочего времени"
text_assign_time_and_cost_entries_to_project: "Связать сообщенные часы и расходы с проектом"
text_destroy_cost_entries_question: "%{cost_entries} сообщили о рабочих пакетах, которые вы собираетесь удалить. Что вы собираетесь сделать?"
text_destroy_time_and_cost_entries: "Удалить отчеты о часах и расходах"
diff --git a/modules/costs/config/locales/crowdin/rw.yml b/modules/costs/config/locales/crowdin/rw.yml
index c357c060d7a5..f82dbbc274e9 100644
--- a/modules/costs/config/locales/crowdin/rw.yml
+++ b/modules/costs/config/locales/crowdin/rw.yml
@@ -85,8 +85,6 @@ rw:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ rw:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ rw:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/si.yml b/modules/costs/config/locales/crowdin/si.yml
index 70744b457af4..80ea91a43eca 100644
--- a/modules/costs/config/locales/crowdin/si.yml
+++ b/modules/costs/config/locales/crowdin/si.yml
@@ -85,8 +85,6 @@ si:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "මත"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ si:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ si:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/sk.yml b/modules/costs/config/locales/crowdin/sk.yml
index 9c5713b60a3c..13ca609e4a89 100644
--- a/modules/costs/config/locales/crowdin/sk.yml
+++ b/modules/costs/config/locales/crowdin/sk.yml
@@ -87,8 +87,6 @@ sk:
label_cost_type_plural: "Typy nákladov"
label_cost_type_specific: "Typ nákladu #%{id}: %{name}"
label_costs_per_page: "Náklady na stranu"
- label_currency: "Mena"
- label_currency_format: "Formát meny"
label_current_default_rate: "Aktuálna štandardná sadzba"
label_date_on: "dňa"
label_deleted_cost_types: "Odstránené typy nákladov"
@@ -122,7 +120,7 @@ sk:
notice_something_wrong: "Niečo sa pokazilo. Prosím skúste to znova."
notice_successful_restore: "Úspešné obnovenie."
notice_successful_lock: "Úspešne blokované."
- notice_cost_logged_successfully: 'Jednotkové náklady boli úspešne zaznamenané.'
+ notice_cost_logged_successfully: "Jednotkové náklady boli úspešne zaznamenané."
permission_edit_cost_entries: "Upraviť zaúčtované jednotkové náklady"
permission_edit_own_cost_entries: "Upraviť vlastné zaúčtované jednotkové náklady"
permission_edit_hourly_rates: "Upraviť hodinové sadzby"
@@ -137,6 +135,10 @@ sk:
permission_view_own_hourly_rate: "Zobraziť vlastnú hodinovú sadzbu"
permission_view_own_time_entries: "Zobraziť vlastný strávený čas"
project_module_costs: "Čas a náklady"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Priradiť výkaz hodín a náklady k projektu"
text_destroy_cost_entries_question: "%{cost_entries} boli hlásené na pracovných balíčkoch, ktoré chcete vymazať. Čo chcete robiť?"
text_destroy_time_and_cost_entries: "Vymazať ohlásené hodiny a náklady"
diff --git a/modules/costs/config/locales/crowdin/sl.yml b/modules/costs/config/locales/crowdin/sl.yml
index 1e134145aa60..908e453f9d5d 100644
--- a/modules/costs/config/locales/crowdin/sl.yml
+++ b/modules/costs/config/locales/crowdin/sl.yml
@@ -87,8 +87,6 @@ sl:
label_cost_type_plural: "Vrsta stroška"
label_cost_type_specific: "Vrsta stroška #%{id}: %{name}"
label_costs_per_page: "Strošek na stran"
- label_currency: "Valuta"
- label_currency_format: "Oblika valute"
label_current_default_rate: "Trenuten prevzeti tečaj\n"
label_date_on: "vključeno"
label_deleted_cost_types: "Izbrisane vrste stroškov"
@@ -122,7 +120,7 @@ sl:
notice_something_wrong: "nekaj je šlo narobe. Prosim poskusite ponovno."
notice_successful_restore: "Uspešna obnovitev."
notice_successful_lock: "Uspešno zaklenjeno."
- notice_cost_logged_successfully: 'Stroški na enoto so bili uspešno prijavljeni.'
+ notice_cost_logged_successfully: "Stroški na enoto so bili uspešno prijavljeni."
permission_edit_cost_entries: "Uredite rezervirane stroške na enoto"
permission_edit_own_cost_entries: "Uredite lastne rezervirane stroške na enoto"
permission_edit_hourly_rates: "Urejanje urnih postavk"
@@ -137,6 +135,10 @@ sl:
permission_view_own_hourly_rate: "Oglejte si lastno urno postavko"
permission_view_own_time_entries: "Oglejte si svoj porabljen čas"
project_module_costs: "Čas in stroški"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Projektu dodelite prijavljene ure in stroške"
text_destroy_cost_entries_question: "%{cost_entries} so bili prijavljeni v delovnih paketih, ki jih boste izbrisali. Kaj želiš narediti ?"
text_destroy_time_and_cost_entries: "Izbrišite prijavljene ure in stroške"
diff --git a/modules/costs/config/locales/crowdin/sr.yml b/modules/costs/config/locales/crowdin/sr.yml
index c330b878f818..fa3c1c8404f9 100644
--- a/modules/costs/config/locales/crowdin/sr.yml
+++ b/modules/costs/config/locales/crowdin/sr.yml
@@ -86,8 +86,6 @@ sr:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -121,7 +119,7 @@ sr:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -136,6 +134,10 @@ sr:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/sv.yml b/modules/costs/config/locales/crowdin/sv.yml
index 1462fe28c7c9..585ecb6fc7af 100644
--- a/modules/costs/config/locales/crowdin/sv.yml
+++ b/modules/costs/config/locales/crowdin/sv.yml
@@ -85,8 +85,6 @@ sv:
label_cost_type_plural: "Kostnadstyper"
label_cost_type_specific: "Kostnadstyp #%{id}: %{name}"
label_costs_per_page: "Kostnader per sida"
- label_currency: "Valuta"
- label_currency_format: "Formatet för valuta"
label_current_default_rate: "Nuvarande standardtimpris"
label_date_on: "den"
label_deleted_cost_types: "Borttagna kostnadstyper"
@@ -120,7 +118,7 @@ sv:
notice_something_wrong: "Ett fel inträffade. Försök igen."
notice_successful_restore: "Lyckad återställning."
notice_successful_lock: "Låst framgångsrikt."
- notice_cost_logged_successfully: 'Enhetskostnaden loggad framgångsrikt.'
+ notice_cost_logged_successfully: "Enhetskostnaden loggad framgångsrikt."
permission_edit_cost_entries: "Redigera bokade enhetskostnader"
permission_edit_own_cost_entries: "Redigera egna bokade enhetskostnader"
permission_edit_hourly_rates: "Redigera timpriser"
@@ -135,6 +133,10 @@ sv:
permission_view_own_hourly_rate: "Visa egna timpriser"
permission_view_own_time_entries: "Visa egen spenderad tid"
project_module_costs: "Tid och kostnader"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Tilldela rapporterade timmar och kostnader till projektet"
text_destroy_cost_entries_question: "%{cost_entries} rapporterades på de arbetspaket som du håller på och tar bort. Vad vill du göra?"
text_destroy_time_and_cost_entries: "Ta bort rapporterade timmar och kostnader"
diff --git a/modules/costs/config/locales/crowdin/th.yml b/modules/costs/config/locales/crowdin/th.yml
index f3b63fdc65b8..bf919dc07b02 100644
--- a/modules/costs/config/locales/crowdin/th.yml
+++ b/modules/costs/config/locales/crowdin/th.yml
@@ -84,8 +84,6 @@ th:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -119,7 +117,7 @@ th:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -134,6 +132,10 @@ th:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/tr.yml b/modules/costs/config/locales/crowdin/tr.yml
index 7ea1a75fe2f0..806057884f52 100644
--- a/modules/costs/config/locales/crowdin/tr.yml
+++ b/modules/costs/config/locales/crowdin/tr.yml
@@ -85,8 +85,6 @@ tr:
label_cost_type_plural: "Maliyet türleri"
label_cost_type_specific: "Maliyet türü #%{id}: %{name}"
label_costs_per_page: "Her sayfa için maliyet"
- label_currency: "Kur"
- label_currency_format: "Para biriminin biçimi"
label_current_default_rate: "Güncel varsayılan oran"
label_date_on: "açık"
label_deleted_cost_types: "Silinmiş maliyet türleri"
@@ -120,7 +118,7 @@ tr:
notice_something_wrong: "Bir şeyler yanlış gitti. Lütfen tekrar deneyin."
notice_successful_restore: "Başarılı bir geri yükleme."
notice_successful_lock: "Başarıyla kilitlendi."
- notice_cost_logged_successfully: 'Birim maliyet başarıyla kaydedildi.'
+ notice_cost_logged_successfully: "Birim maliyet başarıyla kaydedildi."
permission_edit_cost_entries: "Rezervasyon birimi maliyetlerini düzenle"
permission_edit_own_cost_entries: "Kendi rezervasyonu birim maliyetlerini düzenleme"
permission_edit_hourly_rates: "Saatlik oranları düzenleme"
@@ -135,6 +133,10 @@ tr:
permission_view_own_hourly_rate: "Kendi saatlik ücreti görüntüleyin"
permission_view_own_time_entries: "Kendi harcanan zamanı görüntüleyin"
project_module_costs: "Zaman ve maliyetler"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Raporlanan saatleri ve maliyetleri projeye tahsis edin"
text_destroy_cost_entries_question: "%{cost_entries} silmek üzere olduğunuz iş paketleri hakkında rapor edildi. Ne yapmak istiyorsun ?"
text_destroy_time_and_cost_entries: "Raporlanan saat ve masrafları sil"
diff --git a/modules/costs/config/locales/crowdin/uk.yml b/modules/costs/config/locales/crowdin/uk.yml
index 1a8f9c06a9f0..fe80e32144a8 100644
--- a/modules/costs/config/locales/crowdin/uk.yml
+++ b/modules/costs/config/locales/crowdin/uk.yml
@@ -87,8 +87,6 @@ uk:
label_cost_type_plural: "Види витрат"
label_cost_type_specific: "Витрати типу #%{id}: %{name}"
label_costs_per_page: "Витрати на сторінку"
- label_currency: "Валюта"
- label_currency_format: "Формат валюти"
label_current_default_rate: "Поточна ставка за промовчанням"
label_date_on: "на"
label_deleted_cost_types: "Видалені типи витрат"
@@ -122,7 +120,7 @@ uk:
notice_something_wrong: "Щось пішло не так. Будь ласка спробуйте ще раз."
notice_successful_restore: "Успішне відновлення."
notice_successful_lock: "Успішно заблоковано."
- notice_cost_logged_successfully: 'Вартість одиниць успішно зареєстрована.'
+ notice_cost_logged_successfully: "Вартість одиниць успішно зареєстрована."
permission_edit_cost_entries: "Відредагувати витрати на одиницю"
permission_edit_own_cost_entries: "Редагування власних витрат на одиницю"
permission_edit_hourly_rates: "Редагування погодинних ставок"
@@ -137,6 +135,10 @@ uk:
permission_view_own_hourly_rate: "Переглянути власну погодинну ставку"
permission_view_own_time_entries: "Переглянути власний витрачений час"
project_module_costs: "Час і витрати"
+ setting_allow_tracking_start_and_end_times: "Дозволити користувачам відстежувати початок і кінець часу на записах часу"
+ setting_costs_currency: "Валюта"
+ setting_costs_currency_format: "Формат валюти"
+ setting_enforce_tracking_start_and_end_times: "Примусити користувачів встановлювати час початку і закінчення на записах часу"
text_assign_time_and_cost_entries_to_project: "Призначення повідомлених годин і витрат проекту"
text_destroy_cost_entries_question: "%{cost_entries} повідомлялися про робочі пакети, які ви збираєтеся видалити. Що ти хочеш робити ?"
text_destroy_time_and_cost_entries: "Видаліть звітні години та витрати"
diff --git a/modules/costs/config/locales/crowdin/uz.yml b/modules/costs/config/locales/crowdin/uz.yml
index 09fa665a1416..f7cb884bd593 100644
--- a/modules/costs/config/locales/crowdin/uz.yml
+++ b/modules/costs/config/locales/crowdin/uz.yml
@@ -85,8 +85,6 @@ uz:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -120,7 +118,7 @@ uz:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
permission_edit_hourly_rates: "Edit hourly rates"
@@ -135,6 +133,10 @@ uz:
permission_view_own_hourly_rate: "View own hourly rate"
permission_view_own_time_entries: "View own spent time"
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/locales/crowdin/vi.yml b/modules/costs/config/locales/crowdin/vi.yml
index 873edb3077bd..5224ff0d4e56 100644
--- a/modules/costs/config/locales/crowdin/vi.yml
+++ b/modules/costs/config/locales/crowdin/vi.yml
@@ -84,8 +84,6 @@ vi:
label_cost_type_plural: "Các loại chi phí"
label_cost_type_specific: "Loại chi phí #%{id}: %{name}"
label_costs_per_page: "Chi phí trên mỗi trang"
- label_currency: "Tiền tệ"
- label_currency_format: "Định dạng tiền tệ"
label_current_default_rate: "Tỷ lệ mặc định hiện tại"
label_date_on: "vào"
label_deleted_cost_types: "Các loại chi phí đã xóa"
@@ -119,7 +117,7 @@ vi:
notice_something_wrong: "Đã xảy ra lỗi. Vui lòng thử lại."
notice_successful_restore: "Khôi phục thành công."
notice_successful_lock: "Khóa thành công."
- notice_cost_logged_successfully: 'Chi phí đơn vị đã được ghi lại thành công.'
+ notice_cost_logged_successfully: "Chi phí đơn vị đã được ghi lại thành công."
permission_edit_cost_entries: "Chỉnh sửa chi phí đơn vị đã ghi"
permission_edit_own_cost_entries: "Chỉnh sửa chi phí đơn vị đã ghi của chính mình"
permission_edit_hourly_rates: "Chỉnh sửa tỷ lệ theo giờ"
@@ -134,6 +132,10 @@ vi:
permission_view_own_hourly_rate: "Xem tỷ lệ theo giờ của chính mình"
permission_view_own_time_entries: "Xem thời gian đã tiêu của chính mình"
project_module_costs: "Thời gian và chi phí"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
text_assign_time_and_cost_entries_to_project: "Gán giờ và chi phí đã báo cáo cho dự án"
text_destroy_cost_entries_question: "%{cost_entries} đã được báo cáo trên các gói công việc bạn sắp xóa. Bạn muốn làm gì?"
text_destroy_time_and_cost_entries: "Xóa giờ và chi phí đã báo cáo"
diff --git a/modules/costs/config/locales/crowdin/zh-CN.yml b/modules/costs/config/locales/crowdin/zh-CN.yml
index 28c3fae54777..0c9a931ecc2e 100644
--- a/modules/costs/config/locales/crowdin/zh-CN.yml
+++ b/modules/costs/config/locales/crowdin/zh-CN.yml
@@ -84,8 +84,6 @@ zh-CN:
label_cost_type_plural: "成本类型"
label_cost_type_specific: "成本类型 #%{id}:%{name}"
label_costs_per_page: "每页的成本"
- label_currency: "货币"
- label_currency_format: "货币格式"
label_current_default_rate: "当前基本费率"
label_date_on: "日期"
label_deleted_cost_types: "已删除的成本类型"
@@ -119,7 +117,7 @@ zh-CN:
notice_something_wrong: "出错了。请重试。"
notice_successful_restore: "成功恢复。"
notice_successful_lock: "锁定成功。"
- notice_cost_logged_successfully: '单位成本记录成功。'
+ notice_cost_logged_successfully: "单位成本记录成功。"
permission_edit_cost_entries: "编辑记录的单位成本"
permission_edit_own_cost_entries: "编辑自己记录的单位成本"
permission_edit_hourly_rates: "编辑小时费率"
@@ -134,6 +132,10 @@ zh-CN:
permission_view_own_hourly_rate: "查看自己的小时费率"
permission_view_own_time_entries: "查看自己的耗时"
project_module_costs: "工时和成本"
+ setting_allow_tracking_start_and_end_times: "允许用户跟踪时间记录的开始和结束时间"
+ setting_costs_currency: "货币"
+ setting_costs_currency_format: "货币格式"
+ setting_enforce_tracking_start_and_end_times: "强制用户设置时间记录的开始和结束时间"
text_assign_time_and_cost_entries_to_project: "将已上报的工时和成本提交到项目中"
text_destroy_cost_entries_question: "您要删除的工作包已经上报了 %{cost_entries}。您想进行哪种操作?"
text_destroy_time_and_cost_entries: "删除已上报的工时和成本"
diff --git a/modules/costs/config/locales/crowdin/zh-TW.yml b/modules/costs/config/locales/crowdin/zh-TW.yml
index 54ebc1f9f9d2..8920221aa598 100644
--- a/modules/costs/config/locales/crowdin/zh-TW.yml
+++ b/modules/costs/config/locales/crowdin/zh-TW.yml
@@ -84,8 +84,6 @@ zh-TW:
label_cost_type_plural: "費用類別"
label_cost_type_specific: "成本類型 #%{id}:%{name}"
label_costs_per_page: "每頁的成本"
- label_currency: "貨幣"
- label_currency_format: " 貨幣格式"
label_current_default_rate: "目前基本費率"
label_date_on: "開啟"
label_deleted_cost_types: "已刪除的成本類型"
@@ -119,7 +117,7 @@ zh-TW:
notice_something_wrong: "發生錯誤,請重試。"
notice_successful_restore: "復原成功。"
notice_successful_lock: "停用成功。"
- notice_cost_logged_successfully: '單位成本紀錄成功。'
+ notice_cost_logged_successfully: "單位成本紀錄成功。"
permission_edit_cost_entries: "編輯自己紀錄的單位成本"
permission_edit_own_cost_entries: "編輯自己紀錄的單位成本"
permission_edit_hourly_rates: "編輯小時費率"
@@ -134,6 +132,10 @@ zh-TW:
permission_view_own_hourly_rate: "查看自己的小時費率"
permission_view_own_time_entries: "查看自己的耗時"
project_module_costs: "時間與費用"
+ setting_allow_tracking_start_and_end_times: ""
+ setting_costs_currency: "貨幣"
+ setting_costs_currency_format: " 貨幣格式"
+ setting_enforce_tracking_start_and_end_times: "強制使用者在時間記錄上設定開始和結束時間"
text_assign_time_and_cost_entries_to_project: "將已回報的小時數和成本提交到專案中"
text_destroy_cost_entries_question: "您要刪除的工作項目已經回報了 %{cost_entries}。您想如何進行?"
text_destroy_time_and_cost_entries: "刪除已回報的小時數和成本"
diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml
index a143a2933008..9fee7d5e047a 100644
--- a/modules/costs/config/locales/en.yml
+++ b/modules/costs/config/locales/en.yml
@@ -99,8 +99,6 @@ en:
label_cost_type_plural: "Cost types"
label_cost_type_specific: "Cost type #%{id}: %{name}"
label_costs_per_page: "Costs per page"
- label_currency: "Currency"
- label_currency_format: "Format of currency"
label_current_default_rate: "Current default rate"
label_date_on: "on"
label_deleted_cost_types: "Deleted cost types"
@@ -135,7 +133,7 @@ en:
notice_something_wrong: "Something went wrong. Please try again."
notice_successful_restore: "Successful restore."
notice_successful_lock: "Locked successfully."
- notice_cost_logged_successfully: 'Unit cost logged successfully.'
+ notice_cost_logged_successfully: "Unit cost logged successfully."
permission_edit_cost_entries: "Edit booked unit costs"
permission_edit_own_cost_entries: "Edit own booked unit costs"
@@ -153,6 +151,11 @@ en:
project_module_costs: "Time and costs"
+ setting_allow_tracking_start_and_end_times: "Allow users to track start and end time on time records"
+ setting_costs_currency: "Currency"
+ setting_costs_currency_format: "Format of currency"
+ setting_enforce_tracking_start_and_end_times: "Force users to set start and end time on time records"
+
text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
text_destroy_time_and_cost_entries: "Delete reported hours and costs"
diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb
index 67904a56e56f..1619251f6eab 100644
--- a/modules/costs/config/routes.rb
+++ b/modules/costs/config/routes.rb
@@ -61,4 +61,11 @@
# TODO: this is a duplicate from a route defined under project/:project_id, check whether we really want to do that
resources :hourly_rates, only: %i[edit update]
+
+ scope :admin do
+ resource :costs,
+ only: %i[show update],
+ controller: :costs_settings,
+ as: "admin_costs_settings"
+ end
end
diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb
index f51fed3c2dc3..553062b13386 100644
--- a/modules/costs/lib/costs/engine.rb
+++ b/modules/costs/lib/costs/engine.rb
@@ -37,15 +37,7 @@ class Engine < ::Rails::Engine
register "costs",
author_url: "https://www.openproject.org",
bundled: true,
- settings: {
- default: { "costs_currency" => "EUR", "costs_currency_format" => "%n %u" },
- partial: "settings/costs",
- page_title_key: :label_setting_plural,
- breadcrumb_elements: -> {
- [{ href: admin_settings_show_plugin_path(:costs), text: I18n.t(:project_module_costs) }]
- },
- menu_item: :costs_setting
- } do
+ settings: { menu_item: :costs_settings } do
project_module :costs do
permission :view_time_entries,
{},
@@ -124,6 +116,21 @@ class Engine < ::Rails::Engine
end
# Menu extensions
+ menu :admin_menu,
+ :admin_costs,
+ { controller: "/costs_settings", action: :show },
+ if: Proc.new { User.current.admin? },
+ caption: :project_module_costs,
+ after: :enterprise,
+ icon: "op-cost-reports"
+
+ menu :admin_menu,
+ :costs_settings,
+ { controller: "/costs_settings", action: :show },
+ if: Proc.new { User.current.admin? },
+ caption: :label_setting_plural,
+ parent: :admin_costs
+
menu :admin_menu,
:cost_types,
{ controller: "/cost_types", action: "index" },
@@ -132,6 +139,18 @@ class Engine < ::Rails::Engine
caption: :label_cost_type_plural
end
+ initializer "costs.settings" do
+ ::Settings::Definition.add "costs_currency", default: "EUR", format: :string
+ ::Settings::Definition.add "costs_currency_format", default: "%n %u", format: :string
+ ::Settings::Definition.add "allow_tracking_start_and_end_times", default: false, format: :boolean
+ ::Settings::Definition.add "enforce_tracking_start_and_end_times", default: false, format: :boolean
+ end
+
+ initializer "costs.feature_decisions" do
+ OpenProject::FeatureDecisions.add :track_start_and_end_times_for_time_entries,
+ description: "Allows admins to enable tracking start and end times for time entries"
+ end
+
activity_provider :time_entries, class_name: "Activities::TimeEntryActivityProvider", default: false
patches %i[Project User PermittedParams]
diff --git a/modules/costs/lib/costs/patches/number_to_currency_converter_patch.rb b/modules/costs/lib/costs/patches/number_to_currency_converter_patch.rb
index 7343ab9dfc3f..6a7489fb39b0 100644
--- a/modules/costs/lib/costs/patches/number_to_currency_converter_patch.rb
+++ b/modules/costs/lib/costs/patches/number_to_currency_converter_patch.rb
@@ -33,8 +33,8 @@ def self.included(base) # :nodoc:
module InstanceMethods
def i18n_opts
- super.merge(unit: ERB::Util.h(Setting.plugin_costs["costs_currency"]),
- format: ERB::Util.h(Setting.plugin_costs["costs_currency_format"]))
+ super.merge(unit: ERB::Util.h(Setting.costs_currency),
+ format: ERB::Util.h(Setting.costs_currency_format))
end
end
end
diff --git a/modules/costs/lib/costs/query_currency_select.rb b/modules/costs/lib/costs/query_currency_select.rb
index f4d21aa6b19c..795da6bc500b 100644
--- a/modules/costs/lib/costs/query_currency_select.rb
+++ b/modules/costs/lib/costs/query_currency_select.rb
@@ -57,7 +57,8 @@ def real_value(work_package)
Queries::WorkPackages::Selects::WorkPackageSelect
.scoped_column_sum(scope,
"COALESCE(ROUND(SUM(cost_entries_sum), 2)::FLOAT, 0.0) material_costs",
- grouped && query.group_by_statement)
+ grouped:,
+ query:)
}
},
labor_costs: {
@@ -70,7 +71,8 @@ def real_value(work_package)
Queries::WorkPackages::Selects::WorkPackageSelect
.scoped_column_sum(scope,
"COALESCE(ROUND(SUM(time_entries_sum), 2)::FLOAT, 0.0) labor_costs",
- grouped && query.group_by_statement)
+ grouped:,
+ query:)
}
},
overall_costs: {
diff --git a/modules/costs/spec/lib/costs/query_currency_select_spec.rb b/modules/costs/spec/lib/costs/query_currency_select_spec.rb
index 93da8b5e9886..90794f8d2985 100644
--- a/modules/costs/spec/lib/costs/query_currency_select_spec.rb
+++ b/modules/costs/spec/lib/costs/query_currency_select_spec.rb
@@ -86,20 +86,19 @@
query = double("query")
result = double("result")
- allow(query)
- .to receive(:results)
- .and_return result
-
allow(result)
.to receive(:work_packages)
.and_return(WorkPackage.all)
- allow(query)
- .to receive(:group_by_statement)
- .and_return("author_id")
+ allow(query).to receive_messages(
+ results: result,
+ group_by_join_statement: nil,
+ group_by_select: "author_id",
+ group_by_statement: "author_id"
+ )
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).columns)
- .to match_array %w(id material_costs)
+ .to match_array %w(group_id material_costs)
end
end
end
@@ -125,20 +124,19 @@
query = double("query")
result = double("result")
- allow(query)
- .to receive(:results)
- .and_return result
-
allow(result)
.to receive(:work_packages)
.and_return(WorkPackage.all)
- allow(query)
- .to receive(:group_by_statement)
- .and_return("author_id")
+ allow(query).to receive_messages(
+ results: result,
+ group_by_join_statement: nil,
+ group_by_select: "author_id",
+ group_by_statement: "author_id"
+ )
expect(ActiveRecord::Base.connection.select_all(instance.summable.(query, true).to_sql).columns)
- .to match_array %w(id labor_costs)
+ .to match_array %w(group_id labor_costs)
end
end
end
diff --git a/modules/costs/spec/models/time_entry_spec.rb b/modules/costs/spec/models/time_entry_spec.rb
index 4fa4b63e93cf..8f3fa58884d8 100644
--- a/modules/costs/spec/models/time_entry_spec.rb
+++ b/modules/costs/spec/models/time_entry_spec.rb
@@ -52,13 +52,16 @@
let!(:default_hourly_three) { create(:default_hourly_rate, valid_from: 4.days.ago, project:, user: user2) }
let!(:default_hourly_five) { create(:default_hourly_rate, valid_from: 6.days.ago, project:, user: user2) }
let(:hours) { 5.0 }
+ let(:start_time) { 10 * 60 } # 10:00
let(:time_entry) do
create(:time_entry,
project:,
work_package:,
spent_on: date,
hours:,
+ start_time: start_time,
user:,
+ time_zone: user.time_zone,
rate: hourly_one,
comments: "lorem")
end
@@ -74,7 +77,7 @@
comments: "lorem")
end
- def is_member(project, user, permissions)
+ def ensure_membership(project, user, permissions)
create(:member,
project:,
user:,
@@ -296,7 +299,7 @@ def is_member(project, user, permissions)
describe "WHEN the time_entry is assigned to the user " \
"WHEN the user has the view_own_hourly_rate permission" do
before do
- is_member(project, user, [:view_own_hourly_rate])
+ ensure_membership(project, user, [:view_own_hourly_rate])
time_entry.user = user
end
@@ -307,7 +310,7 @@ def is_member(project, user, permissions)
describe "WHEN the time_entry is assigned to the user " \
"WHEN the user lacks permissions" do
before do
- is_member(project, user, [])
+ ensure_membership(project, user, [])
time_entry.user = user
end
@@ -318,7 +321,7 @@ def is_member(project, user, permissions)
describe "WHEN the time_entry is assigned to another user " \
"WHEN the user has the view_hourly_rates permission" do
before do
- is_member(project, user2, [:view_hourly_rates])
+ ensure_membership(project, user2, [:view_hourly_rates])
time_entry.user = user
end
@@ -329,7 +332,7 @@ def is_member(project, user, permissions)
describe "WHEN the time_entry is assigned to another user " \
"WHEN the user has the view_hourly_rates permission in another project" do
before do
- is_member(project2, user2, [:view_hourly_rates])
+ ensure_membership(project2, user2, [:view_hourly_rates])
time_entry.user = user
end
@@ -342,7 +345,7 @@ def is_member(project, user, permissions)
describe "visible_by?" do
context "when not having the necessary permissions" do
before do
- is_member(project, user, [])
+ ensure_membership(project, user, [])
end
it "is visible" do
@@ -352,7 +355,7 @@ def is_member(project, user, permissions)
context "when having the view_time_entries permission" do
before do
- is_member(project, user, [:view_time_entries])
+ ensure_membership(project, user, [:view_time_entries])
end
it "is visible" do
@@ -363,7 +366,7 @@ def is_member(project, user, permissions)
context "when having the view_own_time_entries permission " \
"and being the owner of the time entry" do
before do
- is_member(project, user, [:view_own_time_entries])
+ ensure_membership(project, user, [:view_own_time_entries])
time_entry.user = user
end
@@ -376,7 +379,7 @@ def is_member(project, user, permissions)
context "when having the view_own_time_entries permission " \
"and not being the owner of the time entry" do
before do
- is_member(project, user, [:view_own_time_entries])
+ ensure_membership(project, user, [:view_own_time_entries])
time_entry.user = build :user
end
@@ -386,4 +389,155 @@ def is_member(project, user, permissions)
end
end
end
+
+ describe ".can_track_start_and_end_time?" do
+ context "with the feature flag enabled", with_flag: { track_start_and_end_times_for_time_entries: true } do
+ context "with the setting enabled", with_settings: { allow_tracking_start_and_end_times: true } do
+ it { expect(described_class).to be_can_track_start_and_end_time }
+ end
+
+ context "with the setting disabled", with_settings: { allow_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_can_track_start_and_end_time }
+ end
+ end
+
+ context "with the feature flag disabled", with_flag: { track_start_and_end_times_for_time_entries: false } do
+ context "with the setting enabled", with_settings: { allow_tracking_start_and_end_times: true } do
+ it { expect(described_class).not_to be_can_track_start_and_end_time }
+ end
+
+ context "with the setting disabled", with_settings: { allow_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_can_track_start_and_end_time }
+ end
+ end
+ end
+
+ describe "validations" do
+ describe "start_time" do
+ it "allows blank values" do
+ time_entry.start_time = nil
+ expect(time_entry).to be_valid
+ end
+
+ it "allows integer values between 0 and 1440" do
+ time_entry.start_time = (5 * 60) + 30
+ expect(time_entry).to be_valid
+ end
+
+ it "does not allow non integer values" do
+ time_entry.start_time = 1.5
+ expect(time_entry).not_to be_valid
+ end
+
+ it "does not allow negative values" do
+ time_entry.start_time = -42
+ expect(time_entry).not_to be_valid
+ end
+ end
+
+ describe "start_time and end_time" do
+ context "when enforcing times" do
+ before do
+ allow(described_class).to receive(:must_track_start_and_end_time?).and_return(true)
+ end
+
+ it "validates that both values are present" do
+ time_entry.start_time = nil
+
+ expect(time_entry).not_to be_valid
+
+ time_entry.start_time = 10 * 60
+
+ expect(time_entry).to be_valid
+ end
+ end
+ end
+ end
+
+ describe "#start_timestamp" do
+ it "returns nil if start_time is nil" do
+ time_entry.start_time = nil
+ expect(time_entry.start_timestamp).to be_nil
+ end
+
+ it "returns nil if timezone is nil" do
+ time_entry.time_zone = nil
+ expect(time_entry.start_timestamp).to be_nil
+ end
+
+ it "generates a proper timestamp from the stored information" do
+ time_entry.start_time = 14 * 60
+ time_entry.spent_on = Date.new(2024, 12, 24)
+ time_entry.time_zone = "America/Los_Angeles"
+
+ expect(time_entry.start_timestamp.iso8601).to eq("2024-12-24T14:00:00-08:00")
+ end
+ end
+
+ describe "#end_timestamp" do
+ it "returns nil if start_time is nil" do
+ time_entry.start_time = nil
+ expect(time_entry.end_timestamp).to be_nil
+ end
+
+ it "returns nil if timezone is nil" do
+ time_entry.time_zone = nil
+ expect(time_entry.end_timestamp).to be_nil
+ end
+
+ it "generates a proper timestamp from the stored information" do
+ time_entry.start_time = 8 * 60
+ time_entry.hours = 2.5
+ time_entry.spent_on = Date.new(2024, 12, 24)
+ time_entry.time_zone = "America/Los_Angeles"
+
+ expect(time_entry.end_timestamp.iso8601).to eq("2024-12-24T10:30:00-08:00")
+ end
+ end
+
+ describe ".must_track_start_and_end_time?" do
+ context "with the feature flag enabled", with_flag: { track_start_and_end_times_for_time_entries: true } do
+ context "with the allow setting enabled", with_settings: { allow_tracking_start_and_end_times: true } do
+ context "with the enforce setting enabled", with_settings: { enforce_tracking_start_and_end_times: true } do
+ it { expect(described_class).to be_must_track_start_and_end_time }
+ end
+
+ context "with the enforce setting disabled", with_settings: { enforce_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+ end
+
+ context "with the allow setting disabled", with_settings: { allow_tracking_start_and_end_times: false } do
+ context "with the enforce setting enabled", with_settings: { enforce_tracking_start_and_end_times: true } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+
+ context "with the enforce setting disabled", with_settings: { enforce_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+ end
+ end
+
+ context "with the feature flag disabled", with_flag: { track_start_and_end_times_for_time_entries: false } do
+ context "with the allow setting enabled", with_settings: { allow_tracking_start_and_end_times: true } do
+ context "with the enforce setting enabled", with_settings: { enforce_tracking_start_and_end_times: true } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+
+ context "with the enforce setting disabled", with_settings: { enforce_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+ end
+
+ context "with the allow setting disabled", with_settings: { allow_tracking_start_and_end_times: false } do
+ context "with the enforce setting enabled", with_settings: { enforce_tracking_start_and_end_times: true } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+
+ context "with the enforce setting disabled", with_settings: { enforce_tracking_start_and_end_times: false } do
+ it { expect(described_class).not_to be_must_track_start_and_end_time }
+ end
+ end
+ end
+ end
end
diff --git a/modules/gantt/spec/features/timeline/timeline_navigation_spec.rb b/modules/gantt/spec/features/timeline/timeline_navigation_spec.rb
index 072ec2ccb69e..6bbad8cbe1e1 100644
--- a/modules/gantt/spec/features/timeline/timeline_navigation_spec.rb
+++ b/modules/gantt/spec/features/timeline/timeline_navigation_spec.rb
@@ -363,7 +363,7 @@
split_view = wp_table.open_split_view(wp_cat1)
split_view.switch_to_tab tab: :relations
- relations.remove_relation(wp_cat2)
+ relations.remove_relation(relation)
# Relation should be removed in TL
within(".work-packages-split-view--tabletimeline-side") do
diff --git a/modules/github_integration/spec/workers/cron/check_deploy_status_job_spec.rb b/modules/github_integration/spec/workers/cron/check_deploy_status_job_spec.rb
index 754952e99a1b..5843b87e0404 100644
--- a/modules/github_integration/spec/workers/cron/check_deploy_status_job_spec.rb
+++ b/modules/github_integration/spec/workers/cron/check_deploy_status_job_spec.rb
@@ -101,7 +101,7 @@
{
"Accept" => "*/*",
"Accept-Encoding" => "gzip, deflate",
- "User-Agent" => "httpx.rb/1.3.1"
+ "User-Agent" => "httpx.rb/1.3.4"
}
end
diff --git a/modules/job_status/config/locales/crowdin/ro.yml b/modules/job_status/config/locales/crowdin/ro.yml
index bf271db0545a..3f6a392f691f 100644
--- a/modules/job_status/config/locales/crowdin/ro.yml
+++ b/modules/job_status/config/locales/crowdin/ro.yml
@@ -14,8 +14,8 @@ ro:
errors: 'Something went wrong'
generic_messages:
not_found: 'This job could not be found.'
- in_queue: 'The job has been queued and will be processed shortly.'
- in_process: 'The job is currently being processed.'
+ in_queue: 'Lucrarea a fost pusă în coadă și va fi procesată în scurt timp.'
+ in_process: 'Lucrarea este în curs de procesare.'
error: 'The job has failed to complete.'
cancelled: 'The job has been cancelled due to an error.'
success: 'The job completed successfully.'
diff --git a/modules/meeting/app/components/meeting_agenda_items/item_component/show_component.html.erb b/modules/meeting/app/components/meeting_agenda_items/item_component/show_component.html.erb
index 544eace1f353..0a6b32f1d99f 100644
--- a/modules/meeting/app/components/meeting_agenda_items/item_component/show_component.html.erb
+++ b/modules/meeting/app/components/meeting_agenda_items/item_component/show_component.html.erb
@@ -9,19 +9,7 @@
grid.with_area(:content, tag: :span, mr: 2) do
if @meeting_agenda_item.visible_work_package?
- flex_layout(align_items: :center) do |flex|
- flex.with_column(mr: 2) do
- render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do
- "##{@meeting_agenda_item.work_package.id} #{@meeting_agenda_item.work_package.type.name}"
- end
- end
-
- flex.with_column(mr: 2) do
- render(Primer::Beta::Label.new(font_weight: :bold)) do
- @meeting_agenda_item.work_package.status.name
- end
- end
- end
+ render(WorkPackages::InfoLineComponent.new(work_package: @meeting_agenda_item.work_package))
elsif @meeting_agenda_item.linked_work_package?
render(Primer::Beta::Text.new(font_size: :small, mr: 2, color: :subtle, test_selector: 'op-meeting-agenda-title')) do
I18n.t(:label_agenda_item_undisclosed_wp, id: @meeting_agenda_item.work_package_id)
diff --git a/modules/meeting/app/components/meetings/index/dialog_component.rb b/modules/meeting/app/components/meetings/index/dialog_component.rb
index 1b8edb028109..ad90c65469d1 100644
--- a/modules/meeting/app/components/meetings/index/dialog_component.rb
+++ b/modules/meeting/app/components/meetings/index/dialog_component.rb
@@ -52,7 +52,7 @@ def render?
end
def title
- @type == :new ? I18n.t("label_meeting_new_one_time") : "Copy meeting"
+ @type == :new ? I18n.t("label_meeting_new_dynamic") : "Copy meeting"
end
end
end
diff --git a/modules/meeting/app/components/meetings/index_sub_header_component.html.erb b/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
index 30d5ea1f23e3..dc6622c3c428 100644
--- a/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
+++ b/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
@@ -23,16 +23,16 @@
I18n.t(:label_meeting)
end
- menu.with_item(label: I18n.t("meeting.types.classic"),
- tag: :a,
- href: dynamic_path
- )
-
menu.with_item(label: I18n.t("meeting.types.structured"),
tag: :a,
href: new_dialog_meetings_path(project_id: @project&.id),
content_arguments: { data: { controller: "async-dialog" }}
)
+
+ menu.with_item(label: I18n.t("meeting.types.classic"),
+ tag: :a,
+ href: dynamic_path
+ )
end
end
end
diff --git a/modules/meeting/app/components/meetings/row_component.rb b/modules/meeting/app/components/meetings/row_component.rb
index 8878632c2d2e..0e5778dfbfe9 100644
--- a/modules/meeting/app/components/meetings/row_component.rb
+++ b/modules/meeting/app/components/meetings/row_component.rb
@@ -30,14 +30,6 @@
module Meetings
class RowComponent < ::OpPrimer::BorderBoxRowComponent
- def column_args(column)
- if column == :title
- { style: "grid-column: span 2" }
- else
- super
- end
- end
-
def project_name
helpers.link_to_project model.project, {}, {}, false
end
diff --git a/modules/meeting/app/components/meetings/table_component.rb b/modules/meeting/app/components/meetings/table_component.rb
index 0759ad0e5f63..cd18de9b8913 100644
--- a/modules/meeting/app/components/meetings/table_component.rb
+++ b/modules/meeting/app/components/meetings/table_component.rb
@@ -32,7 +32,13 @@ module Meetings
class TableComponent < ::OpPrimer::BorderBoxTableComponent
options :current_project # used to determine if displaying the projects column
- columns :title, :project_name, :start_time, :duration, :location
+ columns :title, :start_time, :project_name, :duration, :location
+
+ mobile_columns :title, :start_time, :project_name
+
+ mobile_labels :project_name
+
+ main_column :title
def sortable?
true
@@ -46,19 +52,15 @@ def has_actions?
true
end
- def header_args(column)
- if column == :title
- { style: "grid-column: span 2" }
- else
- super
- end
+ def mobile_title
+ I18n.t(:label_meeting_plural)
end
def headers
@headers ||= [
[:title, { caption: Meeting.human_attribute_name(:title) }],
- current_project.blank? ? [:project_name, { caption: Meeting.human_attribute_name(:project) }] : nil,
[:start_time, { caption: I18n.t(:label_meeting_date_and_time) }],
+ current_project.blank? ? [:project_name, { caption: Meeting.human_attribute_name(:project) }] : nil,
[:duration, { caption: Meeting.human_attribute_name(:duration) }],
[:location, { caption: Meeting.human_attribute_name(:location) }]
].compact
diff --git a/modules/meeting/app/forms/meeting/project_autocompleter.rb b/modules/meeting/app/forms/meeting/project_autocompleter.rb
index 608044a5fdd5..93257c1bfcc2 100644
--- a/modules/meeting/app/forms/meeting/project_autocompleter.rb
+++ b/modules/meeting/app/forms/meeting/project_autocompleter.rb
@@ -32,6 +32,7 @@ class Meeting::ProjectAutocompleter < ApplicationForm
name: "project_id",
id: "project_id",
label: Project.model_name.human,
+ required: true,
data: {
"test-selector": "project_id"
},
diff --git a/modules/meeting/config/locales/crowdin/af.yml b/modules/meeting/config/locales/crowdin/af.yml
index 2064c0ed612f..f5751724939d 100644
--- a/modules/meeting/config/locales/crowdin/af.yml
+++ b/modules/meeting/config/locales/crowdin/af.yml
@@ -88,7 +88,7 @@ af:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ af:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ar.yml b/modules/meeting/config/locales/crowdin/ar.yml
index 006b6669be38..5ab9ee39754a 100644
--- a/modules/meeting/config/locales/crowdin/ar.yml
+++ b/modules/meeting/config/locales/crowdin/ar.yml
@@ -92,7 +92,7 @@ ar:
label_meeting: "الاجتماع"
label_meeting_plural: "الاجتماعات"
label_meeting_new: "اجتماع جديد"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "تحرير الاجتماع"
@@ -150,7 +150,7 @@ ar:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "نسخ من الاجتماع #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/az.yml b/modules/meeting/config/locales/crowdin/az.yml
index ce078cdb842a..5c3246c015e5 100644
--- a/modules/meeting/config/locales/crowdin/az.yml
+++ b/modules/meeting/config/locales/crowdin/az.yml
@@ -88,7 +88,7 @@ az:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ az:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/be.yml b/modules/meeting/config/locales/crowdin/be.yml
index 3a15dcc6b9ec..337ee325e0e5 100644
--- a/modules/meeting/config/locales/crowdin/be.yml
+++ b/modules/meeting/config/locales/crowdin/be.yml
@@ -90,7 +90,7 @@ be:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -148,7 +148,7 @@ be:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/bg.yml b/modules/meeting/config/locales/crowdin/bg.yml
index 5e0e0724d547..b7198519d89b 100644
--- a/modules/meeting/config/locales/crowdin/bg.yml
+++ b/modules/meeting/config/locales/crowdin/bg.yml
@@ -88,7 +88,7 @@ bg:
label_meeting: "среща"
label_meeting_plural: "Срещи"
label_meeting_new: "Нова среща"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Редактиране на срещата"
@@ -146,7 +146,7 @@ bg:
types:
classic: "Класически"
classic_text: "Организирайте срещата си в текстова програма и протокол, които могат да се форматират."
- structured: "One-time"
+ structured: "Динамичен"
structured_text: "Организирайте срещата си като списък с точки от дневния ред, като по желание ги свържете с работен пакет."
structured_text_copy: "Копирането на среща понастоящем не копира свързаните с нея точки от дневния ред, а само подробностите."
copied: "Копирано от среща #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ca.yml b/modules/meeting/config/locales/crowdin/ca.yml
index 548fbcba244d..f57353189a01 100644
--- a/modules/meeting/config/locales/crowdin/ca.yml
+++ b/modules/meeting/config/locales/crowdin/ca.yml
@@ -88,7 +88,7 @@ ca:
label_meeting: "Reunió"
label_meeting_plural: "Reunions"
label_meeting_new: "Nova reunió"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Editar reunió"
@@ -146,7 +146,7 @@ ca:
types:
classic: "Clàssica"
classic_text: "Organitzeu la vostra reunió en una agenda i un protocol de text formatejables."
- structured: "One-time"
+ structured: "Dinàmica"
structured_text: "Organitza la vostra reunió com una llista d'elements de l'agenda, enllaçant-los opcionalment a un paquet de treball."
structured_text_copy: "Actualment en copiar una reunió no es copiaran els elements associats a l'agenda de la reunió, només els detalls."
copied: "Copiat de la reunió #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ckb-IR.yml b/modules/meeting/config/locales/crowdin/ckb-IR.yml
index 49db2f2f9a77..b119a6077922 100644
--- a/modules/meeting/config/locales/crowdin/ckb-IR.yml
+++ b/modules/meeting/config/locales/crowdin/ckb-IR.yml
@@ -88,7 +88,7 @@ ckb-IR:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ ckb-IR:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/cs.yml b/modules/meeting/config/locales/crowdin/cs.yml
index 30d4f7db1bfb..a639f804bca8 100644
--- a/modules/meeting/config/locales/crowdin/cs.yml
+++ b/modules/meeting/config/locales/crowdin/cs.yml
@@ -90,19 +90,19 @@ cs:
label_meeting: "Schůzka"
label_meeting_plural: "Schůzky"
label_meeting_new: "Nová schůzka"
- label_meeting_new_one_time: "New one-time meeting"
- label_meeting_create: "Create meeting"
- label_meeting_copy: "Copy meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
+ label_meeting_create: "Vytvořit schůzku"
+ label_meeting_copy: "Kopírovat schůzku"
label_meeting_edit: "Upravit schůzku"
label_meeting_agenda: "Agenda"
label_meeting_minutes: "Zápisy"
label_meeting_close: "Zavřít"
label_meeting_open: "Otevřít"
- label_meeting_index_delete: "Delete"
- label_meeting_open_this_meeting: "Open this meeting"
+ label_meeting_index_delete: "Smazat"
+ label_meeting_open_this_meeting: "Otevřít schůzku"
label_meeting_agenda_close: "Uzavřete agendu pro vytvoření zápisu"
label_meeting_date_time: "Datum/Čas"
- label_meeting_date_and_time: "Date and time"
+ label_meeting_date_and_time: "Datum a čas"
label_meeting_diff: "Rozdíl"
label_upcoming_meetings: "Nadcházející schůzky"
label_past_meetings: "Minulé schůzky"
@@ -148,7 +148,7 @@ cs:
types:
classic: "Klasické"
classic_text: "Uspořádat schůzku do formátů textového programu a protokolu."
- structured: "One-time"
+ structured: "Dynamický"
structured_text: "Uspořádat schůzku jako seznam bodů pořadu jednání, případně je propojit s pracovním balíčkem."
structured_text_copy: "Kopírování schůzky v současné době nezkopíruje související body pořadu jednání, jen podrobnosti"
copied: "Zkopírováno ze schůzky #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/da.yml b/modules/meeting/config/locales/crowdin/da.yml
index 23998667b6a3..a2ad61aea25f 100644
--- a/modules/meeting/config/locales/crowdin/da.yml
+++ b/modules/meeting/config/locales/crowdin/da.yml
@@ -88,7 +88,7 @@ da:
label_meeting: "Møde"
label_meeting_plural: "Møder"
label_meeting_new: "Nyt møde"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Rediger møde"
@@ -146,7 +146,7 @@ da:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/de.yml b/modules/meeting/config/locales/crowdin/de.yml
index e9c3ebc0d0c5..ed4346a8b702 100644
--- a/modules/meeting/config/locales/crowdin/de.yml
+++ b/modules/meeting/config/locales/crowdin/de.yml
@@ -88,19 +88,19 @@ de:
label_meeting: "Besprechung"
label_meeting_plural: "Besprechungen"
label_meeting_new: "Neue Besprechung"
- label_meeting_new_one_time: "New one-time meeting"
- label_meeting_create: "Create meeting"
- label_meeting_copy: "Copy meeting"
+ label_meeting_new_dynamic: "Neue dynamische Besprechung"
+ label_meeting_create: "Besprechung erstellen"
+ label_meeting_copy: "Besprechung kopieren"
label_meeting_edit: "Besprechung bearbeiten"
label_meeting_agenda: "Agenda"
label_meeting_minutes: "Protokoll"
label_meeting_close: "Schließen"
label_meeting_open: "Öffnen"
- label_meeting_index_delete: "Delete"
- label_meeting_open_this_meeting: "Open this meeting"
+ label_meeting_index_delete: "Löschen"
+ label_meeting_open_this_meeting: "Diese Besprechung öffnen"
label_meeting_agenda_close: "Agenda schließen um mit dem Protokoll zu beginnen"
label_meeting_date_time: "Datum/Uhrzeit"
- label_meeting_date_and_time: "Date and time"
+ label_meeting_date_and_time: "Datum und Zeit"
label_meeting_diff: "Differenz"
label_upcoming_meetings: "Zukünftige Meetings"
label_past_meetings: "Vergangene Meetings"
@@ -125,14 +125,14 @@ de:
attachments: "Anhänge kopieren"
attachments_text: "Alle angehängten Dateien in die neue Besprechung kopieren"
agenda: "Tagesordnung kopieren"
- agenda_items: "Copy agenda items"
+ agenda_items: "Tagesordnungspunkte kopieren"
agenda_text: "Tagesordnung der alten Besprechung kopieren"
- participants: "Copy list of participants"
+ participants: "Liste der Teilnehmer kopieren"
email:
send_emails: "E-Mail an Teilnehmer"
send_invitation_emails: >
Senden Sie sofort eine E-Mail Einladung an die oben ausgewählten Teilnehmer. Sie können dies auch jederzeit manuell durchführen.
- send_invitation_emails_structured: "Send an email invitation immediately to all participants. You can also do this manually at any time later."
+ send_invitation_emails_structured: "Sofortige E-Mail-Einladung an alle Teilnehmer senden. Sie können dies auch zu einem späteren Zeitpunkt manuell tun."
open_meeting_link: "Besprechung öffnen"
invited:
summary: "%{actor} hat Ihnen eine Einladung für die folgende Besprechung gesendet: %{title}"
@@ -146,7 +146,7 @@ de:
types:
classic: "Klassisch"
classic_text: "Organisieren Sie Ihr Meeting in einer formatierbaren textbasierter Agenda und zugehöriges Protokoll."
- structured: "One-time"
+ structured: "Dynamisch"
structured_text: "Organisieren Sie Ihr Meeting als strukturierte Liste von Einträgen und verknüpfen Sie diese optional mit einem Arbeitspaket."
structured_text_copy: "Das Kopieren einer Besprechung kopiert derzeit nicht die zugehörigen Tagesordnungspunkte, sondern nur dessen Details"
copied: "Kopiert von Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/el.yml b/modules/meeting/config/locales/crowdin/el.yml
index e3074c3c5201..4fa09c0bca59 100644
--- a/modules/meeting/config/locales/crowdin/el.yml
+++ b/modules/meeting/config/locales/crowdin/el.yml
@@ -88,7 +88,7 @@ el:
label_meeting: "Συνάντηση"
label_meeting_plural: "Συναντήσεις"
label_meeting_new: "Νέα Συνάντηση"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Επεξεργασία Συνάντησης"
@@ -146,7 +146,7 @@ el:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Αντιγράφηκε από τη Συνάντηση #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/eo.yml b/modules/meeting/config/locales/crowdin/eo.yml
index 4e0a36e59685..6ec66eb951d3 100644
--- a/modules/meeting/config/locales/crowdin/eo.yml
+++ b/modules/meeting/config/locales/crowdin/eo.yml
@@ -88,7 +88,7 @@ eo:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ eo:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/es.yml b/modules/meeting/config/locales/crowdin/es.yml
index b3073f0168ac..ce25bf7e8061 100644
--- a/modules/meeting/config/locales/crowdin/es.yml
+++ b/modules/meeting/config/locales/crowdin/es.yml
@@ -88,7 +88,7 @@ es:
label_meeting: "Reunión"
label_meeting_plural: "Reuniones"
label_meeting_new: "Nueva reunión"
- label_meeting_new_one_time: "Nueva reunión única"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Crear reunión"
label_meeting_copy: "Copiar reunión"
label_meeting_edit: "Editar reunión"
@@ -146,7 +146,7 @@ es:
types:
classic: "Clásico"
classic_text: "Organice su reunión en un formato de agenda de texto y protocolos."
- structured: "Único"
+ structured: "Dinámico"
structured_text: "Organice su reunión como una lista de temas del orden del día, enlazándolos opcionalmente a un paquete de trabajo."
structured_text_copy: "Copiar una reunión no copiará actualmente los elementos asociados de la agenda de la reunión, solo los detalles"
copied: "Copiado de la reunión %{id}"
diff --git a/modules/meeting/config/locales/crowdin/et.yml b/modules/meeting/config/locales/crowdin/et.yml
index bebeadc9fb0b..e9e6871d9ed5 100644
--- a/modules/meeting/config/locales/crowdin/et.yml
+++ b/modules/meeting/config/locales/crowdin/et.yml
@@ -88,7 +88,7 @@ et:
label_meeting: "Koosolek"
label_meeting_plural: "Koosolekud"
label_meeting_new: "Uus koosolek"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Muuda koosolekut"
@@ -146,7 +146,7 @@ et:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/eu.yml b/modules/meeting/config/locales/crowdin/eu.yml
index a2e0573e989f..eebd6f557c53 100644
--- a/modules/meeting/config/locales/crowdin/eu.yml
+++ b/modules/meeting/config/locales/crowdin/eu.yml
@@ -88,7 +88,7 @@ eu:
label_meeting: "Hitzordua"
label_meeting_plural: "Hitzorduak"
label_meeting_new: "Hitzordu berria"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Aldatu hitzordua"
@@ -146,7 +146,7 @@ eu:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "#%{id} Hitzordutik kopiatua"
diff --git a/modules/meeting/config/locales/crowdin/fa.yml b/modules/meeting/config/locales/crowdin/fa.yml
index 30e4206c3652..985d2533cc93 100644
--- a/modules/meeting/config/locales/crowdin/fa.yml
+++ b/modules/meeting/config/locales/crowdin/fa.yml
@@ -88,7 +88,7 @@ fa:
label_meeting: "جلسه"
label_meeting_plural: "جلسات"
label_meeting_new: "ایجاد جلسه"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "ویرایش جلسه"
@@ -146,7 +146,7 @@ fa:
types:
classic: "کلاسیک"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/fi.yml b/modules/meeting/config/locales/crowdin/fi.yml
index 7685f7350c66..0790680f4fb5 100644
--- a/modules/meeting/config/locales/crowdin/fi.yml
+++ b/modules/meeting/config/locales/crowdin/fi.yml
@@ -88,7 +88,7 @@ fi:
label_meeting: "Kokous"
label_meeting_plural: "Kokoukset"
label_meeting_new: "Uusi kokous"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Muokkaa kokousta"
@@ -146,7 +146,7 @@ fi:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Kopioitu kokouksesta #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/fil.yml b/modules/meeting/config/locales/crowdin/fil.yml
index 5847adca15e5..02489014a133 100644
--- a/modules/meeting/config/locales/crowdin/fil.yml
+++ b/modules/meeting/config/locales/crowdin/fil.yml
@@ -88,7 +88,7 @@ fil:
label_meeting: "Pagpupulong"
label_meeting_plural: "Mga Pagpupulong"
label_meeting_new: "Bagong Pagpupulong"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "I-edit and Pagpupulong"
@@ -146,7 +146,7 @@ fil:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/fr.yml b/modules/meeting/config/locales/crowdin/fr.yml
index 160830151e93..9eb430858890 100644
--- a/modules/meeting/config/locales/crowdin/fr.yml
+++ b/modules/meeting/config/locales/crowdin/fr.yml
@@ -88,7 +88,7 @@ fr:
label_meeting: "Réunion"
label_meeting_plural: "Réunions"
label_meeting_new: "Nouvelle réunion"
- label_meeting_new_one_time: "Nouvelle réunion ponctuelle"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Créer une réunion"
label_meeting_copy: "Copier la réunion"
label_meeting_edit: "Modifier la réunion"
@@ -146,7 +146,7 @@ fr:
types:
classic: "Classique"
classic_text: "Organisez votre réunion sous la forme d'un ordre du jour et d'un protocole en texte formatable."
- structured: "Unique"
+ structured: "Dynamique"
structured_text: "Organisez votre réunion sous la forme d'une liste de points à l'ordre du jour, en les reliant éventuellement à des lots de travaux."
structured_text_copy: "Actuellement, la copie d'une réunion n'entraîne pas la copie des points de l'ordre du jour de la réunion, mais uniquement des détails."
copied: "Copié depuis la réunion #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/he.yml b/modules/meeting/config/locales/crowdin/he.yml
index 88be16178cf9..54059bcc68d1 100644
--- a/modules/meeting/config/locales/crowdin/he.yml
+++ b/modules/meeting/config/locales/crowdin/he.yml
@@ -90,7 +90,7 @@ he:
label_meeting: "פגישה"
label_meeting_plural: "פגישות"
label_meeting_new: "פגישה חדשה"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "עריכת פגישה"
@@ -148,7 +148,7 @@ he:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/hi.yml b/modules/meeting/config/locales/crowdin/hi.yml
index 7f4df0b250fc..d3cbba6bf0bf 100644
--- a/modules/meeting/config/locales/crowdin/hi.yml
+++ b/modules/meeting/config/locales/crowdin/hi.yml
@@ -88,7 +88,7 @@ hi:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ hi:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/hr.yml b/modules/meeting/config/locales/crowdin/hr.yml
index e9ab9f64781c..9e0969894827 100644
--- a/modules/meeting/config/locales/crowdin/hr.yml
+++ b/modules/meeting/config/locales/crowdin/hr.yml
@@ -89,7 +89,7 @@ hr:
label_meeting: "Sastanak"
label_meeting_plural: "Sastanci"
label_meeting_new: "Novi sastanak"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Uredite sastanak"
@@ -147,7 +147,7 @@ hr:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/hu.yml b/modules/meeting/config/locales/crowdin/hu.yml
index b10b9f13dec7..afa961479bf8 100644
--- a/modules/meeting/config/locales/crowdin/hu.yml
+++ b/modules/meeting/config/locales/crowdin/hu.yml
@@ -88,7 +88,7 @@ hu:
label_meeting: "Megbeszélés"
label_meeting_plural: "Megbeszélések"
label_meeting_new: "Új megbeszélés"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Megbeszélés szerkesztése"
@@ -146,7 +146,7 @@ hu:
types:
classic: "Szokásos"
classic_text: "Szervezd meg a találkozódat formázott szöveges napirendben és protokollban."
- structured: "One-time"
+ structured: "Dinamikus"
structured_text: "Szervezd meg a találkozódat napirendi pontokkal, melyeket opcionálisan munkacsomagokhoz rendelhetsz."
structured_text_copy: "A megbeszélés másolásával jelnleg nem fogja átmásolni a hozzákapcsolt napirendi pontokat, csak a megbeszélés részleteit."
copied: "#%{id} megbeszélésből másolva"
diff --git a/modules/meeting/config/locales/crowdin/id.yml b/modules/meeting/config/locales/crowdin/id.yml
index ed835198b090..116b82a2cba2 100644
--- a/modules/meeting/config/locales/crowdin/id.yml
+++ b/modules/meeting/config/locales/crowdin/id.yml
@@ -87,7 +87,7 @@ id:
label_meeting: "Rapat"
label_meeting_plural: "Rapat"
label_meeting_new: "Rapat Baru"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Rapat"
@@ -145,7 +145,7 @@ id:
types:
classic: "Klasik"
classic_text: "Atur rapat Anda dalam agenda dan protokol teks yang dapat diformat."
- structured: "One-time"
+ structured: "Dinamis"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Disalin dari Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/it.yml b/modules/meeting/config/locales/crowdin/it.yml
index f956cf230abe..6f6bba17c1dd 100644
--- a/modules/meeting/config/locales/crowdin/it.yml
+++ b/modules/meeting/config/locales/crowdin/it.yml
@@ -88,7 +88,7 @@ it:
label_meeting: "Riunione"
label_meeting_plural: "Riunioni"
label_meeting_new: "Nuova riunione"
- label_meeting_new_one_time: "Nuova riunione una tantum"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Crea riunione"
label_meeting_copy: "Copia riunione"
label_meeting_edit: "Modifica riunione"
@@ -146,7 +146,7 @@ it:
types:
classic: "Classica"
classic_text: "Organizza la tua riunione in un'agenda e un protocollo di testo formattabile."
- structured: "Una tantum"
+ structured: "Dinamica"
structured_text: "Organizza la tua riunione come un elenco di punti all'ordine del giorno, collegandoli facoltativamente a un pacchetto di lavoro."
structured_text_copy: "La copia di una riunione al momento non copierà gli elementi dell'ordine del giorno della riunione, ma solo i dettagli"
copied: "Copiato dalla riunione #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ja.yml b/modules/meeting/config/locales/crowdin/ja.yml
index a56e18bd2811..dfa3e09c76e7 100644
--- a/modules/meeting/config/locales/crowdin/ja.yml
+++ b/modules/meeting/config/locales/crowdin/ja.yml
@@ -28,7 +28,7 @@ ja:
activerecord:
attributes:
meeting:
- type: "Meeting type"
+ type: ""
location: "場所"
duration: "期間"
notes: "注記"
@@ -57,7 +57,7 @@ ja:
meeting_agenda_item: "Agenda item"
meeting_agenda: "アジェンダ"
meeting_minutes: "議事録"
- meeting_section: "Section"
+ meeting_section: "セクション"
activity:
filter:
meeting: "会議"
@@ -87,7 +87,7 @@ ja:
label_meeting: "会議"
label_meeting_plural: "会議"
label_meeting_new: "新しい会議"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "会議を編集"
@@ -132,7 +132,7 @@ ja:
send_invitation_emails: >
Send an email invitation immediately to the participants selected above. You can also do this manually at any time later.
send_invitation_emails_structured: "Send an email invitation immediately to all participants. You can also do this manually at any time later."
- open_meeting_link: "Open meeting"
+ open_meeting_link: "会議を開く"
invited:
summary: "%{actor} has sent you an invitation for the meeting %{title}"
rescheduled:
@@ -145,10 +145,10 @@ ja:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
- structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
- copied: "ミーティング#%{id}からコピーしました"
+ structured_text_copy: "会議をコピーする場合、現在は関連する議題項目はコピーされず、詳細のみがコピーされます。"
+ copied: "ミーティング#%{id} からコピーしました"
meeting_section:
untitled_title: "Untitled section"
delete_confirmation: "Deleting the section will also delete all of its agenda items. Are you sure you want to do this?"
diff --git a/modules/meeting/config/locales/crowdin/js-ja.yml b/modules/meeting/config/locales/crowdin/js-ja.yml
index 0e11b465830e..f48b8d41e5c1 100644
--- a/modules/meeting/config/locales/crowdin/js-ja.yml
+++ b/modules/meeting/config/locales/crowdin/js-ja.yml
@@ -24,4 +24,4 @@ ja:
label_meetings: '会議'
work_packages:
tabs:
- meetings: 'Meetings'
+ meetings: '会議'
diff --git a/modules/meeting/config/locales/crowdin/ka.yml b/modules/meeting/config/locales/crowdin/ka.yml
index 176b5802d8c5..983a4841713e 100644
--- a/modules/meeting/config/locales/crowdin/ka.yml
+++ b/modules/meeting/config/locales/crowdin/ka.yml
@@ -88,7 +88,7 @@ ka:
label_meeting: "შეხვედრა"
label_meeting_plural: "შეხვედრები"
label_meeting_new: "ახალი შეხვედრა"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "შეხვედრის ჩასწორება"
@@ -146,7 +146,7 @@ ka:
types:
classic: "კლასიკური"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "დინამიკური"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/kk.yml b/modules/meeting/config/locales/crowdin/kk.yml
index e41c4e9f430c..46369b644ecd 100644
--- a/modules/meeting/config/locales/crowdin/kk.yml
+++ b/modules/meeting/config/locales/crowdin/kk.yml
@@ -88,7 +88,7 @@ kk:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ kk:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ko.yml b/modules/meeting/config/locales/crowdin/ko.yml
index 9f9629389219..314ed4b2eec4 100644
--- a/modules/meeting/config/locales/crowdin/ko.yml
+++ b/modules/meeting/config/locales/crowdin/ko.yml
@@ -87,7 +87,7 @@ ko:
label_meeting: "미팅"
label_meeting_plural: "미팅"
label_meeting_new: "새 미팅"
- label_meeting_new_one_time: "새로운 일회성 미팅"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "미팅 생성"
label_meeting_copy: "미팅 복사"
label_meeting_edit: "미팅 편집"
@@ -145,7 +145,7 @@ ko:
types:
classic: "클래식"
classic_text: "서식 지정 가능한 텍스트 의제 및 프로토콜로 미팅을 구성하세요."
- structured: "일회성"
+ structured: "다이내믹"
structured_text: "미팅을 의제 항목 목록으로 구성하고, 선택적으로 작업 패키지에 연결합니다."
structured_text_copy: "미팅을 복사하면 관련 미팅 의제 항목은 현재 복사되지 않고 세부 정보만 복사됩니다"
copied: "미팅 #%{id}에서 복사됨"
diff --git a/modules/meeting/config/locales/crowdin/lt.yml b/modules/meeting/config/locales/crowdin/lt.yml
index 696f070563d3..b726114eddbf 100644
--- a/modules/meeting/config/locales/crowdin/lt.yml
+++ b/modules/meeting/config/locales/crowdin/lt.yml
@@ -90,7 +90,7 @@ lt:
label_meeting: "Pasitarimas"
label_meeting_plural: "Pasitarimai"
label_meeting_new: "Naujas pasitarimas"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Redaguoti pasitarimą"
@@ -148,7 +148,7 @@ lt:
types:
classic: "Klasikinis"
classic_text: "Organizuokite jūsų susitikimą su formatuojamo teksto darbotvarke ir protokolu."
- structured: "One-time"
+ structured: "Dinaminis"
structured_text: "Organizuokite jūsų susitikimą su darbotvarkės elementų sąrašu, pasirinktinai susiejant juos su darbo paketu."
structured_text_copy: "Šiuo metu nukopijavus susitikimą, susiję susitikimo elmentai nebus kopijuojami, tik detalės."
copied: "Nukopijuota iš susitikimo #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/lv.yml b/modules/meeting/config/locales/crowdin/lv.yml
index 89846a3e051b..b3e2a4ac5f46 100644
--- a/modules/meeting/config/locales/crowdin/lv.yml
+++ b/modules/meeting/config/locales/crowdin/lv.yml
@@ -85,33 +85,33 @@ lv:
meeting_agenda_opened: Sanāksmes darba kārtības atklāšana
meeting_minutes: Sanāksmju protokolu rediģēšana
meeting_minutes_created: Izveidots sanāksmes protokols
- error_notification_with_errors: "Failed to send notification. The following recipients could not be notified: %{recipients}"
+ error_notification_with_errors: "Neizdevās nosūtīt paziņojumu. Nebija iespējams sasniegt šādus adresātus: %{recipients}"
label_meeting: "Sanāksmes"
label_meeting_plural: "Sanāksmes"
- label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
- label_meeting_create: "Create meeting"
- label_meeting_copy: "Copy meeting"
- label_meeting_edit: "Edit Meeting"
+ label_meeting_new: "Jauna sanāksme"
+ label_meeting_new_dynamic: "Jauna dinamiska sanāksme"
+ label_meeting_create: "Izveidot sanāksmi"
+ label_meeting_copy: "Kopēt sanāksmi"
+ label_meeting_edit: "Rediģēt sanāksmi"
label_meeting_agenda: "Darba kārtība"
label_meeting_minutes: "Protokols"
label_meeting_close: "Aizvērt"
label_meeting_open: "Atvērts"
- label_meeting_index_delete: "Delete"
- label_meeting_open_this_meeting: "Open this meeting"
+ label_meeting_index_delete: "Dzēst"
+ label_meeting_open_this_meeting: "Atklāt šo sanāksmi"
label_meeting_agenda_close: "Close the agenda to begin the Minutes"
- label_meeting_date_time: "Date/Time"
- label_meeting_date_and_time: "Date and time"
- label_meeting_diff: "Diff"
- label_upcoming_meetings: "Upcoming meetings"
- label_past_meetings: "Past meetings"
- label_upcoming_meetings_short: "Upcoming"
- label_past_meetings_short: "Past"
+ label_meeting_date_time: "Datums/laiks"
+ label_meeting_date_and_time: "Datums un laiks"
+ label_meeting_diff: "Atšķirība"
+ label_upcoming_meetings: "Gaidāmās sanāksmes"
+ label_past_meetings: "Iepriekšējās sanāksmes"
+ label_upcoming_meetings_short: "Gaidāmās"
+ label_past_meetings_short: "Iepriekšējās"
label_involvement: "Involvement"
- label_upcoming_invitations: "Upcoming invitations"
- label_past_invitations: "Past invitations"
- label_attendee: "Attendee"
- label_author: "Creator"
+ label_upcoming_invitations: "Nākotnes ielūgumi"
+ label_past_invitations: "Iepriekšējie ielūgumi"
+ label_attendee: "Dalībnieks"
+ label_author: "Izveidotājs"
label_notify: "Send for review"
label_icalendar: "Send iCalendar"
label_icalendar_download: "Download iCalendar event"
@@ -145,9 +145,9 @@ lv:
new_date_time: "New date/time"
label_mail_all_participants: "Send email to all participants"
types:
- classic: "Classic"
+ classic: "Klasisks"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
@@ -156,7 +156,7 @@ lv:
delete_confirmation: "Deleting the section will also delete all of its agenda items. Are you sure you want to do this?"
placeholder_title: "New section"
empty_text: "Drag items here or create a new one"
- notice_successful_notification: "Notification sent successfully"
+ notice_successful_notification: "Paziņojums veiksmīgi nosūtīts"
notice_timezone_missing: No time zone is set and %{zone} is assumed. To choose your time zone, please click here.
notice_meeting_updated: "This page has been updated by someone else. Reload to view changes."
permission_create_meetings: "Create meetings"
diff --git a/modules/meeting/config/locales/crowdin/mn.yml b/modules/meeting/config/locales/crowdin/mn.yml
index 87e9878ddb40..4f2f1368c44c 100644
--- a/modules/meeting/config/locales/crowdin/mn.yml
+++ b/modules/meeting/config/locales/crowdin/mn.yml
@@ -88,7 +88,7 @@ mn:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ mn:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ms.yml b/modules/meeting/config/locales/crowdin/ms.yml
index f548219e7083..f51059ec5326 100644
--- a/modules/meeting/config/locales/crowdin/ms.yml
+++ b/modules/meeting/config/locales/crowdin/ms.yml
@@ -87,7 +87,7 @@ ms:
label_meeting: "Mesyuarat"
label_meeting_plural: "Mesyuarat-mesyuarat"
label_meeting_new: "Mesyuarat Baharu"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Mesyuarat"
@@ -145,7 +145,7 @@ ms:
types:
classic: "Klasik"
classic_text: "Aturkan mesyuarat anda dalam agenda teks yang boleh diformat dan diprotokol."
- structured: "One-time"
+ structured: "Dinamik"
structured_text: "Aturkan mesyuarat anda sebagai senarai butiran agenda, pautkannya ke pakej kerja secara pilihan."
structured_text_copy: "Menyalin mesyuarat pada masa ini tidak akan menyalin item agenda mesyuarat yang berkaitan, hanya butiran sahaja"
copied: "Disalin daripada Mesyuarat #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ne.yml b/modules/meeting/config/locales/crowdin/ne.yml
index af13a1f8227a..c8b90e056a1a 100644
--- a/modules/meeting/config/locales/crowdin/ne.yml
+++ b/modules/meeting/config/locales/crowdin/ne.yml
@@ -88,7 +88,7 @@ ne:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ ne:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/nl.yml b/modules/meeting/config/locales/crowdin/nl.yml
index b2802136f5e1..e26043e0b8ff 100644
--- a/modules/meeting/config/locales/crowdin/nl.yml
+++ b/modules/meeting/config/locales/crowdin/nl.yml
@@ -71,7 +71,7 @@ nl:
updated: "veranderd van %{old_value} naar %{value}"
updated_html: "gewijzigd van %{old_value} naar %{value}"
position:
- updated: "reordered"
+ updated: "herschikken"
work_package:
updated: "veranderd van %{old_value} naar %{value}"
updated_html: "gewijzigd van %{old_value} naar %{value}"
@@ -88,19 +88,19 @@ nl:
label_meeting: "Vergadering"
label_meeting_plural: "Vergaderingen"
label_meeting_new: "Nieuwe vergadering"
- label_meeting_new_one_time: "New one-time meeting"
- label_meeting_create: "Create meeting"
- label_meeting_copy: "Copy meeting"
+ label_meeting_new_dynamic: "Nieuwe dynamische vergadering"
+ label_meeting_create: "Creëer vergadering"
+ label_meeting_copy: "Kopie vergadering"
label_meeting_edit: "Vergadering bewerken"
label_meeting_agenda: "Agenda"
label_meeting_minutes: "Notulen"
label_meeting_close: "Sluiten"
label_meeting_open: "Open"
- label_meeting_index_delete: "Delete"
- label_meeting_open_this_meeting: "Open this meeting"
+ label_meeting_index_delete: "Verwijderen"
+ label_meeting_open_this_meeting: "Open deze vergadering"
label_meeting_agenda_close: "Sluit de agenda om de notulen te beginnen"
label_meeting_date_time: "Datum/Tijd"
- label_meeting_date_and_time: "Date and time"
+ label_meeting_date_and_time: "Datum en tijd"
label_meeting_diff: "Diff"
label_upcoming_meetings: "Geplande vergaderingen"
label_past_meetings: "Eerdere vergaderingen"
@@ -119,20 +119,20 @@ nl:
label_start_date: "Startdatum"
meeting:
attachments:
- text: "Attached files are available to all meeting participants. You can also drag and drop these into agenda item notes."
+ text: "Bijgevoegde bestanden zijn beschikbaar voor alle deelnemers aan de vergadering. U kunt deze ook naar agendapunt notities slepen."
copy:
title: "Kopieer vergadering: %{title}"
attachments: "Bijlagen kopiëren"
- attachments_text: "Copy over all attached files to the new meeting"
+ attachments_text: "Kopieer alle bijgevoegde bestanden naar de nieuwe vergadering"
agenda: "Agenda kopiëren"
- agenda_items: "Copy agenda items"
+ agenda_items: "Agendapunten kopiëren"
agenda_text: "Kopieer de agenda van de oude vergadering"
- participants: "Copy list of participants"
+ participants: "Kopieer lijst van deelnemers"
email:
send_emails: "Email participants"
send_invitation_emails: >
- Send an email invitation immediately to the participants selected above. You can also do this manually at any time later.
- send_invitation_emails_structured: "Send an email invitation immediately to all participants. You can also do this manually at any time later."
+ Stuur onmiddellijk een e-mailuitnodiging naar de hierboven geselecteerde deelnemers. U kunt dit ook later op elk gewenst moment handmatig doen.
+ send_invitation_emails_structured: "Stuur onmiddellijk een e-mailuitnodiging naar de hierboven geselecteerde deelnemers. U kunt dit ook later op elk gewenst moment handmatig doen."
open_meeting_link: "Open vergadering"
invited:
summary: "%{actor} heeft je een uitnodiging gestuurd voor de meeting %{title}"
@@ -146,7 +146,7 @@ nl:
types:
classic: "Klassiek"
classic_text: "Organiseer uw vergadering met een opmaakbare tekstagenda en protocol."
- structured: "One-time"
+ structured: "Dynamisch"
structured_text: "Organiseren van uw vergadering als een lijst met agendapunten, eventueel koppelen aan een werkpakket."
structured_text_copy: "Een vergadering kopiëren zal momenteel niet de bijbehorende agendapunten kopiëren, alleen de details"
copied: "Gekopieerd van vergadering #%{id}"
@@ -154,10 +154,10 @@ nl:
untitled_title: "Naamloze sectie"
delete_confirmation: "Deleting the section will also delete all of its agenda items. Are you sure you want to do this?"
placeholder_title: "Nieuwe Sectie"
- empty_text: "Drag items here or create a new one"
+ empty_text: "Sleep items hierheen of maak een nieuwe"
notice_successful_notification: "Notificatie succesvol verzonden"
notice_timezone_missing: Geen tijdzone is ingesteld en %{zone} is aangenomen. Om uw tijdzone te kiezen, klik dan hier.
- notice_meeting_updated: "This page has been updated by someone else. Reload to view changes."
+ notice_meeting_updated: "Deze pagina is door iemand anders bijgewerkt. Herlaad om wijzigingen te bekijken."
permission_create_meetings: "Creëer vergaderingen"
permission_edit_meetings: "Vergaderingen bewerken"
permission_delete_meetings: "Verwijder vergaderingen"
@@ -181,7 +181,7 @@ nl:
text_meeting_minutes_for_meeting: 'minuten voor de vergadering "%{meeting}"'
text_notificiation_invited: "Deze mail bevat een ics item voor de vergadering hieronder:"
text_meeting_empty_heading: "Je vergadering is leeg"
- text_meeting_empty_description_1: "Start by adding agenda items below. Each item can be as simple as just a title, but you can also add additional details like duration and notes."
+ text_meeting_empty_description_1: "Begin met het toevoegen van agendapunten hieronder. Elk item kan zo eenvoudig zijn als alleen een titel, maar u kunt ook extra gegevens toevoegen, zoals duur en aantekeningen."
text_meeting_empty_description_2: 'Je kunt ook verwijzingen naar bestaande werkpakketten toevoegen. Wanneer je dit doet, zullen gerelateerde notities automatisch zichtbaar zijn in het tabblad "Vergaderingen" van het werkpakket.'
label_meeting_empty_action: "Agendapunt toevoegen"
label_meeting_actions: "Vergadering acties"
@@ -189,9 +189,9 @@ nl:
label_meeting_delete: "Verwijder vergadering"
label_meeting_created_by: "Gemaakt door"
label_meeting_last_updated: "Laatst bijgewerkt"
- label_meeting_reload: "Reload"
+ label_meeting_reload: "Herladen"
label_agenda_items: "Agendapunten"
- label_agenda_items_reordered: "reordered"
+ label_agenda_items_reordered: "herschikken"
label_agenda_item_remove: "Van de agenda schrappen"
label_agenda_item_undisclosed_wp: "Werkpakket #%{id} niet zichtbaar"
label_agenda_item_deleted_wp: "Verwijderde verwijzing naar het werkpakket"
@@ -227,9 +227,9 @@ nl:
label_add_work_package_to_meeting_dialog_title: "Werkpakket aan vergadering toevoegen"
label_add_work_package_to_meeting_dialog_button: "Toevoegen aan vergadering"
label_meeting_selection_caption: "It's only possible to add this work package to upcoming or ongoing open meetings."
- text_add_work_package_to_meeting_description: "A work package can be added to one or multiple meetings for discussion. Any notes concerning it are also visible here."
+ text_add_work_package_to_meeting_description: "Een werkpakket kan worden toegevoegd aan één of meerdere vergaderingen voor bespreking. Eventuele notities zijn hier ook zichtbaar."
text_agenda_item_no_notes: "Geen aantekeningen voorzien"
text_agenda_item_not_editable_anymore: "Dit agendapunt kan niet meer bewerkt worden."
text_work_package_has_no_upcoming_meeting_agenda_items: "Dit werkpakket staat nog niet op de agenda van een komende vergadering."
- text_work_package_add_to_meeting_hint: 'Use the "Add to meeting" button to add this work package to an upcoming meeting.'
+ text_work_package_add_to_meeting_hint: 'Gebruik de knop "Aan vergadering toevoegen" om dit werkpakket aan een komende vergadering toe te voegen.'
text_work_package_has_no_past_meeting_agenda_items: "Dit werkpakket werd tijdens een vorige vergadering niet genoemd."
diff --git a/modules/meeting/config/locales/crowdin/no.yml b/modules/meeting/config/locales/crowdin/no.yml
index 6e86b5ea0813..7f53100247dc 100644
--- a/modules/meeting/config/locales/crowdin/no.yml
+++ b/modules/meeting/config/locales/crowdin/no.yml
@@ -88,7 +88,7 @@
label_meeting: "Møte"
label_meeting_plural: "Møter"
label_meeting_new: "Nytt møte"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Redigere møte"
@@ -146,7 +146,7 @@
types:
classic: "Klassisk"
classic_text: "Organiser møtet ditt i en formattabel tekst agenda og protokoll."
- structured: "One-time"
+ structured: "Dynamisk"
structured_text: "Organiser møtet som en liste over saker, eventuelt knytte dem til en arbeidspakke."
structured_text_copy: "Kopiering av et møte vil for tiden ikke kopiere de tilknyttede elementene på dagsordenen, bare detaljene"
copied: "Kopiert fra møte #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/pl.yml b/modules/meeting/config/locales/crowdin/pl.yml
index d6a1e0d84cfb..b527739d60ef 100644
--- a/modules/meeting/config/locales/crowdin/pl.yml
+++ b/modules/meeting/config/locales/crowdin/pl.yml
@@ -90,7 +90,7 @@ pl:
label_meeting: "Spotkanie"
label_meeting_plural: "Spotkania"
label_meeting_new: "Nowe spotkanie"
- label_meeting_new_one_time: "Nowe spotkanie jednorazowe"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Utwórz spotkanie"
label_meeting_copy: "Skopiuj spotkanie"
label_meeting_edit: "Edycja spotkania"
@@ -148,7 +148,7 @@ pl:
types:
classic: "Klasyczny"
classic_text: "Zorganizuj spotkanie w formatowanym tekstowym planie spotkania i protokole."
- structured: "Jednorazowe"
+ structured: "Dynamiczne"
structured_text: "Zorganizuj spotkanie jako listę punktów planu spotkania, opcjonalnie powiązując je z pakietem roboczym."
structured_text_copy: "Skopiowanie spotkania nie powoduje obecnie skopiowania powiązanych pozycji planu spotkania, a jedynie szczegółów"
copied: "Skopiowano ze spotkania #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/pt-BR.yml b/modules/meeting/config/locales/crowdin/pt-BR.yml
index 8b2ef3e6b074..91ef69304bf2 100644
--- a/modules/meeting/config/locales/crowdin/pt-BR.yml
+++ b/modules/meeting/config/locales/crowdin/pt-BR.yml
@@ -88,7 +88,7 @@ pt-BR:
label_meeting: "Reunião"
label_meeting_plural: "Reuniões"
label_meeting_new: "Nova Reunião"
- label_meeting_new_one_time: "Nova reunião única"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Criar reunião"
label_meeting_copy: "Copiar reunião"
label_meeting_edit: "Editar Reunião"
@@ -146,7 +146,7 @@ pt-BR:
types:
classic: "Clássico"
classic_text: "Organize sua reunião em uma agenda e protocolo de texto formatáveis."
- structured: "Única"
+ structured: "Dinâmico"
structured_text: "Organize sua reunião como uma lista de itens da agenda, opcionalmente vinculando-os a um pacote de trabalho."
structured_text_copy: "Copiar uma reunião atualmente não copiará os itens associados da agenda da reunião, apenas os detalhes"
copied: "Copiado para a Reunião #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/pt-PT.yml b/modules/meeting/config/locales/crowdin/pt-PT.yml
index 51571eaf9e6e..54a72d47d27e 100644
--- a/modules/meeting/config/locales/crowdin/pt-PT.yml
+++ b/modules/meeting/config/locales/crowdin/pt-PT.yml
@@ -88,7 +88,7 @@ pt-PT:
label_meeting: "Reunião"
label_meeting_plural: "Reuniões"
label_meeting_new: "Nova reunião"
- label_meeting_new_one_time: "Nova reunião única"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Criar reunião"
label_meeting_copy: "Copiar reunião"
label_meeting_edit: "Editar a reunião"
@@ -146,7 +146,7 @@ pt-PT:
types:
classic: "Clássico"
classic_text: "Organize a sua reunião numa agenda e protocolo de texto formatáveis."
- structured: "Uma vez"
+ structured: "Dinâmico"
structured_text: "Organize a sua reunião como uma lista de itens da agenda, associando-os opcionalmente a um pacote de trabalho."
structured_text_copy: "Atualmente, a cópia de uma reunião não copia os itens associados da agenda da reunião, apenas os detalhes"
copied: "Copiado da reunião #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ro.yml b/modules/meeting/config/locales/crowdin/ro.yml
index a9d9d671a424..364f831caf7d 100644
--- a/modules/meeting/config/locales/crowdin/ro.yml
+++ b/modules/meeting/config/locales/crowdin/ro.yml
@@ -62,7 +62,7 @@ ro:
meeting_section: "Section"
activity:
filter:
- meeting: "Reuniuni"
+ meeting: "Întâlniri"
item:
meeting_agenda_item:
duration:
@@ -89,7 +89,7 @@ ro:
label_meeting: "ID Întâlnire"
label_meeting_plural: "Întâlniri"
label_meeting_new: "Noua întâlnire"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Editare Întâlnire"
@@ -147,7 +147,7 @@ ro:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copiat de la Întâlnire #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/ru.yml b/modules/meeting/config/locales/crowdin/ru.yml
index 5921a5086727..dc0358a432fa 100644
--- a/modules/meeting/config/locales/crowdin/ru.yml
+++ b/modules/meeting/config/locales/crowdin/ru.yml
@@ -90,7 +90,7 @@ ru:
label_meeting: "Совещание"
label_meeting_plural: "Совещания"
label_meeting_new: "Новое совещание"
- label_meeting_new_one_time: "Новое однократное совещание"
+ label_meeting_new_dynamic: "Новая динамичная встреча"
label_meeting_create: "Создать совещание"
label_meeting_copy: "Копировать совещание"
label_meeting_edit: "Измененить совещание"
@@ -148,7 +148,7 @@ ru:
types:
classic: "Классический"
classic_text: "Организуйте своё совещание с помощью форматируемой текстовой повестки и протокола."
- structured: "Однократно"
+ structured: "Динамический"
structured_text: "Организуйте своё совещание в виде списка пунктов повестки, при необходимости связав их с пакетом работ."
structured_text_copy: "При копировании совещания в настоящее время не скопируются связанные с ним пункты повестки, будут скопированы только детали"
copied: "Скопировано со встречи #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/rw.yml b/modules/meeting/config/locales/crowdin/rw.yml
index f5bd45281f5a..fda34d7e5f25 100644
--- a/modules/meeting/config/locales/crowdin/rw.yml
+++ b/modules/meeting/config/locales/crowdin/rw.yml
@@ -88,7 +88,7 @@ rw:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ rw:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/si.yml b/modules/meeting/config/locales/crowdin/si.yml
index 61c0c9a73412..51997928ccd7 100644
--- a/modules/meeting/config/locales/crowdin/si.yml
+++ b/modules/meeting/config/locales/crowdin/si.yml
@@ -88,7 +88,7 @@ si:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ si:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/sk.yml b/modules/meeting/config/locales/crowdin/sk.yml
index fd5fbc77cc75..3ecc01614173 100644
--- a/modules/meeting/config/locales/crowdin/sk.yml
+++ b/modules/meeting/config/locales/crowdin/sk.yml
@@ -90,7 +90,7 @@ sk:
label_meeting: "Stretnutie"
label_meeting_plural: "Stretnutia"
label_meeting_new: "Nové stretnutie"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Upraviť stretnutie"
@@ -148,7 +148,7 @@ sk:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/sl.yml b/modules/meeting/config/locales/crowdin/sl.yml
index 19ee117d8ce8..a15b7cb22ac0 100644
--- a/modules/meeting/config/locales/crowdin/sl.yml
+++ b/modules/meeting/config/locales/crowdin/sl.yml
@@ -90,7 +90,7 @@ sl:
label_meeting: "Sestanek"
label_meeting_plural: "Sestanki"
label_meeting_new: "Nov sestanek"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Uredi sestanek"
@@ -148,7 +148,7 @@ sl:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Kopirano iz sestanka #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/sr.yml b/modules/meeting/config/locales/crowdin/sr.yml
index d0a5014f45ac..e1490563df45 100644
--- a/modules/meeting/config/locales/crowdin/sr.yml
+++ b/modules/meeting/config/locales/crowdin/sr.yml
@@ -89,7 +89,7 @@ sr:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -147,7 +147,7 @@ sr:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/sv.yml b/modules/meeting/config/locales/crowdin/sv.yml
index d9a67fc9f47e..65397b5b65ca 100644
--- a/modules/meeting/config/locales/crowdin/sv.yml
+++ b/modules/meeting/config/locales/crowdin/sv.yml
@@ -88,7 +88,7 @@ sv:
label_meeting: "Möte"
label_meeting_plural: "Möten"
label_meeting_new: "Nytt möte"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Redigera möte"
@@ -96,11 +96,11 @@ sv:
label_meeting_minutes: "Protokoll"
label_meeting_close: "Stäng"
label_meeting_open: "Öppna"
- label_meeting_index_delete: "Delete"
+ label_meeting_index_delete: "Radera"
label_meeting_open_this_meeting: "Open this meeting"
label_meeting_agenda_close: "Stäng agendan för att påbörja protokollet"
label_meeting_date_time: "Datum/tid"
- label_meeting_date_and_time: "Date and time"
+ label_meeting_date_and_time: "Datum och tid"
label_meeting_diff: "Skillnad"
label_upcoming_meetings: "Kommande möten"
label_past_meetings: "Tidigare möten"
@@ -127,7 +127,7 @@ sv:
agenda: "Copy agenda"
agenda_items: "Copy agenda items"
agenda_text: "Copy the agenda of the old meeting"
- participants: "Copy list of participants"
+ participants: "Kopiera deltagarlista"
email:
send_emails: "Email participants"
send_invitation_emails: >
@@ -146,7 +146,7 @@ sv:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Kopierat från möte #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/th.yml b/modules/meeting/config/locales/crowdin/th.yml
index cb8c7db25ec8..506f4b94491f 100644
--- a/modules/meeting/config/locales/crowdin/th.yml
+++ b/modules/meeting/config/locales/crowdin/th.yml
@@ -87,7 +87,7 @@ th:
label_meeting: "ประชุม"
label_meeting_plural: "ประชุม"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -145,7 +145,7 @@ th:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/tr.yml b/modules/meeting/config/locales/crowdin/tr.yml
index 6dcd31df6854..0531186cc0e1 100644
--- a/modules/meeting/config/locales/crowdin/tr.yml
+++ b/modules/meeting/config/locales/crowdin/tr.yml
@@ -88,19 +88,19 @@ tr:
label_meeting: "Toplantı"
label_meeting_plural: "Toplantılar"
label_meeting_new: "Yeni Toplantı"
- label_meeting_new_one_time: "New one-time meeting"
- label_meeting_create: "Create meeting"
- label_meeting_copy: "Copy meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
+ label_meeting_create: "Toplantı oluştur"
+ label_meeting_copy: "Toplantıyı kopyala"
label_meeting_edit: "Toplantıyı Düzenle"
label_meeting_agenda: "Ajanda"
label_meeting_minutes: "Dakika"
label_meeting_close: "Kapat"
label_meeting_open: "Aç"
- label_meeting_index_delete: "Delete"
- label_meeting_open_this_meeting: "Open this meeting"
+ label_meeting_index_delete: "Sil"
+ label_meeting_open_this_meeting: "Bu toplantıyı açı"
label_meeting_agenda_close: "Başlamak için gündemi dakikalar sonra kapatın"
label_meeting_date_time: "Tarih/Saat"
- label_meeting_date_and_time: "Date and time"
+ label_meeting_date_and_time: "Tarih ve zaman"
label_meeting_diff: "Fark"
label_upcoming_meetings: "Yaklaşan toplantılar"
label_past_meetings: "Geçmiş toplantılar"
@@ -125,14 +125,14 @@ tr:
attachments: "Ekleri kopyala"
attachments_text: "Tük eklenen dosyaları yeni toplantıya kopyala"
agenda: "Gündemi kopyala"
- agenda_items: "Copy agenda items"
+ agenda_items: "Gündem maddesini kopyala"
agenda_text: "Eski toplantının gündemini kopyalayın"
- participants: "Copy list of participants"
+ participants: "Katılımcı listesini kopyala"
email:
send_emails: "Email participants"
send_invitation_emails: >
Yukarıda seçilen katılımcıların hepsine e-posta davetiyesi gönder. Bu işi daha sonra herhangi bir zamanda elle de gerçekleştirebilirsiniz.
- send_invitation_emails_structured: "Send an email invitation immediately to all participants. You can also do this manually at any time later."
+ send_invitation_emails_structured: "Tüm katılımcılara e-posta ile davetiye gönder. Bu işlemi daha sonra herhangi bir zamanda elle de gerçekleştirebilirsiniz."
open_meeting_link: "Açık toplantı"
invited:
summary: "%{actor} size toplantı için bir davetiye gönderdi %{title}"
@@ -146,7 +146,7 @@ tr:
types:
classic: "Klasik"
classic_text: "Toplantınızı biçimlendirilebilir bir metin gündemi ve protokolüyle düzenleyin."
- structured: "One-time"
+ structured: "Dinamik"
structured_text: "Toplantınızı, isteğe bağlı olarak bunları bir çalışma paketine bağlayarak, gündem maddelerinin bir listesi halinde düzenleyin."
structured_text_copy: "Toplantıyı kopyalamak toplantıyla ilgili ajanda maddelerini kopyalamayacaktır, sadece detaylar kopyalanacak"
copied: "#%{id} numaralı toplantıdan kopyalandı"
diff --git a/modules/meeting/config/locales/crowdin/uk.yml b/modules/meeting/config/locales/crowdin/uk.yml
index 8fb6e918faf7..9e1726dc48d5 100644
--- a/modules/meeting/config/locales/crowdin/uk.yml
+++ b/modules/meeting/config/locales/crowdin/uk.yml
@@ -90,7 +90,7 @@ uk:
label_meeting: "Зустріч"
label_meeting_plural: "Зустрічі"
label_meeting_new: "Нова зустріч"
- label_meeting_new_one_time: "Нова одноразова нарада"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Створити нараду"
label_meeting_copy: "Копіювати нараду"
label_meeting_edit: "Редагувати збори"
@@ -148,7 +148,7 @@ uk:
types:
classic: "Класична"
classic_text: "Організуйте нараду у вигляді порядку денного й протоколу в текстовому форматі з підтримкою редагування."
- structured: "Одноразова"
+ structured: "Динамічна"
structured_text: "Організуйте нараду у вигляді переліку пунктів порядку денного, за бажанням пов’язавши їх із пакетом робіт."
structured_text_copy: "Зараз копіювання наради стосується лише докладних даних про неї, але не пов’язаних пунктів її порядку денного"
copied: "Скопійовано з наради #%{id} "
diff --git a/modules/meeting/config/locales/crowdin/uz.yml b/modules/meeting/config/locales/crowdin/uz.yml
index ada482494117..d6b7109a8875 100644
--- a/modules/meeting/config/locales/crowdin/uz.yml
+++ b/modules/meeting/config/locales/crowdin/uz.yml
@@ -88,7 +88,7 @@ uz:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -146,7 +146,7 @@ uz:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/vi.yml b/modules/meeting/config/locales/crowdin/vi.yml
index d2f596047945..f4f22acb1943 100644
--- a/modules/meeting/config/locales/crowdin/vi.yml
+++ b/modules/meeting/config/locales/crowdin/vi.yml
@@ -87,7 +87,7 @@ vi:
label_meeting: "Cuộc họp"
label_meeting_plural: "Những cuộc họp"
label_meeting_new: "Cuộc họp mới"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Chỉnh sửa cuộc họp"
@@ -145,7 +145,7 @@ vi:
types:
classic: "Cổ điển"
classic_text: "Tổ chức cuộc họp của bạn bằng một chương trình nghị sự và biên bản định dạng văn bản."
- structured: "One-time"
+ structured: "Dạng động"
structured_text: "Tổ chức cuộc họp của bạn dưới dạng danh sách các mục chương trình nghị sự, có thể liên kết chúng với một gói công việc."
structured_text_copy: "Sao chép một cuộc họp hiện tại sẽ không sao chép các mục chương trình nghị sự liên quan, chỉ sao chép các chi tiết"
copied: "Sao chép từ Cuộc họp #%{id}"
diff --git a/modules/meeting/config/locales/crowdin/zh-CN.yml b/modules/meeting/config/locales/crowdin/zh-CN.yml
index b165ef29867e..51aba6c89e0a 100644
--- a/modules/meeting/config/locales/crowdin/zh-CN.yml
+++ b/modules/meeting/config/locales/crowdin/zh-CN.yml
@@ -87,7 +87,7 @@ zh-CN:
label_meeting: "会议"
label_meeting_plural: "会议"
label_meeting_new: "新增会议"
- label_meeting_new_one_time: "新一次性会议"
+ label_meeting_new_dynamic: "新动态会议"
label_meeting_create: "创建会议"
label_meeting_copy: "复制会议"
label_meeting_edit: "编辑会议"
@@ -145,7 +145,7 @@ zh-CN:
types:
classic: "经典"
classic_text: "将您的会议以可格式化的文本议程和会议纪要的形式进行组织。"
- structured: "一次性的"
+ structured: "动态"
structured_text: "将您的会议组织为议程项目列表,并可选地将其与工作包链接。"
structured_text_copy: "目前复制会议不会复制相关会议的议程项目,只会复制会议的详细信息。"
copied: "从会议 #%{id} 复制"
diff --git a/modules/meeting/config/locales/crowdin/zh-TW.yml b/modules/meeting/config/locales/crowdin/zh-TW.yml
index d3832aa75e00..706d027fb035 100644
--- a/modules/meeting/config/locales/crowdin/zh-TW.yml
+++ b/modules/meeting/config/locales/crowdin/zh-TW.yml
@@ -43,7 +43,7 @@ zh-TW:
start_time_hour: "開始時間"
meeting_agenda_item:
title: "標題"
- author: "作者"
+ author: "會議發起者"
duration_in_minutes: "分鐘"
description: "備註"
presenter: "簡報者"
@@ -87,7 +87,7 @@ zh-TW:
label_meeting: "會議"
label_meeting_plural: "會議"
label_meeting_new: "新增會議"
- label_meeting_new_one_time: "建立一次性會議"
+ label_meeting_new_dynamic: "新的動態會議"
label_meeting_create: "新增會議"
label_meeting_copy: "複製會議"
label_meeting_edit: "編輯會議"
@@ -145,7 +145,7 @@ zh-TW:
types:
classic: "經典"
classic_text: "以調整好的文字議程和程序來整理您的會議紀錄。"
- structured: "一次性"
+ structured: "動態"
structured_text: "將您的會議整理成一個議程項目列表,並可選擇地將它們與一個工作項目連接起來。"
structured_text_copy: "目前複製會議不會複製相關會議的議程項目,只會複製會議的詳細信息。"
copied: "從會議紀錄 #%{id} 中複製"
diff --git a/modules/meeting/config/locales/en.yml b/modules/meeting/config/locales/en.yml
index 797124c433fb..c5c7f6beee96 100644
--- a/modules/meeting/config/locales/en.yml
+++ b/modules/meeting/config/locales/en.yml
@@ -104,7 +104,7 @@ en:
label_meeting: "Meeting"
label_meeting_plural: "Meetings"
label_meeting_new: "New Meeting"
- label_meeting_new_one_time: "New one-time meeting"
+ label_meeting_new_dynamic: "New dynamic meeting"
label_meeting_create: "Create meeting"
label_meeting_copy: "Copy meeting"
label_meeting_edit: "Edit Meeting"
@@ -164,7 +164,7 @@ en:
types:
classic: "Classic"
classic_text: "Organize your meeting in a formattable text agenda and protocol."
- structured: "One-time"
+ structured: "Dynamic"
structured_text: "Organize your meeting as a list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb
index 523df5737559..fb52588d6d33 100644
--- a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb
+++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb
@@ -72,7 +72,7 @@
meetings_page.visit!
expect(page).to have_current_path(meetings_page.path) # rubocop:disable RSpec/ExpectInHook
meetings_page.click_on "add-meeting-button"
- meetings_page.click_on "One-time"
+ meetings_page.click_on "Dynamic"
meetings_page.set_title "Some title"
meetings_page.set_start_date "2013-03-28"
diff --git a/modules/meeting/spec/support/pages/meetings/index.rb b/modules/meeting/spec/support/pages/meetings/index.rb
index f60665f6dc4f..e8633c583891 100644
--- a/modules/meeting/spec/support/pages/meetings/index.rb
+++ b/modules/meeting/spec/support/pages/meetings/index.rb
@@ -100,7 +100,7 @@ def expect_create_new_types
click_on("add-meeting-button")
expect(page).to have_link("Classic")
- expect(page).to have_link("One-time")
+ expect(page).to have_link("Dynamic")
end
def expect_copy_action(meeting)
diff --git a/modules/openid_connect/app/components/openid_connect/providers/client_details_form.rb b/modules/openid_connect/app/components/openid_connect/providers/client_details_form.rb
index 8c945ac679aa..609c3304381f 100644
--- a/modules/openid_connect/app/components/openid_connect/providers/client_details_form.rb
+++ b/modules/openid_connect/app/components/openid_connect/providers/client_details_form.rb
@@ -29,6 +29,8 @@
module OpenIDConnect
module Providers
class ClientDetailsForm < BaseForm
+ include Redmine::I18n
+
form do |f|
%i[client_id client_secret].each do |attr|
f.text_field(
@@ -48,6 +50,20 @@ class ClientDetailsForm < BaseForm
required: false,
input_width: :large
)
+ f.text_field(
+ name: :scope,
+ label: I18n.t("activemodel.attributes.openid_connect/provider.scope"),
+ placeholder: "openid email profile",
+ caption: link_translate(
+ "openid_connect.instructions.scope",
+ links: {
+ docs_url: "https://openid.net/specs/openid-connect-basic-1_0.html#Scopes"
+ }
+ ),
+ disabled: provider.seeded_from_env?,
+ required: false,
+ input_width: :large
+ )
f.check_box(
name: :limit_self_registration,
label: I18n.t("activemodel.attributes.openid_connect/provider.limit_self_registration"),
diff --git a/modules/openid_connect/app/components/openid_connect/providers/row_component.rb b/modules/openid_connect/app/components/openid_connect/providers/row_component.rb
index c7e87dc5c26a..f33c87fe894d 100644
--- a/modules/openid_connect/app/components/openid_connect/providers/row_component.rb
+++ b/modules/openid_connect/app/components/openid_connect/providers/row_component.rb
@@ -5,18 +5,11 @@ def provider
model
end
- def column_args(column)
- if column == :name
- { style: "grid-column: span 3" }
- else
- super
- end
- end
-
def name
concat(provider_name)
+
unless provider.configured?
- concat(incomplete_label)
+ incomplete_label
end
end
diff --git a/modules/openid_connect/app/components/openid_connect/providers/table_component.rb b/modules/openid_connect/app/components/openid_connect/providers/table_component.rb
index 9f23c1b974dc..a0696ab019d0 100644
--- a/modules/openid_connect/app/components/openid_connect/providers/table_component.rb
+++ b/modules/openid_connect/app/components/openid_connect/providers/table_component.rb
@@ -3,18 +3,16 @@ module Providers
class TableComponent < ::OpPrimer::BorderBoxTableComponent
columns :name, :type, :users, :creator, :created_at
+ main_column :name
+
+ mobile_columns :name, :type, :users
+
+ mobile_labels :users
+
def initial_sort
%i[id asc]
end
- def header_args(column)
- if column == :name
- { style: "grid-column: span 3" }
- else
- super
- end
- end
-
def has_actions?
false
end
@@ -27,6 +25,10 @@ def empty_row_message
I18n.t "openid_connect.providers.no_results_table"
end
+ def mobile_title
+ I18n.t("openid_connect.providers.label_providers")
+ end
+
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
diff --git a/modules/openid_connect/app/models/openid_connect/provider.rb b/modules/openid_connect/app/models/openid_connect/provider.rb
index 18fa645b5150..c61e5b04589a 100644
--- a/modules/openid_connect/app/models/openid_connect/provider.rb
+++ b/modules/openid_connect/app/models/openid_connect/provider.rb
@@ -31,6 +31,7 @@ class Provider < AuthProvider
store_attribute :options, :scheme, :string
store_attribute :options, :port, :string
+ store_attribute :options, :scope, :string
store_attribute :options, :claims, :string
store_attribute :options, :acr_values, :string
diff --git a/modules/openid_connect/app/models/openid_connect/provider/hash_builder.rb b/modules/openid_connect/app/models/openid_connect/provider/hash_builder.rb
index 23e59aa43a8e..0beea596b22e 100644
--- a/modules/openid_connect/app/models/openid_connect/provider/hash_builder.rb
+++ b/modules/openid_connect/app/models/openid_connect/provider/hash_builder.rb
@@ -19,6 +19,7 @@ def to_h # rubocop:disable Metrics/AbcSize
authorization_endpoint:,
jwks_uri:,
issuer:,
+ scope:,
identifier: client_id,
secret: client_secret,
token_endpoint:,
@@ -39,7 +40,8 @@ def provider_specific_to_h
}
when "microsoft_entra"
{
- use_graph_api:
+ use_graph_api:,
+ tenant:
}
else
{}
diff --git a/modules/openid_connect/app/services/openid_connect/configuration_mapper.rb b/modules/openid_connect/app/services/openid_connect/configuration_mapper.rb
index 5ee45847c192..bd9f5247febe 100644
--- a/modules/openid_connect/app/services/openid_connect/configuration_mapper.rb
+++ b/modules/openid_connect/app/services/openid_connect/configuration_mapper.rb
@@ -53,6 +53,7 @@ def call! # rubocop:disable Metrics/AbcSize
"limit_self_registration" => options["limit_self_registration"],
"use_graph_api" => options["use_graph_api"],
"acr_values" => options["acr_values"],
+ "scope" => extract_scope(options["scope"]),
"authorization_endpoint" => extract_url(options, "authorization_endpoint"),
"token_endpoint" => extract_url(options, "token_endpoint"),
"userinfo_endpoint" => extract_url(options, "userinfo_endpoint"),
@@ -68,6 +69,17 @@ def call! # rubocop:disable Metrics/AbcSize
private
+ def extract_scope(value)
+ return if value.blank?
+
+ case value
+ when Array
+ value.join(" ")
+ else
+ value
+ end
+ end
+
def oidc_provider(options)
case options["name"]
when /azure/
diff --git a/modules/openid_connect/config/locales/crowdin/af.yml b/modules/openid_connect/config/locales/crowdin/af.yml
index 57d7d7c75621..e5cab04ca0ae 100644
--- a/modules/openid_connect/config/locales/crowdin/af.yml
+++ b/modules/openid_connect/config/locales/crowdin/af.yml
@@ -59,9 +59,10 @@ af:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ af:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ar.yml b/modules/openid_connect/config/locales/crowdin/ar.yml
index 2f48c3d4b6ca..c3600cb08570 100644
--- a/modules/openid_connect/config/locales/crowdin/ar.yml
+++ b/modules/openid_connect/config/locales/crowdin/ar.yml
@@ -59,9 +59,10 @@ ar:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ar:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/az.yml b/modules/openid_connect/config/locales/crowdin/az.yml
index c40ca1c1ab86..998048ff3da3 100644
--- a/modules/openid_connect/config/locales/crowdin/az.yml
+++ b/modules/openid_connect/config/locales/crowdin/az.yml
@@ -59,9 +59,10 @@ az:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ az:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/be.yml b/modules/openid_connect/config/locales/crowdin/be.yml
index abc8e04f2b7d..826e82deb78c 100644
--- a/modules/openid_connect/config/locales/crowdin/be.yml
+++ b/modules/openid_connect/config/locales/crowdin/be.yml
@@ -59,9 +59,10 @@ be:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ be:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/bg.yml b/modules/openid_connect/config/locales/crowdin/bg.yml
index afd23659f307..840e3b0f149a 100644
--- a/modules/openid_connect/config/locales/crowdin/bg.yml
+++ b/modules/openid_connect/config/locales/crowdin/bg.yml
@@ -59,9 +59,10 @@ bg:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ bg:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ca.yml b/modules/openid_connect/config/locales/crowdin/ca.yml
index cbc8b7a06122..6c92ab3cef2d 100644
--- a/modules/openid_connect/config/locales/crowdin/ca.yml
+++ b/modules/openid_connect/config/locales/crowdin/ca.yml
@@ -59,9 +59,10 @@ ca:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ca:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ckb-IR.yml b/modules/openid_connect/config/locales/crowdin/ckb-IR.yml
index c6a688bb60a9..0a2933627bd0 100644
--- a/modules/openid_connect/config/locales/crowdin/ckb-IR.yml
+++ b/modules/openid_connect/config/locales/crowdin/ckb-IR.yml
@@ -59,9 +59,10 @@ ckb-IR:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ckb-IR:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/cs.yml b/modules/openid_connect/config/locales/crowdin/cs.yml
index b8eaa8e04f8f..94bfd92ea4e2 100644
--- a/modules/openid_connect/config/locales/crowdin/cs.yml
+++ b/modules/openid_connect/config/locales/crowdin/cs.yml
@@ -59,9 +59,10 @@ cs:
metadata_url: Mám adresu URL koncového bodu zjišťování
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ cs:
metadata_url: I have a discovery endpoint URL
endpoint_url: URL koncového bodu
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/da.yml b/modules/openid_connect/config/locales/crowdin/da.yml
index cb8ecd38cc5d..334671687c2a 100644
--- a/modules/openid_connect/config/locales/crowdin/da.yml
+++ b/modules/openid_connect/config/locales/crowdin/da.yml
@@ -59,9 +59,10 @@ da:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ da:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/de.yml b/modules/openid_connect/config/locales/crowdin/de.yml
index 86a3e03c7e8a..b58b9f09b335 100644
--- a/modules/openid_connect/config/locales/crowdin/de.yml
+++ b/modules/openid_connect/config/locales/crowdin/de.yml
@@ -59,9 +59,10 @@ de:
metadata_url: Ich habe eine Discovery-Endpunkt-URL
client_id: Dies ist die Client-ID, die Sie von Ihrem OpenID Connect-Anbieter erhalten haben
client_secret: Dies ist das Client-Secret, die Sie von Ihrem OpenID Connect-Anbieter erhalten haben
- limit_self_registration: Wenn diese Option aktiviert ist, können sich Benutzer nur dann über diesen Anbieter registrieren, wenn die Konfiguration in OpenProject es erlaubt.
- display_name: Der Name des SSO-Anbieters. Dieser wird in der Login-Schaltfläche angezeigt.
+ limit_self_registration: Wenn diese Option aktiviert ist, können sich Benutzer nur dann über diesen Anbieter registrieren, wenn die Konfiguration aufseiten des Anbieters dies zulässt.
+ display_name: Der Name des Anbieters. Dieser wird als Anmeldeschaltfläche und in der Liste der Anbieter angezeigt.
tenant: 'Bitte ersetzen Sie den vorgegebenen Mandanten durch Ihren eigenen, falls zutreffend. Siehe diese Dokumentation von Microsoft Entra.'
+ scope: Wenn Sie benutzerdefinierte Scopes anfordern möchten, können Sie hier einen oder mehrere durch Leerzeichen getrennte Werte hinzufügen. Weitere Informationen finden Sie in der [OpenID Connect Dokumentation](docs_url).
post_logout_redirect_uri: Die URL, an die der OpenID Connect-Anbieter nach einer erfolgreichen Abmeldung weiterleiten soll.
claims: >
Sie können zusätzliche Ansprüche (Claims) für die Endpunkte userinfo und id token anfordern. Weitere Informationen finden Sie in [unserer OpenID Connect Dokumentation](docs_url).
@@ -82,6 +83,7 @@ de:
metadata_url: Ich habe eine Discovery-Endpunkt-URL
endpoint_url: Endpunkt-URL
providers:
+ label_providers: "Provider"
seeded_from_env: "Dieser Anbieter wurde über Umgebungsvariablen konfiguriert. Er kann nicht in der Oberfläche bearbeitet werden."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/el.yml b/modules/openid_connect/config/locales/crowdin/el.yml
index 99920f4895ac..287bd25e9dc9 100644
--- a/modules/openid_connect/config/locales/crowdin/el.yml
+++ b/modules/openid_connect/config/locales/crowdin/el.yml
@@ -59,9 +59,10 @@ el:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ el:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/eo.yml b/modules/openid_connect/config/locales/crowdin/eo.yml
index e9a17c160cb3..92fcadeba9cc 100644
--- a/modules/openid_connect/config/locales/crowdin/eo.yml
+++ b/modules/openid_connect/config/locales/crowdin/eo.yml
@@ -59,9 +59,10 @@ eo:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ eo:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/es.yml b/modules/openid_connect/config/locales/crowdin/es.yml
index 645ad72c2477..e95110ca1cda 100644
--- a/modules/openid_connect/config/locales/crowdin/es.yml
+++ b/modules/openid_connect/config/locales/crowdin/es.yml
@@ -59,9 +59,10 @@ es:
metadata_url: Tengo una URL del terminal de descubrimiento
client_id: Este es el ID de cliente que le ha proporcionado su proveedor de OpenID Connect
client_secret: Este es el secreto de cliente que le ha proporcionado su proveedor de OpenID Connect
- limit_self_registration: Si está activada, los usuarios solo podrán registrarse utilizando este proveedor si la configuración del proveedor lo permite.
- display_name: El nombre del proveedor. Aparecerá como botón de inicio de sesión y en la lista de proveedores.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Sustituya el inquilino predeterminado por el suyo si procede. Consulte esto.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: La URL a la que debe redirigir el proveedor de OpenID Connect tras una solicitud de cierre de sesión.
claims: >
Puede solicitar reclamaciones adicionales para los terminales userinfo e id token. Consulte [nuestra documentación sobre OpenID connect](docs_url) para obtener más información.
@@ -82,6 +83,7 @@ es:
metadata_url: Tengo una URL del terminal de descubrimiento
endpoint_url: URL del terminal
providers:
+ label_providers: "Providers"
seeded_from_env: "Este proveedor fue sembrado desde la configuración del entorno. No puede editarse."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/et.yml b/modules/openid_connect/config/locales/crowdin/et.yml
index 7c92849176c5..fbf583040266 100644
--- a/modules/openid_connect/config/locales/crowdin/et.yml
+++ b/modules/openid_connect/config/locales/crowdin/et.yml
@@ -59,9 +59,10 @@ et:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ et:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/eu.yml b/modules/openid_connect/config/locales/crowdin/eu.yml
index e9aa246d3472..1c2a17f0725f 100644
--- a/modules/openid_connect/config/locales/crowdin/eu.yml
+++ b/modules/openid_connect/config/locales/crowdin/eu.yml
@@ -59,9 +59,10 @@ eu:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ eu:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/fa.yml b/modules/openid_connect/config/locales/crowdin/fa.yml
index 77d2d515c3b0..c899511c01d9 100644
--- a/modules/openid_connect/config/locales/crowdin/fa.yml
+++ b/modules/openid_connect/config/locales/crowdin/fa.yml
@@ -59,9 +59,10 @@ fa:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ fa:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/fi.yml b/modules/openid_connect/config/locales/crowdin/fi.yml
index 5a55d81a41de..9893b4afe2ce 100644
--- a/modules/openid_connect/config/locales/crowdin/fi.yml
+++ b/modules/openid_connect/config/locales/crowdin/fi.yml
@@ -59,9 +59,10 @@ fi:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ fi:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/fil.yml b/modules/openid_connect/config/locales/crowdin/fil.yml
index aeb7c557893e..16ba7b4b01f4 100644
--- a/modules/openid_connect/config/locales/crowdin/fil.yml
+++ b/modules/openid_connect/config/locales/crowdin/fil.yml
@@ -59,9 +59,10 @@ fil:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ fil:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/fr.yml b/modules/openid_connect/config/locales/crowdin/fr.yml
index 1c5c69f5f074..4bea344da781 100644
--- a/modules/openid_connect/config/locales/crowdin/fr.yml
+++ b/modules/openid_connect/config/locales/crowdin/fr.yml
@@ -59,9 +59,10 @@ fr:
metadata_url: J'ai une URL de point de terminaison de découverte
client_id: Il s'agit de l'ID de client qui vous a été attribué par votre fournisseur OpenID Connect
client_secret: Il s'agit de la clé secrète du client qui vous a été communiquée par votre fournisseur OpenID Connect
- limit_self_registration: Si cette option est activée, les utilisateurs peuvent s'inscrire par le biais de ce fournisseur uniquement si la configuration du côté du fournisseur le permet.
- display_name: Le nom du fournisseur. Il sera affiché comme bouton de connexion et dans la liste des fournisseurs.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Veuillez remplacer le locataire par défaut par le vôtre, le cas échéant. Vous pouvez en savoir plus ici.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: L'URL vers laquelle le fournisseur OpenID Connect doit rediriger le client après une demande de déconnexion.
claims: >
Vous pouvez demander des réclamations supplémentaires pour les points de terminaison userinfo et id token. Veuillez consulter [notre documentation OpenID Connect](docs_url) pour obtenir plus d'informations.
@@ -82,6 +83,7 @@ fr:
metadata_url: J'ai une URL de point de terminaison de découverte
endpoint_url: URL du point de terminaison
providers:
+ label_providers: "Providers"
seeded_from_env: "Ce fournisseur a été ajouté à partir de la configuration de l'environnement. Il ne peut pas être modifié."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/he.yml b/modules/openid_connect/config/locales/crowdin/he.yml
index ac2975693fa5..ff542006a208 100644
--- a/modules/openid_connect/config/locales/crowdin/he.yml
+++ b/modules/openid_connect/config/locales/crowdin/he.yml
@@ -59,9 +59,10 @@ he:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ he:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/hi.yml b/modules/openid_connect/config/locales/crowdin/hi.yml
index d6d05fc47e65..9eb049a29f0b 100644
--- a/modules/openid_connect/config/locales/crowdin/hi.yml
+++ b/modules/openid_connect/config/locales/crowdin/hi.yml
@@ -59,9 +59,10 @@ hi:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ hi:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/hr.yml b/modules/openid_connect/config/locales/crowdin/hr.yml
index b1505a24936a..e92961044358 100644
--- a/modules/openid_connect/config/locales/crowdin/hr.yml
+++ b/modules/openid_connect/config/locales/crowdin/hr.yml
@@ -59,9 +59,10 @@ hr:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ hr:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/hu.yml b/modules/openid_connect/config/locales/crowdin/hu.yml
index 9dd8c2430198..1a3f0e833971 100644
--- a/modules/openid_connect/config/locales/crowdin/hu.yml
+++ b/modules/openid_connect/config/locales/crowdin/hu.yml
@@ -59,9 +59,10 @@ hu:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ hu:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/id.yml b/modules/openid_connect/config/locales/crowdin/id.yml
index 6f4525fc1e67..61b351e6b019 100644
--- a/modules/openid_connect/config/locales/crowdin/id.yml
+++ b/modules/openid_connect/config/locales/crowdin/id.yml
@@ -59,9 +59,10 @@ id:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ id:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/it.yml b/modules/openid_connect/config/locales/crowdin/it.yml
index fcf0be841d6b..023f79935c83 100644
--- a/modules/openid_connect/config/locales/crowdin/it.yml
+++ b/modules/openid_connect/config/locales/crowdin/it.yml
@@ -59,9 +59,10 @@ it:
metadata_url: Ho un URL endpoint di ricerca
client_id: Questo è l'ID client che ti viene assegnato dal tuo fornitore OpenID Connect
client_secret: Questa è la chiave segreta del client che ti viene assegnata dal tuo fornitore OpenID Connect
- limit_self_registration: Se l'opzione è abilitata, gli utenti possono registrarsi utilizzando questo fornitore solo se la configurazione da parte del fornitore lo consente.
- display_name: Il nome del fornitore. Questo verrà visualizzato come pulsante di accesso e nell'elenco dei fornitori.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Sostituisci il tenant predefinito con il proprio se applicabile. Consulta questa pagina.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: L'URL a cui il fornitore OpenID Connect deve reindirizzare dopo una richiesta di logout.
claims: >
Puoi effettuare richieste aggiuntive per gli endpoint userinfo e id token. Per maggiori informazioni, consulta [la nostra documentazione OpenID connect](docs_url).
@@ -82,6 +83,7 @@ it:
metadata_url: Ho un URL endpoint di ricerca
endpoint_url: URL dell'endpoint
providers:
+ label_providers: "Providers"
seeded_from_env: "Questo fornitore è stato salvato dalla configurazione dell'ambiente. Non può essere modificato."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ja.yml b/modules/openid_connect/config/locales/crowdin/ja.yml
index cbc00c4956c9..2b8c1674ad37 100644
--- a/modules/openid_connect/config/locales/crowdin/ja.yml
+++ b/modules/openid_connect/config/locales/crowdin/ja.yml
@@ -59,9 +59,10 @@ ja:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ja:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ka.yml b/modules/openid_connect/config/locales/crowdin/ka.yml
index 49dbd9ea83d8..2bc51f64580f 100644
--- a/modules/openid_connect/config/locales/crowdin/ka.yml
+++ b/modules/openid_connect/config/locales/crowdin/ka.yml
@@ -59,9 +59,10 @@ ka:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ka:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/kk.yml b/modules/openid_connect/config/locales/crowdin/kk.yml
index 2b7c36ec2092..49d43fa92ef1 100644
--- a/modules/openid_connect/config/locales/crowdin/kk.yml
+++ b/modules/openid_connect/config/locales/crowdin/kk.yml
@@ -59,9 +59,10 @@ kk:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ kk:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ko.yml b/modules/openid_connect/config/locales/crowdin/ko.yml
index 155da52838ff..7acbe03d976d 100644
--- a/modules/openid_connect/config/locales/crowdin/ko.yml
+++ b/modules/openid_connect/config/locales/crowdin/ko.yml
@@ -37,52 +37,54 @@ ko:
format: "검색 엔드포인트 URL %{message}"
response_is_not_successful: " - 응답 상태는 %{status}입니다."
response_is_not_json: " - JSON 본문을 반환하지 않습니다."
- response_misses_required_attributes: " does not return required attributes. Missing attributes are: %{missing_attributes}."
+ response_misses_required_attributes: " - 필수 특성을 반환하지 않습니다. 누락된 특성: %{missing_attributes}."
provider:
delete_warning:
- input_delete_confirmation: Enter the provider name %{name} to confirm deletion.
- irreversible_notice: Deleting an SSO provider is an irreversible action.
- provider: 'Are you sure you want to delete the SSO provider %{name}? To confirm this action please enter the name of the provider in the field below, this will:'
- delete_result_1: Remove the provider from the list of available providers.
+ input_delete_confirmation: 삭제를 확인하려면 공급자 이름 %{name}(을)를 입력하세요.
+ irreversible_notice: SSO 공급자를 삭제하면 되돌릴 수 없습니다.
+ provider: 'SSO 공급자 %{name}을(를) 삭제하시겠습니까? 이 작업을 확인하려면 아래 필드에 공급자 이름을 입력하세요. 이렇게 하면 다음과 같이 됩니다.'
+ delete_result_1: 사용 가능한 공급자 목록에서 공급자를 제거합니다.
delete_result_user_count:
- zero: No users are currently using this provider. No further action is required.
- one: "One user is currently still using this provider. They will need to be re-invited or logging in with another provider."
- other: "%{count} users are currently still using this provider. They will need to be re-invited or logging in with another provider."
+ zero: 현재 이 공급자를 사용하는 사용자가 없습니다. 추가 조치가 필요하지 않습니다.
+ one: "1명의 사용자가 이 공급자를 계속 사용하고 있습니다. 해당 사용자를 다시 초대하거나 다른 공급자로 로그인해야 합니다."
+ other: "%{count}명의 사용자가 이 공급자를 계속 사용하고 있습니다. 해당 사용자를 다시 초대하거나 다른 공급자로 로그인해야 합니다."
delete_result_direct: 이 공급자는 직접 로그인 공급자로 표시됩니다. 이 설정은 제거되며 사용자는 더 이상 로그인을 위해 해당 공급자로 리디렉션되지 않습니다.
openid_connect:
menu_title: OpenID 공급자
delete_title: "OpenID Connect 공급자 삭제"
instructions:
redirect_url: 로그인 성공 후 OpenID Connect 공급자가 OpenProject로 다시 리디렉션할 때 사용해야 하는 리디렉션 URL입니다.
- endpoint_url: OpenID Connect 공급자가 사용자에게 제공한 엔드포인트 URL
+ endpoint_url: OpenID Connect 공급자가 귀하에게 제공한 엔드포인트 URL
metadata_none: 이 정보가 없습니다
metadata_url: 검색 엔드포인트 URL이 있습니다
- client_id: This is the client ID given to you by your OpenID Connect provider
- client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
- tenant: 'Please replace the default tenant with your own if applicable. See this.'
- post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
+ client_id: OpenID Connect 공급자가 귀하에게 제공한 클라이언트 ID입니다
+ client_secret: OpenID Connect 공급자가 귀하에게 제공한 클라이언트 비밀번호입니다
+ limit_self_registration: 활성화된 경우, 사용자는 공급자 측의 구성에서 허용하는 경우에만 이 공급자를 사용하여 등록할 수 있습니다.
+ display_name: 공급자 이름입니다. 로그인 버튼으로 표시되며 공급자 목록에 표시됩니다.
+ tenant: '해당되는 경우 기본 테넌트를 고유한 테넌트로 교체하세요. 여기를 참조하세요.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
+ post_logout_redirect_uri: 로그아웃 요청 후 OpenID Connect 공급자가 리디렉션해야 하는 URL입니다.
claims: >
- You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
+ 사용자 정보 및 ID 토큰 엔드포인트에 대한 추가 클레임을 요청할 수 있습니다. 자세한 내용은 [OpenID Connect 문서](docs_url)를 참조하세요.
acr_values: >
- Request non-essential claims in an easier format. See [our documentation on acr_values](docs_url) for more information.
+ 필수적이지 않은 클레임은 더 간편한 형식으로 요청하세요. 자세한 내용은 [acr_values에 대한 문서](docs_url)를 참조하세요.
mapping_login: >
- Provide a custom mapping in the userinfo response to be used for the login attribute.
+ 로그인 특성에 사용할 사용자 정보 응답에 사용자 지정 매핑을 제공합니다.
mapping_email: >
- Provide a custom mapping in the userinfo response to be used for the email attribute.
+ 이메일 특성에 사용할 사용자 정보 응답에 사용자 지정 매핑을 제공합니다.
mapping_first_name: >
- Provide a custom mapping in the userinfo response to be used for the first name.
+ 이름에 사용할 사용자 정보 응답에 사용자 지정 매핑을 제공합니다.
mapping_last_name: >
- Provide a custom mapping in the userinfo response to be used for the last name.
+ 성에 사용할 사용자 정보 응답에 사용자 지정 매핑을 제공합니다.
mapping_admin: >
- Provide a custom mapping in the userinfo response to be used for the admin status. It expects a boolean attribute to be returned.
+ 관리자 상태에 사용할 사용자 정보 응답에 사용자 지정 매핑을 제공합니다. 부울 특성이 반환될 것으로 예상됩니다.
settings:
- metadata_none: I don't have this information
- metadata_url: I have a discovery endpoint URL
- endpoint_url: Endpoint URL
+ metadata_none: 이 정보가 없습니다
+ metadata_url: 검색 엔드포인트 URL이 있습니다
+ endpoint_url: 엔드포인트 URL
providers:
- seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
+ label_providers: "Providers"
+ seeded_from_env: "이 공급자는 환경 구성에서 시드되었으며, 편집할 수 없습니다."
google:
name: Google
microsoft_entra:
@@ -96,26 +98,26 @@ ko:
label_empty_title: 아직 구성된 OpenID 공급자가 없습니다.
label_empty_description: 여기에서 보려면 공급자를 추가합니다.
label_metadata: OpenID Connect 검색 엔드포인트
- label_automatic_configuration: Automatic configuration
- label_optional_configuration: Optional configuration
+ label_automatic_configuration: 자동 구성
+ label_optional_configuration: 옵션 구성
label_advanced_configuration: 고급 구성
label_configuration_details: 메타데이터
label_client_details: 클라이언트 세부 정보
label_attribute_mapping: 특성 매핑
- client_details_description: Configuration details of OpenProject as an OIDC client
+ client_details_description: OIDC 클라이언트인 OpenProject의 구성 세부 정보
no_results_table: 아직 정의된 공급자가 없습니다.
plural: OpenID 공급자
singular: OpenID 공급자
section_texts:
- metadata: Pre-fill configuration using an OpenID Connect discovery endpoint URL
- metadata_form_banner: Editing the discovery endpoint may override existing pre-filled metadata values.
- metadata_form_title: OpenID Connect Discovery endpoint
- metadata_form_description: If your identity provider has a discovery endpoint URL. Use it below to pre-fill configuration.
- configuration_metadata: The information has been pre-filled using the supplied discovery endpoint. In most cases, they do not require editing.
- configuration: Configuration details of the OpenID Connect provider
- display_name: The display name visible to users.
- attribute_mapping: Configure the mapping of attributes between OpenProject and the OpenID Connect provider.
- claims: Request additional claims for the ID token or userinfo response.
+ metadata: OpenID Connect 검색 엔드포인트 URL을 사용하여 구성 미리 채우기
+ metadata_form_banner: 검색 엔드포인트를 편집하면 미리 채워진 기존 메타데이터 값이 재정의될 수 있습니다.
+ metadata_form_title: OpenID Connect 검색 엔드포인트
+ metadata_form_description: ID 공급자에게 검색 엔드포인트 URL이 있는 경우. 구성을 미리 채우려면 아래 항목을 사용하세요.
+ configuration_metadata: 이 정보는 제공된 검색 엔드포인트를 사용하여 미리 채워졌습니다. 대부분의 경우 편집할 필요가 없습니다.
+ configuration: OpenID Connect 공급자의 구성 세부 정보
+ display_name: 사용자에게 표시되는 표시 이름입니다.
+ attribute_mapping: OpenProject와 OpenID Connect 공급자 간의 특성 매핑을 구성합니다.
+ claims: ID 토큰 또는 사용자 정보 응답에 대한 추가 클레임을 요청합니다.
setting_instructions:
limit_self_registration: >
활성화되면, 사용자는 자체 등록 설정에서 허용하는 경우에만 이 공급자를 사용하여 등록할 수 있습니다.
diff --git a/modules/openid_connect/config/locales/crowdin/lt.yml b/modules/openid_connect/config/locales/crowdin/lt.yml
index 7d8a87dd8f0a..8b21a7bd605d 100644
--- a/modules/openid_connect/config/locales/crowdin/lt.yml
+++ b/modules/openid_connect/config/locales/crowdin/lt.yml
@@ -59,9 +59,10 @@ lt:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ lt:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/lv.yml b/modules/openid_connect/config/locales/crowdin/lv.yml
index d23e5a89a769..5730c6228f4a 100644
--- a/modules/openid_connect/config/locales/crowdin/lv.yml
+++ b/modules/openid_connect/config/locales/crowdin/lv.yml
@@ -59,9 +59,10 @@ lv:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ lv:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/mn.yml b/modules/openid_connect/config/locales/crowdin/mn.yml
index 4912b38ffba8..5a7e93b75ab9 100644
--- a/modules/openid_connect/config/locales/crowdin/mn.yml
+++ b/modules/openid_connect/config/locales/crowdin/mn.yml
@@ -59,9 +59,10 @@ mn:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ mn:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ms.yml b/modules/openid_connect/config/locales/crowdin/ms.yml
index f0ae18934d39..9e24b6dfe5f3 100644
--- a/modules/openid_connect/config/locales/crowdin/ms.yml
+++ b/modules/openid_connect/config/locales/crowdin/ms.yml
@@ -59,9 +59,10 @@ ms:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ms:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ne.yml b/modules/openid_connect/config/locales/crowdin/ne.yml
index 0df3afb001e5..2c5bc8e68b5f 100644
--- a/modules/openid_connect/config/locales/crowdin/ne.yml
+++ b/modules/openid_connect/config/locales/crowdin/ne.yml
@@ -59,9 +59,10 @@ ne:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ne:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/nl.yml b/modules/openid_connect/config/locales/crowdin/nl.yml
index e14645501c38..796203bf7f2b 100644
--- a/modules/openid_connect/config/locales/crowdin/nl.yml
+++ b/modules/openid_connect/config/locales/crowdin/nl.yml
@@ -1,7 +1,7 @@
nl:
plugin_openproject_openid_connect:
name: "OpenProject OpenID Connect"
- description: "Adds OmniAuth OpenID Connect strategy providers to OpenProject."
+ description: "Voegt OmniAuth OpenID Connect strategieproviders toe aan OpenProject."
logout_warning: >
U bent uitgelogd. De inhoud van uw formulier kan verloren gaan. Alstublieft [log in].
activemodel:
@@ -15,18 +15,18 @@ nl:
secret: Geheim
scope: Scope
limit_self_registration: Beperk zelf-registratie
- authorization_endpoint: Authorization endpoint
- userinfo_endpoint: User information endpoint
- token_endpoint: Token endpoint
- end_session_endpoint: End session endpoint
- post_logout_redirect_uri: Post logout redirect URI
+ authorization_endpoint: Autorisatie eindpunt
+ userinfo_endpoint: Eindpunt gebruikersinformatie
+ token_endpoint: Eindpunt token
+ end_session_endpoint: End sessie endpoint
+ post_logout_redirect_uri: URI voor doorsturen na uitloggen
jwks_uri: JWKS URI
- issuer: Issuer
- tenant: Tenant
- metadata_url: Metadata URL
- icon: Custom icon
+ issuer: Uitgever
+ tenant: Huurder
+ metadata_url: Metagegevens URL
+ icon: Aangepast icoon
claims: Claims
- acr_values: ACR values
+ acr_values: ACR waarden
redirect_url: Redirect URL
activerecord:
errors:
@@ -35,17 +35,17 @@ nl:
attributes:
metadata_url:
format: "Discovery endpoint URL %{message}"
- response_is_not_successful: " responds with %{status}."
- response_is_not_json: " does not return JSON body."
- response_misses_required_attributes: " does not return required attributes. Missing attributes are: %{missing_attributes}."
+ response_is_not_successful: " reageert met %{status}."
+ response_is_not_json: " retourneert geen JSON body."
+ response_misses_required_attributes: " geeft niet de vereiste attributen terug. Ontbrekende attributen zijn: %{missing_attributes}."
provider:
delete_warning:
- input_delete_confirmation: Enter the provider name %{name} to confirm deletion.
- irreversible_notice: Deleting an SSO provider is an irreversible action.
- provider: 'Are you sure you want to delete the SSO provider %{name}? To confirm this action please enter the name of the provider in the field below, this will:'
- delete_result_1: Remove the provider from the list of available providers.
+ input_delete_confirmation: Voer de providernaam %{name} in om de verwijdering te bevestigen.
+ irreversible_notice: Het verwijderen van een SSO provider is een onomkeerbare actie.
+ provider: 'Weet u zeker dat u de SSO provider %{name} wilt verwijderen? Voer de naam van de provider in het onderstaande veld in om deze actie te bevestigen, dit wil:'
+ delete_result_1: Verwijder de provider uit de lijst met beschikbare providers.
delete_result_user_count:
- zero: No users are currently using this provider. No further action is required.
+ zero: Er zijn momenteel geen gebruikers die deze provider gebruiken. Er is geen verdere actie vereist.
one: "One user is currently still using this provider. They will need to be re-invited or logging in with another provider."
other: "%{count} users are currently still using this provider. They will need to be re-invited or logging in with another provider."
delete_result_direct: This provider is marked as a direct login provider. The setting will be removed and users will no longer be redirected to the provider for login.
@@ -59,9 +59,10 @@ nl:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ nl:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/no.yml b/modules/openid_connect/config/locales/crowdin/no.yml
index 4f99f39732c1..a49e692bca5c 100644
--- a/modules/openid_connect/config/locales/crowdin/no.yml
+++ b/modules/openid_connect/config/locales/crowdin/no.yml
@@ -59,9 +59,10 @@
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/pl.yml b/modules/openid_connect/config/locales/crowdin/pl.yml
index 1edd0bed86ff..ce779c7dd5c0 100644
--- a/modules/openid_connect/config/locales/crowdin/pl.yml
+++ b/modules/openid_connect/config/locales/crowdin/pl.yml
@@ -59,9 +59,10 @@ pl:
metadata_url: Mam adres URL punktu końcowego odkrywania
client_id: Jest to identyfikator klienta nadany Ci przez dostawcę OpenID Connect
client_secret: Jest to klucz tajny klienta podany Ci przez dostawcę OpenID Connect
- limit_self_registration: Jeśli opcja jest włączona, użytkownicy mogą rejestrować się przy użyciu tego dostawcy tylko wtedy, gdy pozwala na to konfiguracja po stronie dostawcy.
- display_name: Nazwa dostawcy. Zostanie wyświetlona jako przycisk logowania i element listy dostawców.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'W razie potrzeby zastąp dzierżawcę domyślnego własnym. Zobacz to.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: Adres URL, do którego ma przekierowywać dostawca OpenID Connect po żądaniu wylogowania.
claims: >
Możesz zażądać dodatkowych oświadczeń dla punktów końcowych userinfo i id token. Więcej informacji znajdziesz w [naszej dokumentacji OpenID connect](docs_url).
@@ -82,6 +83,7 @@ pl:
metadata_url: Mam adres URL punktu końcowego odkrywania
endpoint_url: Adres URL punktu końcowego
providers:
+ label_providers: "Providers"
seeded_from_env: "Ten dostawca został zainicjowany z konfiguracji środowiska. Nie można go edytować."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/pt-BR.yml b/modules/openid_connect/config/locales/crowdin/pt-BR.yml
index 869531341467..49ceae3c4537 100644
--- a/modules/openid_connect/config/locales/crowdin/pt-BR.yml
+++ b/modules/openid_connect/config/locales/crowdin/pt-BR.yml
@@ -47,7 +47,7 @@ pt-BR:
delete_result_user_count:
zero: Nenhum usuário está utilizando este provedor no momento. Não é necessária nenhuma ação adicional.
one: "Há um usuário utilizando este provedor. Ele precisará ser re-convidado ou fazer login com outro provedor."
- other: "Atualmente, %{count} usuários ainda estão usando este provedor. Eles precisarão ser re-convidados ou fazer login com outro provedor."
+ other: "Há %{count} usuários utilizando este provedor. Eles precisarão ser re-convidados ou fazer login com outro provedor."
delete_result_direct: Este provedor está configurado como provedor de login direto. A configuração será removida e os usuários não serão mais redirecionados para ele para realizar o login.
openid_connect:
menu_title: Provedores OpenID
@@ -62,6 +62,7 @@ pt-BR:
limit_self_registration: Se ativado, os usuários só poderão se registrar usando este provedor se a configuração no lado do provedor permitir.
display_name: Nome do provedor. Ele será exibido como o botão de login e na lista de provedores.
tenant: 'Substitua o locatário padrão pelo seu, se necessário. Consulte aqui.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: O URL para a qual o provedor OpenID Connect deve redirecionar após a solicitação de logout.
claims: >
Você pode solicitar reivindicações adicionais para os endpoints userinfo e id token. Consulte [nossa documentação sobre OpenID Connect](docs_url) para obter mais informações.
@@ -82,6 +83,7 @@ pt-BR:
metadata_url: Tenho um URL de endpoint de descoberta
endpoint_url: URL de Endpoint
providers:
+ label_providers: "Providers"
seeded_from_env: "Este provedor foi configurado a partir das configurações do ambiente e não pode ser editado."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/pt-PT.yml b/modules/openid_connect/config/locales/crowdin/pt-PT.yml
index 83c9c7bc498e..df4c222d033e 100644
--- a/modules/openid_connect/config/locales/crowdin/pt-PT.yml
+++ b/modules/openid_connect/config/locales/crowdin/pt-PT.yml
@@ -59,9 +59,10 @@ pt-PT:
metadata_url: Tenho um URL de endpoint de descoberta
client_id: Este é o ID de cliente que lhe foi atribuído pelo seu fornecedor OpenID Connect
client_secret: Este é o segredo do cliente que lhe foi atribuído pelo seu fornecedor OpenID Connect
- limit_self_registration: Se estiver ativado, os utilizadores só podem registar-se utilizando este fornecedor se a configuração no lado do fornecedor o permitir.
+ limit_self_registration: Se estiver ativado, os utilizadores só podem registar-se utilizando este fornecedor se a configuração do fornecedor o permitir.
display_name: O nome do fornecedor. Este nome será apresentado no botão de início de sessão e na lista de fornecedores.
tenant: 'Substitua o inquilino predefinido pelo seu próprio inquilino, se aplicável. Consulte este artigo.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: O URL para o qual o fornecedor do OpenID Connect deve redirecionar após um pedido de encerramento de sessão.
claims: >
Pode solicitar reclamações adicionais para os endpoints userinfo e id token. Consulte [a nossa documentação sobre o OpenID connect](docs_url) para obter mais informações.
@@ -82,6 +83,7 @@ pt-PT:
metadata_url: Tenho um URL de endpoint de descoberta
endpoint_url: URL do Endpoint
providers:
+ label_providers: "Providers"
seeded_from_env: "Este fornecedor foi semeado a partir da configuração do ambiente. Não pode ser editado."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ro.yml b/modules/openid_connect/config/locales/crowdin/ro.yml
index f74f3ef2a08d..ef872c50096d 100644
--- a/modules/openid_connect/config/locales/crowdin/ro.yml
+++ b/modules/openid_connect/config/locales/crowdin/ro.yml
@@ -59,9 +59,10 @@ ro:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ ro:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/ru.yml b/modules/openid_connect/config/locales/crowdin/ru.yml
index 7fbfeeb59020..09d5c477af7e 100644
--- a/modules/openid_connect/config/locales/crowdin/ru.yml
+++ b/modules/openid_connect/config/locales/crowdin/ru.yml
@@ -8,7 +8,7 @@ ru:
attributes:
openid_connect/provider:
name: Имя
- slug: Unique identifier
+ slug: Уникальный идентификатор
display_name: Отображаемое имя
client_id: ID клиента
client_secret: Закрытый ключ клиента
@@ -27,7 +27,7 @@ ru:
icon: Пользовательская иконка
claims: Претензии
acr_values: Значения ACR
- redirect_url: Redirect URL
+ redirect_url: URL переадресации
activerecord:
errors:
models:
@@ -53,36 +53,38 @@ ru:
menu_title: Провайдеры OpenID
delete_title: "Удалить провайдера OpenID Connect"
instructions:
- redirect_url: This is the redirect URL that the OpenID Connect provider should use to redirect back to OpenProject after a successful login.
+ redirect_url: Это URL, который провайдер OpenID Connect должен использовать для перенаправления обратно на OpenProject после успешного входа в систему.
endpoint_url: URL-адрес конечной точки, указанный поставщиком OpenID Connect
metadata_none: У меня нет этой информации
metadata_url: У меня есть URL конечной точки обнаружения
client_id: Это идентификатор клиента, присвоенный вам провайдером OpenID Connect
client_secret: Это ключ клиента, предоставленный вам провайдером OpenID Connect
- limit_self_registration: Если этот параметр включен, пользователи могут регистрироваться только с помощью этого провайдера, если это позволяет конфигурация провайдера.
+ limit_self_registration: Если эта опция включена, пользователи смогут регистрироваться у этого провайдера только в том случае, если конфигурация провайдера позволяет это делать.
display_name: Это имя провайдера. Оно будет отображаться как кнопка входа и в списке провайдеров.
tenant: 'Пожалуйста, замените владельца по умолчанию на своего собственного, если это применимо. См. это.'
+ scope: Если Вы хотите запросить пользовательские области видимости, Вы можете добавить здесь одно или несколько значений диапазона, разделенных пробелами. Более подробную информацию Вы найдете в [документации OpenID Connect](docs_url).
post_logout_redirect_uri: URL-адрес, на который провайдер OpenID Connect должен перенаправить Вас после запроса на выход из системы.
claims: >
Вы можете запросить дополнительные требования к конечным точкам пользовательской информации и id токена. Пожалуйста, обратитесь к [нашей документации для OpenID подключения](docs_url) для получения дополнительной информации.
acr_values: >
- Request non-essential claims in an easier format. See [our documentation on acr_values](docs_url) for more information.
+ Запрос несущественных требований в более удобном формате. Смотрите [нашу документацию по acr_values](docs_url) для получения дополнительной информации.
mapping_login: >
- Provide a custom mapping in the userinfo response to be used for the login attribute.
+ Предоставьте пользовательское отображение в ответе userinfo, которое будет использоваться для атрибута логина.
mapping_email: >
- Provide a custom mapping in the userinfo response to be used for the email attribute.
+ Предоставьте пользовательское отображение в ответе userinfo, которое будет использоваться для атрибута электронной почты.
mapping_first_name: >
- Provide a custom mapping in the userinfo response to be used for the first name.
+ Предоставьте пользовательское отображение в ответе userinfo, которое будет использоваться для атрибута имени.
mapping_last_name: >
- Provide a custom mapping in the userinfo response to be used for the last name.
+ Предоставьте пользовательское отображение в ответе userinfo, которое будет использоваться для атрибута фамилии.
mapping_admin: >
- Provide a custom mapping in the userinfo response to be used for the admin status. It expects a boolean attribute to be returned.
+ Предоставьте пользовательское отображение в ответе userinfo, которое будет использоваться для определения статуса администратора. Ожидается, что будет возвращен атрибут boolean.
settings:
metadata_none: У меня нет этой информации
metadata_url: У меня есть URL конечной точки обнаружения
endpoint_url: URL конечной точки
providers:
- seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
+ label_providers: "Провайдеры"
+ seeded_from_env: "Этот провайдер был инициализирован из конфигурации среды. Он не может быть отредактирован."
google:
name: Google
microsoft_entra:
@@ -90,7 +92,7 @@ ru:
custom:
name: Пользовательский
upsale:
- description: Connect OpenProject to an OpenID connect identity provider
+ description: Подключить OpenProject к провайдеру идентификации OpenID connect
label_add_new: Добавить нового провайдера OpenID
label_edit: Редактировать провайдера OpenID %{name}
label_empty_title: Провайдеры OpenID еще не настроены.
@@ -107,15 +109,15 @@ ru:
plural: Провайдеры OpenID
singular: Провайдер OpenID
section_texts:
- metadata: Pre-fill configuration using an OpenID Connect discovery endpoint URL
- metadata_form_banner: Editing the discovery endpoint may override existing pre-filled metadata values.
+ metadata: Предварительно заполните конфигурацию, используя URL обнаружения OpenID Connect
+ metadata_form_banner: Редактирование URL может перезаписать ранее заполненные значения метаданных.
metadata_form_title: Конечная точка обнаружения OpenID Connect
- metadata_form_description: If your identity provider has a discovery endpoint URL. Use it below to pre-fill configuration.
- configuration_metadata: The information has been pre-filled using the supplied discovery endpoint. In most cases, they do not require editing.
- configuration: Configuration details of the OpenID Connect provider
+ metadata_form_description: Если ваш поставщик идентификации имеет URL обнаружения. Используйте его ниже для предварительного заполнения.
+ configuration_metadata: Эта информация была предварительно заполнена с использованием URL обнаружения. В большинстве случаев она не требует редактирования.
+ configuration: Подробности конфигурации провайдера OpenID Connect
display_name: Отображаемое имя, видимое пользователям.
- attribute_mapping: Configure the mapping of attributes between OpenProject and the OpenID Connect provider.
- claims: Request additional claims for the ID token or userinfo response.
+ attribute_mapping: Настройте сопоставление атрибутов между OpenProject и провайдером OpenID Connect.
+ claims: Запросите дополнительные требования к ID-токену или ответу userinfo.
setting_instructions:
limit_self_registration: >
Если включено, пользователи могут зарегистрироваться только с помощью данного провайдера, если это позволяет сама регистрация.
diff --git a/modules/openid_connect/config/locales/crowdin/rw.yml b/modules/openid_connect/config/locales/crowdin/rw.yml
index e0805990a366..22e141b152ff 100644
--- a/modules/openid_connect/config/locales/crowdin/rw.yml
+++ b/modules/openid_connect/config/locales/crowdin/rw.yml
@@ -59,9 +59,10 @@ rw:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ rw:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/si.yml b/modules/openid_connect/config/locales/crowdin/si.yml
index 36b201aae74e..69b0cfde684b 100644
--- a/modules/openid_connect/config/locales/crowdin/si.yml
+++ b/modules/openid_connect/config/locales/crowdin/si.yml
@@ -59,9 +59,10 @@ si:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ si:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/sk.yml b/modules/openid_connect/config/locales/crowdin/sk.yml
index ff2183aa9e01..a33bf7eed056 100644
--- a/modules/openid_connect/config/locales/crowdin/sk.yml
+++ b/modules/openid_connect/config/locales/crowdin/sk.yml
@@ -59,9 +59,10 @@ sk:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ sk:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/sl.yml b/modules/openid_connect/config/locales/crowdin/sl.yml
index 6c5e965e24bb..6edf3b7868a1 100644
--- a/modules/openid_connect/config/locales/crowdin/sl.yml
+++ b/modules/openid_connect/config/locales/crowdin/sl.yml
@@ -59,9 +59,10 @@ sl:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ sl:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/sr.yml b/modules/openid_connect/config/locales/crowdin/sr.yml
index 8ddb323d4ae6..43b321a70b0f 100644
--- a/modules/openid_connect/config/locales/crowdin/sr.yml
+++ b/modules/openid_connect/config/locales/crowdin/sr.yml
@@ -59,9 +59,10 @@ sr:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ sr:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/sv.yml b/modules/openid_connect/config/locales/crowdin/sv.yml
index 17fca2880da2..fb14d3839f14 100644
--- a/modules/openid_connect/config/locales/crowdin/sv.yml
+++ b/modules/openid_connect/config/locales/crowdin/sv.yml
@@ -59,9 +59,10 @@ sv:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ sv:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/th.yml b/modules/openid_connect/config/locales/crowdin/th.yml
index 3445696c370d..6df03dca9103 100644
--- a/modules/openid_connect/config/locales/crowdin/th.yml
+++ b/modules/openid_connect/config/locales/crowdin/th.yml
@@ -59,9 +59,10 @@ th:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ th:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/tr.yml b/modules/openid_connect/config/locales/crowdin/tr.yml
index 5e9e151b75c3..d51b2c62cf99 100644
--- a/modules/openid_connect/config/locales/crowdin/tr.yml
+++ b/modules/openid_connect/config/locales/crowdin/tr.yml
@@ -1,7 +1,7 @@
tr:
plugin_openproject_openid_connect:
name: "OpenProject OpenID Connect"
- description: "Adds OmniAuth OpenID Connect strategy providers to OpenProject."
+ description: "OmniAuth OpenID Bağlantı strateji sağlayıcılarını OpenProject'e ekler."
logout_warning: >
Çıkış yaptınız. Gönderdiğiniz herhangi bir formun içeriği kaybolabilir. Lütfen giriş yapın].
activemodel:
@@ -10,23 +10,23 @@ tr:
name: İsim
slug: Unique identifier
display_name: Ekran adı
- client_id: Client ID
- client_secret: Client secret
+ client_id: İstemci ID
+ client_secret: İstemci gizliliği
secret: Gizli
scope: kapsam
limit_self_registration: Limit self registration
- authorization_endpoint: Authorization endpoint
- userinfo_endpoint: User information endpoint
- token_endpoint: Token endpoint
- end_session_endpoint: End session endpoint
- post_logout_redirect_uri: Post logout redirect URI
+ authorization_endpoint: Yetkilendirme bitiş noktası
+ userinfo_endpoint: Kullanıcı bilgisi bitiş noktası
+ token_endpoint: Gösterge bitiş noktası
+ end_session_endpoint: Oturum bitiş noktasını sonlandır
+ post_logout_redirect_uri: Oturum kapatma sonrası yönlendirme URI'si
jwks_uri: JWKS URI
- issuer: Issuer
- tenant: Tenant
- metadata_url: Metadata URL
- icon: Custom icon
- claims: Claims
- acr_values: ACR values
+ issuer: Sağlayıcı
+ tenant: Müşteri
+ metadata_url: Metaveri URL'si
+ icon: Özel simge
+ claims: Bilgiler
+ acr_values: ACR değerleri
redirect_url: Redirect URL
activerecord:
errors:
@@ -34,41 +34,42 @@ tr:
openid_connect/provider:
attributes:
metadata_url:
- format: "Discovery endpoint URL %{message}"
- response_is_not_successful: " responds with %{status}."
- response_is_not_json: " does not return JSON body."
- response_misses_required_attributes: " does not return required attributes. Missing attributes are: %{missing_attributes}."
+ format: "Keşif uç noktası URL'si %{message}"
+ response_is_not_successful: " %{status} ile yanıt verir."
+ response_is_not_json: " JSON gövdesini cevap olarak vermez."
+ response_misses_required_attributes: " gerekli öznitelikleri döndürmez. Eksik öznitelikler: %{missing_attributes}."
provider:
delete_warning:
- input_delete_confirmation: Enter the provider name %{name} to confirm deletion.
- irreversible_notice: Deleting an SSO provider is an irreversible action.
- provider: 'Are you sure you want to delete the SSO provider %{name}? To confirm this action please enter the name of the provider in the field below, this will:'
- delete_result_1: Remove the provider from the list of available providers.
+ input_delete_confirmation: Silme işlemini onaylamak için sağlayıcı %{name} ismini girin.
+ irreversible_notice: SSO sağlayıcısını silme geri alınamaz bir işlemdir.
+ provider: 'SSO sağlayıcısı %{name} silmek istediğinize emin misiniz? İşlemi onaylamak için lütfen sağlayıcının adını aşağıdaki alana giriniz:'
+ delete_result_1: Sağlayıcının adını mevcut sağlayıcılardan kaldır.
delete_result_user_count:
- zero: No users are currently using this provider. No further action is required.
- one: "One user is currently still using this provider. They will need to be re-invited or logging in with another provider."
- other: "%{count} users are currently still using this provider. They will need to be re-invited or logging in with another provider."
- delete_result_direct: This provider is marked as a direct login provider. The setting will be removed and users will no longer be redirected to the provider for login.
+ zero: Bu sağlayıcıyı hiçbir kullanıcı kullanmıyor. Ek bir işleme gerek yok.
+ one: "Bir kullanıcı bu sağlayıcıyı kullanıyor. Tekrar davet edilmesi ya da başka bir kullanıcı ile giriş yapması gerekecek."
+ other: "%{count} kullanıcı bu sağlayıcıyı kullanıyor. Tekrar davet edilmeleri ya da başka bir kullanıcı ile giriş yapmaları gerekecek."
+ delete_result_direct: Bu sağlayıcı doğrudan oturum açma sağlayıcısı olarak işaretlenmiştir. Ayar kaldırılacak ve kullanıcılar artık oturum açmak için sağlayıcıya yönlendirilmeyecektir.
openid_connect:
menu_title: OpenID sağlayıcıları
- delete_title: "Delete OpenID Connect provider"
+ delete_title: "OpenID Connect sağlayıcısını sil"
instructions:
redirect_url: This is the redirect URL that the OpenID Connect provider should use to redirect back to OpenProject after a successful login.
- endpoint_url: The endpoint URL given to you by the OpenID Connect provider
- metadata_none: I don't have this information
- metadata_url: I have a discovery endpoint URL
- client_id: This is the client ID given to you by your OpenID Connect provider
- client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
- tenant: 'Please replace the default tenant with your own if applicable. See this.'
- post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
+ endpoint_url: OpenID Connect sağlayıcısı tarafından size verilen uç nokta URL'si
+ metadata_none: Bu bilgiye sahip değilim
+ metadata_url: Bir keşif uç noktası URL'im var
+ client_id: OpenID Connect sağlayıcısı tarafından size verilen istemci URL'si
+ client_secret: OpenID Connect sağlayıcısı tarafından size verilen gizlilik URL'si
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
+ tenant: 'Lütfen öntanımlı müşteriyi kendinizinkiyle değiştirin, mümkünse. Bu belgeye bakın.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
+ post_logout_redirect_uri: OpenID Connect sağlayıcısının bir oturum kapatma isteğinden sonra yeniden yönlendirmesi gereken URL.
claims: >
- You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
+ Kullanıcı bilgisi ve id göstergesi uç noktaları için ek bilgi talebinde bulunabilirsiniz. Daha fazla bilgi için lütfen [OpenID bağlantı belgelerimize] (docs_url) bakın.
acr_values: >
- Request non-essential claims in an easier format. See [our documentation on acr_values](docs_url) for more information.
+ Zorunlu olmayan bilgileri daha kolay bir biçimde talep edin. Daha fazla bilgi için [acr_values hakkındaki belgelerimize] (docs_url) bakın.
mapping_login: >
- Provide a custom mapping in the userinfo response to be used for the login attribute.
+ Oturum açma özniteliği için kullanılmak üzere userinfo yanıtında özel bir eşleme sağlayın.
mapping_email: >
Provide a custom mapping in the userinfo response to be used for the email attribute.
mapping_first_name: >
@@ -82,6 +83,7 @@ tr:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/uk.yml b/modules/openid_connect/config/locales/crowdin/uk.yml
index d396ba46abd7..c9237d8ba7a7 100644
--- a/modules/openid_connect/config/locales/crowdin/uk.yml
+++ b/modules/openid_connect/config/locales/crowdin/uk.yml
@@ -59,9 +59,10 @@ uk:
metadata_url: У мене є URL-адреса кінцевої точки виявлення
client_id: ID клієнта, наданий вам постачальником OpenID Connect
client_secret: Це клієнтський секрет ключ, наданий вам постачальником OpenID Connect
- limit_self_registration: Якщо увімкнено, користувачі можуть реєструватися з використанням цього провайдера, якщо це дозволено конфігурацією на стороні провайдера.
- display_name: Ім’я постачальника послуг. Відображатиметься як кнопка входу й пункт у списку постачальників.
+ limit_self_registration: Якщо увімкнено, користувачі можуть зареєструватися за допомогою цього провайдера лише за умови, що конфігурація з боку провайдера це дозволяє.
+ display_name: Назва провайдера. Це буде зображено як кнопка входу та у списку провайдерів.
tenant: 'Замініть клієнта за замовчуванням власним, якщо це можливо. Див. тут.'
+ scope: Якщо ви хочете запросити користувацькі області, ви можете додати одне або кілька значень областей, розділених пробілами, тут. Для отримання додаткової інформації дивіться [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: URL-адреса, на яку постачальник послуг OpenID Connect має виконувати переспрямування після отримання запиту на вихід.
claims: >
Ви також можете надсилати запити на кінцеві точки даних користувача й маркера ID. Щоб дізнатися більше, ознайомтеся з [нашою документацією щодо OpenID Connect](docs_url).
@@ -82,6 +83,7 @@ uk:
metadata_url: У мене є URL-адреса кінцевої точки виявлення
endpoint_url: URL кінцевої точки
providers:
+ label_providers: "Providers"
seeded_from_env: "Цього постачальника послуг додано з конфігурації середовища. Його не можна змінити."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/uz.yml b/modules/openid_connect/config/locales/crowdin/uz.yml
index dbbc2a4b99aa..220b3facffaf 100644
--- a/modules/openid_connect/config/locales/crowdin/uz.yml
+++ b/modules/openid_connect/config/locales/crowdin/uz.yml
@@ -59,9 +59,10 @@ uz:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ uz:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/vi.yml b/modules/openid_connect/config/locales/crowdin/vi.yml
index 545d5ab26fb1..cfd53d4ad83d 100644
--- a/modules/openid_connect/config/locales/crowdin/vi.yml
+++ b/modules/openid_connect/config/locales/crowdin/vi.yml
@@ -59,9 +59,10 @@ vi:
metadata_url: I have a discovery endpoint URL
client_id: This is the client ID given to you by your OpenID Connect provider
client_secret: This is the client secret given to you by your OpenID Connect provider
- limit_self_registration: If enabled, users can only register using this provider if configuration on the prvoder's end allows it.
- display_name: Then name of the provider. This will be displayed as the login button and in the list of providers.
+ limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
+ display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: 'Please replace the default tenant with your own if applicable. See this.'
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -82,6 +83,7 @@ vi:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/config/locales/crowdin/zh-CN.yml b/modules/openid_connect/config/locales/crowdin/zh-CN.yml
index d9e3315f2013..c946d002dd2b 100644
--- a/modules/openid_connect/config/locales/crowdin/zh-CN.yml
+++ b/modules/openid_connect/config/locales/crowdin/zh-CN.yml
@@ -59,9 +59,10 @@ zh-CN:
metadata_url: 我有一个发现端点网址
client_id: 这是您的 OpenID Connect 提供商给您的客户端 ID
client_secret: 这是您的 OpenID Connect 提供商给您的客户端密钥
- limit_self_registration: 如果启用,用户只能在提供商端的配置允许的情况下使用此提供商注册。
+ limit_self_registration: 如果启用,用户只能在提供商的配置允许的情况下使用该提供商进行注册。
display_name: 提供商的名称。这将显示为登录按钮以及提供商列表中的名称。
tenant: '如果适用,请将默认租户替换为您自己的租户。请参阅此处。'
+ scope: 如果你需要自定义场景值,可以在这里添加一个值,或者用空格分开的多个值。详情参考 [OpenID 连接文档](docs_url)。
post_logout_redirect_uri: OpenID Connect 提供商在注销请求后应重定向到的 URL。
claims: >
您可以请求用户信息和 Id 令牌端点的附加声明。有关更多信息,请参阅 [我们的 OpenID Connect 文档](docs_url)。
@@ -82,6 +83,7 @@ zh-CN:
metadata_url: 我有一个发现端点 URL
endpoint_url: 端点 URL
providers:
+ label_providers: "供应商"
seeded_from_env: "此提供商是从环境配置中生成的。它无法被编辑。"
google:
name: 谷歌
diff --git a/modules/openid_connect/config/locales/crowdin/zh-TW.yml b/modules/openid_connect/config/locales/crowdin/zh-TW.yml
index bf66475f2146..61b9f6383ebe 100644
--- a/modules/openid_connect/config/locales/crowdin/zh-TW.yml
+++ b/modules/openid_connect/config/locales/crowdin/zh-TW.yml
@@ -59,9 +59,10 @@ zh-TW:
metadata_url: 我有一個發現端點 URL
client_id: 這是 OpenID Connect 提供者給予您的用戶端 ID
client_secret: 這是 OpenID Connect 提供者給予您的用戶端密鑰
- limit_self_registration: 如果啟用,使用者只能在 prvoder 端設定允許的情況下使用此提供者註冊。
- display_name: 提供商的名稱。這將會顯示在登入按鈕及提供者清單中。
+ limit_self_registration: 如果啟用,只有在提供商端的設定允許的情況下,使用者才能使用此提供商註冊。
+ display_name: 提供商的名稱。這將會顯示在登入按鈕及提供商清單中。
tenant: '如果適用,請用您自己的租戶取代預設租戶。請參閱此內容。'
+ scope: 如果您想要要求自訂範圍,您可以在此新增一個或多個範圍值,並以空格分隔。如需詳細資訊,請參閱 [OpenID Connect 文件](docs_url)。
post_logout_redirect_uri: OpenID Connect 提供者應在登出請求後重定向至的 URL。
claims: >
您可以針對 使用者資訊 和 id token(權杖) 端點要求額外的資料。請參閱 [我們的 OpenID connect 文件](docs_url) 以取得更多資訊。
@@ -82,6 +83,7 @@ zh-TW:
metadata_url: 我有發現一個端點 URL
endpoint_url: 端點 URL
providers:
+ label_providers: "提供商"
seeded_from_env: "此提供者是從環境組態根植的。無法編輯。"
google:
name: Google
diff --git a/modules/openid_connect/config/locales/en.yml b/modules/openid_connect/config/locales/en.yml
index ed720f91b4bb..e1a8f4431cdd 100644
--- a/modules/openid_connect/config/locales/en.yml
+++ b/modules/openid_connect/config/locales/en.yml
@@ -66,6 +66,7 @@ en:
limit_self_registration: If enabled, users can only register using this provider if configuration on the provider's end allows it.
display_name: The name of the provider. This will be displayed as the login button and in the list of providers.
tenant: Please replace the default tenant with your own if applicable. See this.
+ scope: If you want to request custom scopes, you can add one or multiple scope values separated by spaces here. For more information, see the [OpenID Connect documentation](docs_url).
post_logout_redirect_uri: The URL the OpenID Connect provider should redirect to after a logout request.
claims: >
You can request additional claims for the userinfo and id token endpoints. Please see [our OpenID connect documentation](docs_url) for more information.
@@ -87,6 +88,7 @@ en:
metadata_url: I have a discovery endpoint URL
endpoint_url: Endpoint URL
providers:
+ label_providers: "Providers"
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
google:
name: Google
diff --git a/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb b/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb
index 714723041076..ab1f0cd42cac 100644
--- a/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb
+++ b/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb
@@ -64,6 +64,9 @@
fill_in "Client ID", with: "client_id"
fill_in "Client secret", with: "client secret"
+ # Scope
+ fill_in "Scope", with: "custom_scope another_scope"
+
click_link_or_button "Continue"
# Mapping form
diff --git a/modules/openid_connect/spec/migration/migrate_oidc_settings_to_providers_spec.rb b/modules/openid_connect/spec/migration/migrate_oidc_settings_to_providers_spec.rb
index 045779278060..2938dbb23c3c 100644
--- a/modules/openid_connect/spec/migration/migrate_oidc_settings_to_providers_spec.rb
+++ b/modules/openid_connect/spec/migration/migrate_oidc_settings_to_providers_spec.rb
@@ -107,6 +107,7 @@ class TestOpenIDConnectProvider < OpenIDConnect::Provider
host: "localhost",
port: "8080",
scheme: "http",
+ scope: ["foo", "bar"],
identifier: "http://localhost:3000",
secret: "IVl6GxxujAQ3mt6thAXKxyYYvmyRr8jw",
issuer: "http://localhost:8080/realms/test",
@@ -129,6 +130,7 @@ class TestOpenIDConnectProvider < OpenIDConnect::Provider
expect(provider.host).to eq "localhost"
expect(provider.port).to eq "8080"
expect(provider.scheme).to eq "http"
+ expect(provider.scope).to eq "foo bar"
expect(provider.client_id).to eq "http://localhost:3000"
expect(provider.client_secret).to eq "IVl6GxxujAQ3mt6thAXKxyYYvmyRr8jw"
expect(provider.issuer).to eq "http://localhost:8080/realms/test"
diff --git a/modules/openid_connect/spec/services/openid_connect/configuration_mapper_spec.rb b/modules/openid_connect/spec/services/openid_connect/configuration_mapper_spec.rb
index e5dd3128f681..e8daef6db17e 100644
--- a/modules/openid_connect/spec/services/openid_connect/configuration_mapper_spec.rb
+++ b/modules/openid_connect/spec/services/openid_connect/configuration_mapper_spec.rb
@@ -98,6 +98,28 @@
end
end
+ describe "scope" do
+ subject { result }
+
+ context "when provided" do
+ let(:configuration) { { scope: "custom" } }
+
+ it { is_expected.to include("scope" => "custom") }
+ end
+
+ context "when provided as array" do
+ let(:configuration) { { scope: ["foo", "bar"] } }
+
+ it { is_expected.to include("scope" => "foo bar") }
+ end
+
+ context "when not provided" do
+ let(:configuration) { { foo: "bar" } }
+
+ it { is_expected.not_to have_key("scope") }
+ end
+ end
+
describe "issuer" do
subject { result }
diff --git a/modules/recaptcha/config/locales/crowdin/lv.yml b/modules/recaptcha/config/locales/crowdin/lv.yml
index 5cb59a5403c9..5389d78752ec 100644
--- a/modules/recaptcha/config/locales/crowdin/lv.yml
+++ b/modules/recaptcha/config/locales/crowdin/lv.yml
@@ -14,7 +14,7 @@ lv:
response_limit_text: 'The maximum number of characters to treat the HCaptcha response as valid.'
website_key_text: 'Enter the website key you created on the reCAPTCHA admin console for this domain.'
secret_key: 'Slepenā atslēga'
- secret_key_text: 'Enter the secret key you created on the reCAPTCHA admin console.'
+ secret_key_text: 'Ievadiet slepeno atslēgu, ko izveidojāt reCAPTCHA admin konsole.'
type: 'Use reCAPTCHA'
type_disabled: 'Disable reCAPTCHA'
type_v2: 'reCAPTCHA v2'
diff --git a/modules/recaptcha/config/locales/crowdin/nl.yml b/modules/recaptcha/config/locales/crowdin/nl.yml
index 6f6a09791fac..863c3886c5f8 100644
--- a/modules/recaptcha/config/locales/crowdin/nl.yml
+++ b/modules/recaptcha/config/locales/crowdin/nl.yml
@@ -9,7 +9,7 @@ nl:
verify_account: "Verifieer je account"
error_captcha: "Uw account kon niet worden geverifieerd. Neem contact op met een beheerder."
settings:
- website_key: 'Website key (May also be called "Site key")'
+ website_key: 'Websleutel (Kan ook "Site key" worden genoemd)'
response_limit: 'Antwoordlimiet voor HCaptcha'
response_limit_text: 'Het maximum aantal tekens om het HCaptcha antwoord als geldig te behandelen.'
website_key_text: 'Vul de website sleutel in die je hebt gemaakt op de reCAPTCHA admin console voor dit domein.'
@@ -22,4 +22,4 @@ nl:
type_hcaptcha: 'HCaptcha'
type_turnstile: 'Cloudflare Turnstile™'
captcha_description_html: >
- reCAPTCHA is a free service by Google that can be enabled for your OpenProject instance. If enabled, a captcha form will be rendered upon login for all users that have not verified a captcha yet.
Please see the following link for more details on reCAPTCHA and their versions, and how to create the website and secret keys: %{recaptcha_link}
HCaptcha is a Google-free alternative that you can use if you do not want to use reCAPTCHA. See this link for more information: %{hcaptcha_link}
Cloudflare Turnstile™ is another alternative that is more convenient for users while still providing the same level of security. See this link for more information: %{turnstile_link}
+ reCAPTCHA is een gratis service van Google die ingeschakeld kan worden voor uw OpenProject-instantie. Indien ingeschakeld, wordt er bij het inloggen een captcha-formulier weergegeven voor alle gebruikers die nog geen captcha hebben geverifieerd.
Zie de volgende link voor meer details over reCAPTCHA en hun versies, en hoe u de website en geheime sleutels aanmaakt: %{recaptcha_link}
HCaptcha is een Google-vrij alternatief dat u kunt gebruiken als u reCAPTCHA niet wilt gebruiken. Zie deze link voor meer informatie: %{hcaptcha_link}
Cloudflare Turnstile™ is een ander alternatief dat handiger is voor gebruikers en toch hetzelfde beveiligingsniveau biedt. Zie deze koppeling voor meer informatie: %{turnstile_link}
diff --git a/modules/reporting/app/models/cost_query/filter/user_id.rb b/modules/reporting/app/models/cost_query/filter/user_id.rb
index c6d8f0217505..ade496258899 100644
--- a/modules/reporting/app/models/cost_query/filter/user_id.rb
+++ b/modules/reporting/app/models/cost_query/filter/user_id.rb
@@ -52,11 +52,10 @@ def replace_me_value(value)
def self.available_values(*)
# All users which are members in projects the user can see.
# Excludes the anonymous user
- users = User.joins(members: :project)
- .merge(Project.visible)
+ users = User.in_visible_project
.human
+ .ordered_by_name
.select(User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s) << :id)
- .distinct
values = users.map { |u| [u.name, u.id] }
values.unshift [::I18n.t(:label_me), me_value] if User.current.logged?
diff --git a/modules/reporting/config/locales/crowdin/lv.yml b/modules/reporting/config/locales/crowdin/lv.yml
index c87b112a2785..3d3b7742aad0 100644
--- a/modules/reporting/config/locales/crowdin/lv.yml
+++ b/modules/reporting/config/locales/crowdin/lv.yml
@@ -83,12 +83,12 @@ lv:
text_costs_are_rounded_note: "Displayed values are rounded. All calculations are based on the non-rounded values."
toggle_multiselect: "activate/deactivate multiselect"
units: "Vienības"
- validation_failure_date: "is not a valid date"
- validation_failure_integer: "is not a valid integer"
+ validation_failure_date: "nav derīgs datums"
+ validation_failure_integer: "nav derīgs vesels skaitlis"
export:
cost_reports:
title: "Your Cost Reports XLS export"
reporting:
group_by:
- selected_columns: "Selected columns"
+ selected_columns: "Izvēlētās kolonnas"
selected_rows: "Selected rows"
diff --git a/modules/reporting/config/locales/crowdin/nl.yml b/modules/reporting/config/locales/crowdin/nl.yml
index 6caa6021b612..13c1214251bd 100644
--- a/modules/reporting/config/locales/crowdin/nl.yml
+++ b/modules/reporting/config/locales/crowdin/nl.yml
@@ -23,7 +23,7 @@ nl:
plugin_openproject_reporting:
name: "OpenProject Rapportage"
description: "This plugin allows creating custom cost reports with filtering and grouping created by the OpenProject Time and costs plugin."
- button_save_report_as: "Save report as..."
+ button_save_report_as: "Rapport opslaan als..."
comments: "Commentaar"
cost_reports_title: "Tijd en kosten"
label_cost_report: "Kostenrapport"
diff --git a/modules/storages/app/common/storages/peripherals/nextcloud_connection_validator.rb b/modules/storages/app/common/storages/peripherals/nextcloud_connection_validator.rb
index 371d24b18ccf..399f871d21a3 100644
--- a/modules/storages/app/common/storages/peripherals/nextcloud_connection_validator.rb
+++ b/modules/storages/app/common/storages/peripherals/nextcloud_connection_validator.rb
@@ -31,6 +31,7 @@
module Storages
module Peripherals
class NextcloudConnectionValidator
+ include TaggedLogging
include Dry::Monads[:maybe]
using ServiceResultRefinements
@@ -40,13 +41,15 @@ def initialize(storage:)
end
def validate
- maybe_is_not_configured
- .or { has_base_configuration_error? }
- .or { has_ampf_configuration_error? }
- .value_or(ConnectionValidation.new(type: :healthy,
- error_code: :none,
- timestamp: Time.current,
- description: nil))
+ with_tagged_logger do
+ maybe_is_not_configured
+ .or { has_base_configuration_error? }
+ .or { has_ampf_configuration_error? }
+ .value_or(ConnectionValidation.new(type: :healthy,
+ error_code: :none,
+ timestamp: Time.current,
+ description: nil))
+ end
end
private
@@ -194,6 +197,11 @@ def with_unexpected_content
unexpected_files = files.result.files.reject { |file| expected_folder_ids.include?(file.id) }
return None() if unexpected_files.empty?
+ file_representation = unexpected_files.map do |file|
+ "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}"
+ end
+ warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}"
+
Some(
ConnectionValidation.new(
type: :warning,
@@ -209,13 +217,11 @@ def with_unexpected_content
def capabilities_request_failed_with_unknown_error
return None() if capabilities.success?
- Rails.logger.error(
- "Connection validation failed with unknown error:\n\t" \
- "storage: ##{@storage.id} #{@storage.name}\n\t" \
- "request: Nextcloud capabilities\n\t" \
- "status: #{capabilities.result}\n\t" \
- "response: #{capabilities.error_payload}"
- )
+ error "Connection validation failed with unknown error:\n\t" \
+ "storage: ##{@storage.id} #{@storage.name}\n\t" \
+ "request: Nextcloud capabilities\n\t" \
+ "status: #{capabilities.result}\n\t" \
+ "response: #{capabilities.error_payload}"
Some(ConnectionValidation.new(type: :error,
error_code: :err_unknown,
@@ -226,13 +232,11 @@ def capabilities_request_failed_with_unknown_error
def files_request_failed_with_unknown_error
return None() if files.success?
- Rails.logger.error(
- "Connection validation failed with unknown error:\n\t" \
- "storage: ##{@storage.id} #{@storage.name}\n\t" \
- "request: Group folder content\n\t" \
- "status: #{files.result}\n\t" \
- "response: #{files.error_payload}"
- )
+ error "Connection validation failed with unknown error:\n\t" \
+ "storage: ##{@storage.id} #{@storage.name}\n\t" \
+ "request: Group folder content\n\t" \
+ "status: #{files.result}\n\t" \
+ "response: #{files.error_payload}"
Some(ConnectionValidation.new(type: :error,
error_code: :err_unknown,
diff --git a/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb b/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb
index 911f8280f51c..5c62084e37d7 100644
--- a/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb
+++ b/modules/storages/app/common/storages/peripherals/one_drive_connection_validator.rb
@@ -31,6 +31,7 @@
module Storages
module Peripherals
class OneDriveConnectionValidator
+ include TaggedLogging
include Dry::Monads[:maybe]
using ServiceResultRefinements
@@ -40,17 +41,19 @@ def initialize(storage:)
end
def validate
- maybe_is_not_configured
- .or { tenant_id_wrong }
- .or { client_id_wrong }
- .or { client_secret_wrong }
- .or { drive_id_wrong }
- .or { request_failed_with_unknown_error }
- .or { drive_with_unexpected_content }
- .value_or(ConnectionValidation.new(type: :healthy,
- error_code: :none,
- timestamp: Time.current,
- description: nil))
+ with_tagged_logger do
+ maybe_is_not_configured
+ .or { tenant_id_wrong }
+ .or { client_id_wrong }
+ .or { client_secret_wrong }
+ .or { drive_id_wrong }
+ .or { request_failed_with_unknown_error }
+ .or { drive_with_unexpected_content }
+ .value_or(ConnectionValidation.new(type: :healthy,
+ error_code: :none,
+ timestamp: Time.current,
+ description: nil))
+ end
end
private
@@ -146,6 +149,11 @@ def drive_with_unexpected_content
unexpected_files = query.result.files.reject { |file| expected_folder_ids.include?(file.id) }
return None() if unexpected_files.empty?
+ file_representation = unexpected_files.map do |file|
+ "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}"
+ end
+ warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}"
+
Some(ConnectionValidation.new(type: :warning,
error_code: :wrn_unexpected_content,
timestamp: Time.current,
@@ -157,10 +165,10 @@ def drive_with_unexpected_content
def request_failed_with_unknown_error
return None() if query.success?
- Rails.logger.error("Connection validation failed with unknown error:\n\t" \
- "storage: ##{@storage.id} #{@storage.name}\n\t" \
- "status: #{query.result}\n\t" \
- "response: #{query.error_payload}")
+ error "Connection validation failed with unknown error:\n\t" \
+ "storage: ##{@storage.id} #{@storage.name}\n\t" \
+ "status: #{query.result}\n\t" \
+ "response: #{query.error_payload}"
Some(ConnectionValidation.new(type: :error,
error_code: :err_unknown,
diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb
index c69ef2e6d210..7b673626cf10 100644
--- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb
+++ b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb
@@ -137,10 +137,14 @@ def handle_response(response)
# rubocop:enable Metrics/AbcSize
def get_folder_id(auth_strategy, destination_path)
+ # file_path_to_id_map query returns keys without trailing slashes
+ # TODO: Harden this with https://community.openproject.org/wp/57850
+ sanitized_path = destination_path.chomp("/")
+
Registry
.resolve("nextcloud.queries.file_path_to_id_map")
- .call(storage: @storage, auth_strategy:, folder: ParentFolder.new(destination_path), depth: 0)
- .map { |result| @data.with(id: result[destination_path].id) }
+ .call(storage: @storage, auth_strategy:, folder: ParentFolder.new(sanitized_path), depth: 0)
+ .map { |result| @data.with(id: result[sanitized_path].id) }
end
def source = self.class
diff --git a/modules/storages/app/common/storages/tagged_logging.rb b/modules/storages/app/common/storages/tagged_logging.rb
index 28d18c4884fd..2a5e353435e9 100644
--- a/modules/storages/app/common/storages/tagged_logging.rb
+++ b/modules/storages/app/common/storages/tagged_logging.rb
@@ -30,7 +30,7 @@
module Storages
module TaggedLogging
- delegate :info, :error, to: :logger
+ delegate :info, :warn, :error, to: :logger
# @param tag [Class, String, Array] the tag or list of tags to annotate the logs with
# @yield [Logger]
diff --git a/modules/storages/app/models/storages/file_link.rb b/modules/storages/app/models/storages/file_link.rb
index 11daa248a134..b98eff8e2d3e 100644
--- a/modules/storages/app/models/storages/file_link.rb
+++ b/modules/storages/app/models/storages/file_link.rb
@@ -37,11 +37,7 @@ class Storages::FileLink < ApplicationRecord
validates :container_type, inclusion: { in: ["WorkPackage", nil] }
validates :origin_id, presence: true
- attr_writer :origin_status
-
- def origin_status
- @origin_status || nil
- end
+ attribute :origin_status
delegate :project, to: :container
diff --git a/modules/storages/config/locales/crowdin/ko.yml b/modules/storages/config/locales/crowdin/ko.yml
index 05fbc46a7436..0f905157a220 100644
--- a/modules/storages/config/locales/crowdin/ko.yml
+++ b/modules/storages/config/locales/crowdin/ko.yml
@@ -115,7 +115,7 @@ ko:
permission_not_set: '- %{path}에 대한 권한을 설정할 수 없습니다.'
remote_folders:
not_allowed: '%{username}에게는 %{group_folder} 폴더에 대한 액세스 권한이 없습니다. Nextcloud의 폴더 권한을 확인하세요.'
- not_found: "%{group_folder} folder wasn't found. Please check your Nextcloud setup."
+ not_found: "%{group_folder} 폴더를 찾을 수 없습니다. Nextcloud 설정을 확인하세요."
remove_user_from_group:
conflict: '다음과 같은 이유로 %{user} 사용자를 %{group} 그룹에서 제거할 수 없습니다: %{reason}'
failed_to_remove: '다음과 같은 이유로 %{user} 사용자를 %{group} 그룹에서 제거할 수 없습니다: %{reason}'
diff --git a/modules/storages/lib/api/v3/file_links/create_endpoint.rb b/modules/storages/lib/api/v3/file_links/create_endpoint.rb
index 4d40fd76e8f2..af545e226cc8 100644
--- a/modules/storages/lib/api/v3/file_links/create_endpoint.rb
+++ b/modules/storages/lib/api/v3/file_links/create_endpoint.rb
@@ -28,59 +28,56 @@
# See COPYRIGHT and LICENSE files for more details.
#++
-# Handles /api/v3/work_packages/:work_package_id/file_links as defined
-# in modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb
-#
-# Multiple classes are involved during its lifecycle:
-# - Storages::Peripherals::ParseCreateParamsService
-# - API::V3::FileLinks::FileLinkCollectionRepresenter
-# - Storages::FileLinks::CreateService
-#
-# These classes are either deduced from the model class, or given as parameter
-# on class instantiation.
-class API::V3::FileLinks::CreateEndpoint < API::Utilities::Endpoints::Create
- include ::API::V3::Utilities::Endpoints::V3Deductions
- include ::API::V3::Utilities::Endpoints::V3PresentSingle
+module API
+ module V3
+ module FileLinks
+ class CreateEndpoint < API::Utilities::Endpoints::Create
+ include Utilities::Endpoints::V3Deductions
+ include Utilities::Endpoints::V3PresentSingle
- # As this endpoint receives a list of file links to create, it calls the
- # create service multiple times, one time for each file link to create. The
- # call is done by calling the `super` method. Results are aggregated in
- # global_result using the `add_dependent!` method.
- def process(request, params_elements)
- global_result = ServiceResult.success
+ # As this endpoint receives a list of file links to create, it calls the
+ # create service multiple times, one time for each file link to create. The
+ # call is done by calling the `super` method. Results are aggregated in
+ # global_result using the `add_dependent!` method.
+ def process(request, params_elements)
+ global_result = ServiceResult.success
- Storages::FileLink.transaction do
- params_elements.each do |params|
- # call the default API::Utilities::Endpoints::Create#process
- # implementation for each of the params_element array
- one_result = super(request, params)
- # merge service result in one
- global_result.add_dependent!(one_result)
- end
+ ::Storages::FileLink.transaction do
+ params_elements.each do |params|
+ # call the default API::Utilities::Endpoints::Create#process
+ # implementation for each of the params_element array
+ one_result = super(request, params)
+ # merge service result in one
+ global_result.add_dependent!(one_result)
+ end
- # rollback records created if an error occurred (validation failed)
- raise ActiveRecord::Rollback if global_result.failure?
- end
+ # rollback records created if an error occurred (validation failed)
+ raise ActiveRecord::Rollback if global_result.failure?
+ end
- global_result
- end
+ global_result
+ end
- def present_success(request, service_call)
- file_links = service_call.all_results.map do |file_link|
- file_link.origin_status = :view_allowed
- file_link
- end
+ def present_success(request, service_call)
+ id_status_map = {}
- render_representer.create(
- file_links,
- self_link: self_link(request),
- current_user: request.current_user
- )
- end
+ service_call.all_results.each do |file_link|
+ id_status_map[file_link.id] = "view_allowed"
+ end
+
+ render_representer.create(
+ JoinOriginStatusToFileLinksRelation.create(id_status_map),
+ self_link: self_link(request),
+ current_user: request.current_user
+ )
+ end
- private
+ private
- def self_link(_request)
- "#{::API::V3::URN_PREFIX}file_links:no_link_provided"
+ def self_link(_request)
+ "#{URN_PREFIX}file_links:no_link_provided"
+ end
+ end
+ end
end
end
diff --git a/modules/storages/lib/api/v3/file_links/file_link_collection_representer.rb b/modules/storages/lib/api/v3/file_links/file_link_collection_representer.rb
index 3ca1d17aaa56..7ecfca7c285b 100644
--- a/modules/storages/lib/api/v3/file_links/file_link_collection_representer.rb
+++ b/modules/storages/lib/api/v3/file_links/file_link_collection_representer.rb
@@ -29,7 +29,8 @@
module API
module V3
module FileLinks
- class FileLinkCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
+ class FileLinkCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
+ property :count, getter: ->(*) { count(:id) }
end
end
end
diff --git a/modules/storages/lib/api/v3/file_links/file_link_representer.rb b/modules/storages/lib/api/v3/file_links/file_link_representer.rb
index 28269707da65..5d792d40334b 100644
--- a/modules/storages/lib/api/v3/file_links/file_link_representer.rb
+++ b/modules/storages/lib/api/v3/file_links/file_link_representer.rb
@@ -91,7 +91,7 @@ class FileLinkRepresenter < ::API::Decorators::Single
link :status, uncacheable: true do
next if represented.origin_status.nil?
- PERMISSION_LINKS[represented.origin_status]
+ PERMISSION_LINKS[represented.origin_status.to_sym]
end
link :staticOriginOpen do
diff --git a/modules/storages/lib/api/v3/file_links/join_origin_status_to_file_links_relation.rb b/modules/storages/lib/api/v3/file_links/join_origin_status_to_file_links_relation.rb
new file mode 100644
index 000000000000..926c9fa97467
--- /dev/null
+++ b/modules/storages/lib/api/v3/file_links/join_origin_status_to_file_links_relation.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module API
+ module V3
+ module FileLinks
+ class JoinOriginStatusToFileLinksRelation
+ # @param [Hash] id_status_map A hash mapping file link IDs to their origin status
+ # in the format { 137: "view_allowed", 142: "error" }
+ def self.create(id_status_map)
+ sanitized_sql = ActiveRecord::Base.sanitize_sql_array(
+ [origin_status_join(id_status_map.size), *id_status_map.flatten]
+ )
+
+ ::Storages::FileLink.where(id: id_status_map.keys)
+ .order(:id)
+ .joins(sanitized_sql)
+ .select("file_links.*, origin_status.status AS origin_status")
+ end
+
+ def self.origin_status_join(value_count)
+ placeholders = Array.new(value_count).map { "(?,?)" }.join(",")
+
+ <<-SQL.squish
+ LEFT JOIN (VALUES #{placeholders}) AS origin_status (id,status) ON origin_status.id = file_links.id
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb
index 488ab74bc69a..efdae12785f6 100644
--- a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb
+++ b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb
@@ -28,50 +28,78 @@
# See COPYRIGHT and LICENSE files for more details.
#++
-class API::V3::FileLinks::WorkPackagesFileLinksAPI < API::OpenProjectAPI
- # The `:resources` keyword defines the API namespace -> /api/v3/work_packages/:id/file_links/...
- resources :file_links do
- get do
- query = ParamsToQueryService
- .new(::Storages::Storage,
- current_user,
- query_class: ::Queries::Storages::FileLinks::FileLinkQuery)
- .call(params)
+module API
+ module V3
+ module FileLinks
+ class WorkPackagesFileLinksAPI < API::OpenProjectAPI
+ helpers do
+ def sync_and_convert_relation(file_links)
+ return ::Storages::FileLink.none if file_links.empty?
- unless query.valid?
- message = I18n.t("api_v3.errors.missing_or_malformed_parameter", parameter: "filters")
- raise ::API::Errors::InvalidQuery.new(message)
- end
+ sync_result = ::Storages::FileLinkSyncService
+ .new(user: current_user)
+ .call(file_links)
+ .result
- result = if current_user.allowed_in_project?(:view_file_links, @work_package.project)
- file_links = query.results.where(container_id: @work_package.id,
- container_type: "WorkPackage",
- storage: @work_package.project.storages)
- ::Storages::FileLinkSyncService
- .new(user: current_user)
- .call(file_links)
- .result
- else
- []
- end
- ::API::V3::FileLinks::FileLinkCollectionRepresenter.new(
- result,
- self_link: api_v3_paths.file_links(@work_package.id),
- current_user:
- )
- end
+ id_status_map = {}
+
+ sync_result.each do |file_link|
+ id_status_map[file_link.id] = file_link.origin_status.to_s
+ end
+
+ JoinOriginStatusToFileLinksRelation.create(id_status_map)
+ end
+ end
+
+ resources :file_links do
+ get do
+ query = ParamsToQueryService
+ .new(::Storages::Storage,
+ current_user,
+ query_class: ::Queries::Storages::FileLinks::FileLinkQuery)
+ .call(params)
+
+ unless query.valid?
+ message = I18n.t("api_v3.errors.missing_or_malformed_parameter", parameter: "filters")
+ raise ::API::Errors::InvalidQuery.new(message)
+ end
- post &::API::V3::FileLinks::WorkPackagesFileLinksCreateEndpoint
- .new(
- model: ::Storages::FileLink,
- parse_service: Storages::Peripherals::ParseCreateParamsService,
- render_representer: ::API::V3::FileLinks::FileLinkCollectionRepresenter,
- params_modifier: ->(params) do
- params[:container_id] = work_package.id
- params[:container_type] = work_package.class.name
- params
- end
+ relation = if current_user.allowed_in_project?(:view_file_links, @work_package.project)
+ file_links = query.results.where(container_id: @work_package.id,
+ container_type: "WorkPackage",
+ storage: @work_package.project.storages)
+
+ if params[:pageSize] == "0"
+ file_links
+ else
+ sync_and_convert_relation(file_links)
+ end
+ else
+ ::Storages::FileLink.none
+ end
+
+ FileLinkCollectionRepresenter.new(
+ relation,
+ per_page: params[:pageSize],
+ self_link: api_v3_paths.file_links(@work_package.id),
+ current_user:
)
- .mount
+ end
+
+ post &WorkPackagesFileLinksCreateEndpoint
+ .new(
+ model: ::Storages::FileLink,
+ parse_service: ::Storages::Peripherals::ParseCreateParamsService,
+ render_representer: FileLinkCollectionRepresenter,
+ params_modifier: ->(params) do
+ params[:container_id] = work_package.id
+ params[:container_type] = work_package.class.name
+ params
+ end
+ )
+ .mount
+ end
+ end
+ end
end
end
diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb
index 993d769f68d6..498de7cca273 100644
--- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb
+++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb
@@ -72,16 +72,10 @@
before do
create_group(auth, storage, group)
add_user_to_group(user, group)
-
- # There is a bug in the group folder API that does not allow to remove a user from a group,
- # if this is its last group
- create_group(auth, storage, "Sith Assassins Backup")
- add_user_to_group(user, "Sith Assassins Backup")
end
after do
remove_group(auth, storage, group)
- remove_group(auth, storage, "Sith Assassins Backup")
end
it "returns a success" do
diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb
index f3de4f0b1e63..057233a04bb4 100644
--- a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb
+++ b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb
@@ -146,7 +146,7 @@ def disable_module(project, modul)
) do
expect(Storages::FileLink.count).to eq 2
Storages::FileLink.find_each.with_index do |file_link, i|
- unset_keys = %w[container_id container_type]
+ unset_keys = %w[container_id container_type origin_status]
set_keys = (file_link.attributes.keys - unset_keys)
set_keys.each do |key|
expect(file_link.attributes[key]).not_to(
@@ -159,9 +159,8 @@ def disable_module(project, modul)
end
end
- expect(response.body).to be_json_eql(
- "urn:openproject-org:api:v3:file_links:no_link_provided".to_json
- ).at_path("_links/self/href")
+ self_href = "urn:openproject-org:api:v3:file_links:no_link_provided?offset=1&pageSize=30".to_json
+ expect(response.body).to be_json_eql(self_href).at_path("_links/self/href")
end
end
end
@@ -313,14 +312,20 @@ def disable_module(project, modul)
) do
expect(Storages::FileLink.count).to eq 2
Storages::FileLink.find_each.with_index do |file_link, i|
- file_link.attributes.each do |(key, value)|
- # check nil values to ensure the :file_link_element factory is accurate
- expect(value).not_to be_nil,
- "expected attribute #{key.inspect} of FileLink ##{i + 1} to be set.\ngot nil."
+ unset_keys = %w[origin_status]
+ set_keys = (file_link.attributes.keys - unset_keys)
+ set_keys.each do |key|
+ expect(file_link.attributes[key]).not_to(
+ be_nil,
+ "expected attribute #{key.inspect} of FileLink ##{i + 1} to be set.\ngot nil."
+ )
+ end
+ unset_keys.each do |key|
+ expect(file_link.attributes[key]).to be_nil
end
end
- expect(response.body).to be_json_eql(path.to_json).at_path("_links/self/href")
+ expect(response.body).to be_json_eql("#{path}?offset=1&pageSize=30".to_json).at_path("_links/self/href")
end
end
@@ -391,23 +396,18 @@ def disable_module(project, modul)
]
end
- it_behaves_like "API V3 collection response", 3, 3, "FileLink" do
- let(:elements) { Storages::FileLink.order(id: :asc) }
+ it_behaves_like "API V3 collection response", 1, 1, "FileLink" do
+ let(:elements) { [Storages::FileLink.first] }
let(:expected_status_code) { 201 }
end
- it(
- "creates only one FileLink for all duplicates and " \
- "uses metadata from the first item and " \
- "replies with as many embedded elements as in the request, all identical"
- ) do
+ it "creates only one FileLink for all duplicates and uses metadata from the first item" do
expect(Storages::FileLink.count).to eq 1
expect(Storages::FileLink.first.origin_name).to eq "first name"
replied_elements = JSON.parse(last_response.body).dig("_embedded", "elements")
- expect(replied_elements.count).to eq(embedded_elements.count)
- expect(replied_elements[1..]).to all(eq(replied_elements.first))
+ expect(replied_elements.count).to eq(1)
end
end
diff --git a/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb b/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb
index 4e3fddf3965d..a02c51569ff7 100644
--- a/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb
+++ b/modules/storages/spec/requests/api/v3/file_links/mixed_case_file_links_integration_spec.rb
@@ -171,13 +171,14 @@
# total, count, element_type, collection_type = 'Collection'
it_behaves_like "API V3 collection response", 6, 6, "FileLink", "Collection" do
let(:elements) do
+ # ordered by id
[
- file_link_timeout_happy,
- file_link_error_happy,
- file_link_unauth_happy,
- file_link_deleted,
+ file_link_happy,
file_link_other_user,
- file_link_happy
+ file_link_deleted,
+ file_link_unauth_happy,
+ file_link_error_happy,
+ file_link_timeout_happy
]
end
end
diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/remove_user_from_group_success.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/remove_user_from_group_success.yml
index 3622540470ff..9f567bfdc12b 100644
--- a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/remove_user_from_group_success.yml
+++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/remove_user_from_group_success.yml
@@ -12,7 +12,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -35,30 +35,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:34 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:01 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=9762849c349fec419d4d915242d15e12; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=DZ6L8GHmQgY2z%2B3foy2pUqmyeMgyiphLjNdRgVJ730AI27UtJHnhpTapN6kiLCTtskMoKUaXrPc15eL3LtZwYfw4Eve42BLZMzJG8s2unmeF%2B4cqZz%2FvWFGsYrDU4gj3;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9762849c349fec419d4d915242d15e12;
+ - oc07ul6b4oaw=21c9dc077368b09dd0ccef059539746a; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=9emV%2BdNp8kCMiTlMOsInpolUEx1yOWHm5HiBNoUBCxec5k%2BktQuDYj4aO38QEtHfO3zTck5ACGzgZ3PV4xJZom77Ac6qy9%2BABSxeAxMqtE4ta4dpp2jBrk3r%2Fzhwb0mE;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=21c9dc077368b09dd0ccef059539746a;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=9762849c349fec419d4d915242d15e12;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9762849c349fec419d4d915242d15e12;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=1e76183d834700018922edb625e879e2;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:35 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=21c9dc077368b09dd0ccef059539746a;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=21c9dc077368b09dd0ccef059539746a;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=311d0429cc20407278845a667f6d80ad;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:01 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -66,9 +62,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - TBw85AYaMZcZNiY5oEru
+ - q3Y1SCVKkQfEiwzGjVCW
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -89,7 +85,7 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:35 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:01 GMT
- request:
method: post
uri: https://nextcloud.local/ocs/v1.php/cloud/users/m.jade@death.star/groups
@@ -102,7 +98,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -125,30 +121,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:35 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:01 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=6cc4b771ce27bb97b8f3238e7653da31; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=B2I0t1BGIs1gvjZeGy7jqq8c%2FIxKtkypUxnIhP5m2lJhvyGp5k8CDf%2F1Cqn4YtKlIkIH3nT%2Fm5WD3ShkVPf5pnCupGYQqyWEp3GCkGuQXt5i6QhhP3XNfD%2FU4VtGxUgf;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=6cc4b771ce27bb97b8f3238e7653da31;
+ - oc07ul6b4oaw=3a9639c066a5fa86a27273fc0dc3581a; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=P2uApN%2FWNxMYjUzW%2FkwSEqI7ux9QUNzTnHWuHmzBE%2B2PZrv2PQPK4gVsz6jc7Y8FvfZOfVv%2B%2FX%2FOX5PyVW%2BZ9JpNIFxVFRibrRzGnIMvKGAJ%2BASlkF0MWBgrY3M0qeEg;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=3a9639c066a5fa86a27273fc0dc3581a;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=6cc4b771ce27bb97b8f3238e7653da31;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=6cc4b771ce27bb97b8f3238e7653da31;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=de374ccc4540ad2b501264cf9d57d7b0;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:35 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=3a9639c066a5fa86a27273fc0dc3581a;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=3a9639c066a5fa86a27273fc0dc3581a;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9a493de5794ac6c0ab0ec728d82afc07;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:02 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -156,9 +148,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - 2OHmy14Z2uyX80wetXYp
+ - PyoqZuyOeHHxNYg0IqiT
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -179,187 +171,7 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:36 GMT
-- request:
- method: post
- uri: https://nextcloud.local/ocs/v1.php/cloud/groups
- body:
- encoding: ASCII-8BIT
- string: groupid=Sith+Assassins+Backup
- headers:
- Authorization:
- - Basic
- Ocs-Apirequest:
- - 'true'
- User-Agent:
- - httpx.rb/1.3.1
- Accept:
- - "*/*"
- Accept-Encoding:
- - gzip, deflate
- Content-Type:
- - application/x-www-form-urlencoded
- Content-Length:
- - '29'
- response:
- status:
- code: 200
- message: OK
- headers:
- Cache-Control:
- - no-cache, no-store, must-revalidate
- Content-Encoding:
- - gzip
- Content-Security-Policy:
- - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'
- Content-Type:
- - application/xml; charset=utf-8
- Date:
- - Wed, 02 Oct 2024 08:50:36 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
- Feature-Policy:
- - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
- 'none';payment 'none'
- Pragma:
- - no-cache
- Referrer-Policy:
- - no-referrer
- Server:
- - Apache/2.4.59 (Debian)
- Set-Cookie:
- - oc07ul6b4oaw=a59820aa38fc321b9f172ac4176c1d55; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=tOAFguNR0Jp%2FxNtxIOqXQ%2F6DNz%2FxwIkdRbte7S0BC8lnDIib3RD3DxuoFtc0FxIsQEim0fze6h0sofcgU%2BhLps1nghETPItFWjqn75oa1BdUTk9lacQyzV3IlPGH6fEG;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=a59820aa38fc321b9f172ac4176c1d55;
- path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
- path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
- __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=a59820aa38fc321b9f172ac4176c1d55;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=a59820aa38fc321b9f172ac4176c1d55;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=b8539104cd9a4e13ad6a8bad9d1ac0e5;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:36 GMT; Max-Age=3600
- X-Content-Type-Options:
- - nosniff
- X-Frame-Options:
- - SAMEORIGIN
- X-Permitted-Cross-Domain-Policies:
- - none
- X-Powered-By:
- - PHP/8.2.21
- X-Request-Id:
- - Rhr2ktjrVgX7DNPKUMQ0
- X-Robots-Tag:
- - noindex, nofollow
- X-Xss-Protection:
- - 1; mode=block
- Content-Length:
- - '140'
- body:
- encoding: UTF-8
- string: |
-
-
-
- ok
- 100
- OK
-
-
-
-
-
- recorded_at: Wed, 02 Oct 2024 08:50:36 GMT
-- request:
- method: post
- uri: https://nextcloud.local/ocs/v1.php/cloud/users/m.jade@death.star/groups
- body:
- encoding: ASCII-8BIT
- string: groupid=Sith+Assassins+Backup
- headers:
- Authorization:
- - Basic
- Ocs-Apirequest:
- - 'true'
- User-Agent:
- - httpx.rb/1.3.1
- Accept:
- - "*/*"
- Accept-Encoding:
- - gzip, deflate
- Content-Type:
- - application/x-www-form-urlencoded
- Content-Length:
- - '29'
- response:
- status:
- code: 200
- message: OK
- headers:
- Cache-Control:
- - no-cache, no-store, must-revalidate
- Content-Encoding:
- - gzip
- Content-Security-Policy:
- - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'
- Content-Type:
- - application/xml; charset=utf-8
- Date:
- - Wed, 02 Oct 2024 08:50:36 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
- Feature-Policy:
- - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
- 'none';payment 'none'
- Pragma:
- - no-cache
- Referrer-Policy:
- - no-referrer
- Server:
- - Apache/2.4.59 (Debian)
- Set-Cookie:
- - oc07ul6b4oaw=c1b3c3f4214d9d8966f07f16711725ed; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=Y9itI1jA1U7ht32uGGWcKURt1KGYrNnp2bG2uJPZ3y9Te7JXG4vcw5ZBf6ZrZTLKv9dLwEnmABA0OlKdh10jEyMxWVJXYRgXrsdLiDlaTAFHC9%2FMz4bQ7Cy%2BoQ71jekj;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=c1b3c3f4214d9d8966f07f16711725ed;
- path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
- path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
- __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=c1b3c3f4214d9d8966f07f16711725ed;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=c1b3c3f4214d9d8966f07f16711725ed;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=fb555d0ca125d098bfe72a357f752fb0;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:37 GMT; Max-Age=3600
- X-Content-Type-Options:
- - nosniff
- X-Frame-Options:
- - SAMEORIGIN
- X-Permitted-Cross-Domain-Policies:
- - none
- X-Powered-By:
- - PHP/8.2.21
- X-Request-Id:
- - 0QVHV77irKMx1GSbQPVW
- X-Robots-Tag:
- - noindex, nofollow
- X-Xss-Protection:
- - 1; mode=block
- Content-Length:
- - '140'
- body:
- encoding: UTF-8
- string: |
-
-
-
- ok
- 100
- OK
-
-
-
-
-
- recorded_at: Wed, 02 Oct 2024 08:50:37 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:02 GMT
- request:
method: get
uri: https://nextcloud.local/ocs/v1.php/cloud/groups/Sith%20Assassins
@@ -372,7 +184,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -391,30 +203,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:37 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:02 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=01da2b49d5abec2a8999bd0070130b04; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=LNjDQi4jb3HBPgwLDak3PZLcefxXWs1BL1%2FeQ0HB3ocStx7NYlMWsR6IrIfg8NqysVn%2BaDcibaS%2Bz72Go2OZw3kWyTF7hoEwPejH2nL3u7Pa%2FG708ecz3HUI%2FztROvZc;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=01da2b49d5abec2a8999bd0070130b04;
+ - oc07ul6b4oaw=97730690484ff8d8bc16bf8fc836e192; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=3ydCf8F7NM%2F2VCcqBKJzCm01GTLiQCKt%2Fz91Z8O2gMhhGnKbHF7oCWjtdsRj3witWmPLU8mtODjXwpHzYQwxfTBmfYOuoCpQiLTGBJikmgriSmIoHBvA%2BE3DqeHYxRE%2F;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=97730690484ff8d8bc16bf8fc836e192;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=01da2b49d5abec2a8999bd0070130b04;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=01da2b49d5abec2a8999bd0070130b04;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=615d677637cb137e351629b49d573d0c;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:37 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=97730690484ff8d8bc16bf8fc836e192;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=97730690484ff8d8bc16bf8fc836e192;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=dd4c6a90f94ad0eb4f374f306a9664d1;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:02 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -422,9 +230,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - dGJ5c48eHtcbGlNhi6fw
+ - bNfP3fO1q5cVdXC3LA6B
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -449,7 +257,7 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:37 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:02 GMT
- request:
method: delete
uri: https://nextcloud.local/ocs/v1.php/cloud/users/m.jade@death.star/groups?groupid=Sith%20Assassins
@@ -462,7 +270,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -481,30 +289,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:37 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:02 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=85dfa52e03afb69fbe548d3053d08e15; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=kOE0o2v1ep7XtIKKVnmqy8ck7Rbukw9NitYKaRPsLSS9b8mWIW%2Bwuw2dCZtEDmQdBZzItzOHZdRZim0v%2Bu06yNhj8xeY7dU0xqXzLKez9Zj%2BrA2KKmZl1jw6c9Ubx4PX;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=85dfa52e03afb69fbe548d3053d08e15;
+ - oc07ul6b4oaw=3c61a0db6149722cc6a95d3f42230f55; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=gw8mGeh8Fu0rKt%2BIzOD%2Brnwku6prDLtJC6DpOm5YTTp4Pbw7uETlDw3alKWSkn4%2BmMjjhOk5KkngEf527gY7b7ACVUFutHhvEdphAyYx5hSpQwulPU5R55wJDLQPCs52;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=3c61a0db6149722cc6a95d3f42230f55;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=85dfa52e03afb69fbe548d3053d08e15;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=85dfa52e03afb69fbe548d3053d08e15;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9f9f2207d2c10825f6ba2d6c07d2658b;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:38 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=3c61a0db6149722cc6a95d3f42230f55;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=3c61a0db6149722cc6a95d3f42230f55;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=64e2322d4338def4292ea6fe248ecfd6;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:03 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -512,9 +316,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - HL85t6eG92CsTVcZzlkl
+ - RfCII0YdCOpcI72nKP3u
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -535,7 +339,7 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:38 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:03 GMT
- request:
method: get
uri: https://nextcloud.local/ocs/v1.php/cloud/groups/Sith%20Assassins
@@ -548,7 +352,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -567,30 +371,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:38 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:03 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=4311d7d8a7e8b3ba35b5d1fcff7d1b7a; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=GAzp%2FwJNf3dp%2BnY3U%2B4F4SXwmgC7f3Jqms%2FbwvIn2L7ztvCw3fPyMF8CAwno8o3xJ7O7Pbeu7IDG0ElNxKwl%2FBKviLvxlW4sa5woQEos5FRHFpMy93eMkNOsJjfBziTe;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=4311d7d8a7e8b3ba35b5d1fcff7d1b7a;
+ - oc07ul6b4oaw=2bd34228d8722fb511ac830b7af8d150; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=qTfCH0VUr5Q4zNJInH3WreCzodbpF9Stp14DQEYJovF46QRnj9wSZEyMB6ztRQgTe2RAnjhM8kXzgacZlyDnOnqiweJNbxau%2B9qZ83SkqqGBMSbYULa1pVHaNZVZ3fnl;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=2bd34228d8722fb511ac830b7af8d150;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=4311d7d8a7e8b3ba35b5d1fcff7d1b7a;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=4311d7d8a7e8b3ba35b5d1fcff7d1b7a;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=f7715138d243dc8627e3b3647a74b419;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:38 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=2bd34228d8722fb511ac830b7af8d150;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=2bd34228d8722fb511ac830b7af8d150;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=df99da2ec87f19ccad9529158f642ec4;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:04 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -598,9 +398,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - DaZNZ94HD5CWPkX9ZTgQ
+ - rEa0uC587txch3ofMyqm
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -623,7 +423,7 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:38 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:04 GMT
- request:
method: delete
uri: https://nextcloud.local/ocs/v1.php/cloud/groups/Sith%20Assassins
@@ -636,93 +436,7 @@ http_interactions:
Ocs-Apirequest:
- 'true'
User-Agent:
- - httpx.rb/1.3.1
- Accept:
- - "*/*"
- Accept-Encoding:
- - gzip, deflate
- response:
- status:
- code: 200
- message: OK
- headers:
- Cache-Control:
- - no-cache, no-store, must-revalidate
- Content-Encoding:
- - gzip
- Content-Security-Policy:
- - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'
- Content-Type:
- - application/xml; charset=utf-8
- Date:
- - Wed, 02 Oct 2024 08:50:38 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
- Feature-Policy:
- - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
- 'none';payment 'none'
- Pragma:
- - no-cache
- Referrer-Policy:
- - no-referrer
- Server:
- - Apache/2.4.59 (Debian)
- Set-Cookie:
- - oc07ul6b4oaw=fd4ecbd33c8c4056f827d54926c7d7e1; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=dIlm6nPh4GMsA38cZj2SyDxlQhuyL1CP86iZATnMY6OqdehoRJUgPfH27xShrr1yg5Hd2tC7h79f9lzwPh8DMjnmy%2FzR5qpY%2BkPmvvFohO00pSSbkhgIRVuJBJygpAJm;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=fd4ecbd33c8c4056f827d54926c7d7e1;
- path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
- path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
- __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=fd4ecbd33c8c4056f827d54926c7d7e1;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=fd4ecbd33c8c4056f827d54926c7d7e1;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=9fb44996d3f7d257f11f151473b7b955;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:39 GMT; Max-Age=3600
- X-Content-Type-Options:
- - nosniff
- X-Frame-Options:
- - SAMEORIGIN
- X-Permitted-Cross-Domain-Policies:
- - none
- X-Powered-By:
- - PHP/8.2.21
- X-Request-Id:
- - 4EQLXfbKAVqZkKshqVYc
- X-Robots-Tag:
- - noindex, nofollow
- X-Xss-Protection:
- - 1; mode=block
- Content-Length:
- - '140'
- body:
- encoding: UTF-8
- string: |
-
-
-
- ok
- 100
- OK
-
-
-
-
-
- recorded_at: Wed, 02 Oct 2024 08:50:39 GMT
-- request:
- method: delete
- uri: https://nextcloud.local/ocs/v1.php/cloud/groups/Sith%20Assassins%20Backup
- body:
- encoding: US-ASCII
- string: ''
- headers:
- Authorization:
- - Basic
- Ocs-Apirequest:
- - 'true'
- User-Agent:
- - httpx.rb/1.3.1
+ - httpx.rb/1.3.3
Accept:
- "*/*"
Accept-Encoding:
@@ -741,30 +455,26 @@ http_interactions:
Content-Type:
- application/xml; charset=utf-8
Date:
- - Wed, 02 Oct 2024 08:50:39 GMT
- Expires:
- - Thu, 19 Nov 1981 08:52:00 GMT
+ - Thu, 28 Nov 2024 14:30:04 GMT
Feature-Policy:
- autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone
'none';payment 'none'
- Pragma:
- - no-cache
Referrer-Policy:
- no-referrer
Server:
- - Apache/2.4.59 (Debian)
+ - Apache/2.4.62 (Debian)
Set-Cookie:
- - oc07ul6b4oaw=38a807de0bfea4578a190796ea3679bf; path=/; secure; HttpOnly; SameSite=Lax,
- oc_sessionPassphrase=ykYwdbn02mf%2B2FIYpFHhFhySmy5y1j4D9kX1We59HLdc50Otjzyj03S%2Fg11ZjBeD13%2BShKDXhMDDDQBf7K%2F%2B7cZjzB12b1LGlwjEIwGnIhBykM0QeIB2htLjvWZ1DdHw;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=38a807de0bfea4578a190796ea3679bf;
+ - oc07ul6b4oaw=0067f072357c13bc15a002ee0f531c38; path=/; secure; HttpOnly; SameSite=Lax,
+ oc_sessionPassphrase=lF9u%2Bt37SHRU7rCTB0UOObOoFgF1nSi3576l1M%2BYHicFwsOWSy43n7IE8usNib8PnxalqewI6xAdAQR11GmbJASdh8ysogMD2l7JEJMEl%2Ff03xn9u%2BPKSe5uEWrT1tcV;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=0067f072357c13bc15a002ee0f531c38;
path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true;
path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax,
__Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri,
- 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=38a807de0bfea4578a190796ea3679bf;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=38a807de0bfea4578a190796ea3679bf;
- path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=899b9aecb6d1dc7c35ef9a847d0e7bfc;
- path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Wed, 02
- Oct 2024 09:50:39 GMT; Max-Age=3600
+ 31-Dec-2100 23:59:59 GMT; SameSite=strict, oc07ul6b4oaw=0067f072357c13bc15a002ee0f531c38;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=0067f072357c13bc15a002ee0f531c38;
+ path=/; secure; HttpOnly; SameSite=Lax, oc07ul6b4oaw=3f7baf5f27c60be23f6465d9ee4b55e9;
+ path=/; secure; HttpOnly; SameSite=Lax, cookie_test=test; expires=Thu, 28
+ Nov 2024 15:30:04 GMT; Max-Age=3600
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@@ -772,9 +482,9 @@ http_interactions:
X-Permitted-Cross-Domain-Policies:
- none
X-Powered-By:
- - PHP/8.2.21
+ - PHP/8.2.26
X-Request-Id:
- - 52hU39z1usAaL575cnKp
+ - 1M6gPBPREUgx1DOsIdtJ
X-Robots-Tag:
- noindex, nofollow
X-Xss-Protection:
@@ -795,5 +505,5 @@ http_interactions:
- recorded_at: Wed, 02 Oct 2024 08:50:39 GMT
+ recorded_at: Thu, 28 Nov 2024 14:30:04 GMT
recorded_with: VCR 6.3.1
diff --git a/modules/team_planner/config/locales/crowdin/js-hu.yml b/modules/team_planner/config/locales/crowdin/js-hu.yml
index ddbdb1399c4c..bc0af00f5cc8 100644
--- a/modules/team_planner/config/locales/crowdin/js-hu.yml
+++ b/modules/team_planner/config/locales/crowdin/js-hu.yml
@@ -3,7 +3,7 @@ hu:
js:
team_planner:
add_existing: 'Létező hozzáadása'
- add_existing_title: 'Add existing work packages'
+ add_existing_title: 'Meglévő munkacsomagok hozzáadása'
create_label: 'Csoport tervező'
create_title: 'Új csoport tervező létrehozása'
unsaved_title: 'Névtelen csoport tervező'
@@ -12,9 +12,9 @@ hu:
remove_assignee: 'Megbízott eltávolítása'
two_weeks: '2 hét'
one_week: '1 hét'
- four_weeks: '4-week'
- eight_weeks: '8-week'
- work_week: 'Work week'
+ four_weeks: '4 hét'
+ eight_weeks: '8 hetes'
+ work_week: 'Munkahét'
today: 'Ma'
drag_here_to_remove: 'Drag here to remove assignee and start and end dates.'
cannot_drag_here: 'Cannot remove the work package due to permissions or editing restrictions.'
@@ -24,5 +24,5 @@ hu:
search_placeholder: 'Keresés...'
modify:
errors:
- permission_denied: 'You do not have the necessary permissions to modify this.'
+ permission_denied: 'Nem rendelkezik a módosításhoz szükséges jogosultságokkal.'
fallback: 'Ez a munkacsomag nem szerkeszthető.'
diff --git a/modules/team_planner/config/locales/crowdin/js-ja.yml b/modules/team_planner/config/locales/crowdin/js-ja.yml
index 5222b7cfe6c6..240394ff011b 100644
--- a/modules/team_planner/config/locales/crowdin/js-ja.yml
+++ b/modules/team_planner/config/locales/crowdin/js-ja.yml
@@ -14,14 +14,14 @@ ja:
one_week: '1-week'
four_weeks: '4-week'
eight_weeks: '8-week'
- work_week: 'Work week'
+ work_week: ''
today: '今日'
- drag_here_to_remove: 'Drag here to remove assignee and start and end dates.'
+ drag_here_to_remove: ''
cannot_drag_here: 'Cannot remove the work package due to permissions or editing restrictions.'
cannot_drag_to_non_working_day: 'This work package cannot start/finish on a non-working day.'
quick_add:
empty_state: 'Use the search field to find work packages and drag them to the planner to assign it to someone and define start and end dates.'
- search_placeholder: 'Search...'
+ search_placeholder: '検索'
modify:
errors:
permission_denied: 'You do not have the necessary permissions to modify this.'
diff --git a/modules/team_planner/config/locales/crowdin/js-ro.yml b/modules/team_planner/config/locales/crowdin/js-ro.yml
index a8960cd99bc8..2a4b159aaecf 100644
--- a/modules/team_planner/config/locales/crowdin/js-ro.yml
+++ b/modules/team_planner/config/locales/crowdin/js-ro.yml
@@ -5,10 +5,10 @@ ro:
add_existing: 'Adaugă existent'
add_existing_title: 'Adăugarea pachetelor de lucru existente'
create_label: 'Planificator echipă'
- create_title: 'Creați un nou planificator de echipă'
+ create_title: 'Creează planificare echipă nouă'
unsaved_title: 'Planificator de echipă nenumit'
no_data: 'Adăugați persoane desemnate pentru a vă configura planificatorul echipei.'
- add_assignee: 'Assignee'
+ add_assignee: 'Executant'
remove_assignee: 'Înlătură responsabil'
two_weeks: '2-săptămână'
one_week: '1-săptămână'
@@ -18,9 +18,9 @@ ro:
today: 'Azi'
drag_here_to_remove: 'Trageți aici pentru a elimina responsabilul și a începe și a termina datele.'
cannot_drag_here: 'Nu se poate elimina pachetul de lucru din cauza permisiunilor sau restricțiilor de editare.'
- cannot_drag_to_non_working_day: 'Acest pachet de lucru nu poate începe/încheia o zi nelucrătoare.'
+ cannot_drag_to_non_working_day: 'Acest pachet de lucru nu poate să înceapă/se încheie într-o zi nelucrătoare.'
quick_add:
- empty_state: 'Utilizați câmpul de căutare pentru a găsi pachete de lucru și trageți-le la planificator pentru a-l atribui cuiva și defini datele de început și de sfârșit.'
+ empty_state: 'Utilizează câmpul de căutare pentru a găsi pachete de lucru și trage-le în planificare pentru a-l atribui cuiva și defini datele de început și de sfârșit.'
search_placeholder: 'Caută...'
modify:
errors:
diff --git a/modules/team_planner/config/locales/crowdin/ro.yml b/modules/team_planner/config/locales/crowdin/ro.yml
index 7bd738200e06..1b3c3660aa0e 100644
--- a/modules/team_planner/config/locales/crowdin/ro.yml
+++ b/modules/team_planner/config/locales/crowdin/ro.yml
@@ -1,16 +1,16 @@
#English strings go here
ro:
plugin_openproject_team_planner:
- name: "OpenProject Team Planner"
+ name: "Planificare echipă OpenProject"
description: "Provides team planner views."
permission_view_team_planner: "Vezi planificatorul echipei"
permission_manage_team_planner: "Gestionează planificatorul de echipe"
- project_module_team_planner_view: "Planificatori echipă"
+ project_module_team_planner_view: "Planificare echipă"
team_planner:
label_team_planner: "Planificator echipă"
label_new_team_planner: "Planificator echipă nou"
label_create_new_team_planner: "Creează planificator echipă nou"
- label_team_planner_plural: "Planificatori echipe"
+ label_team_planner_plural: "Planificare echipă"
label_assignees: "Responsabili"
upsale:
title: "Planificator echipă"
diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb
index a2cb5a503807..d224bbbde888 100644
--- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb
+++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb
@@ -106,7 +106,7 @@ def otp_service_for_verification(user)
def remembered_device(user)
if session[:two_factor_authentication_device_id]
- user.otp_devices.find(session[:two_factor_authentication_device_id])
+ user.otp_devices.find_by(id: session[:two_factor_authentication_device_id])
end
end
diff --git a/modules/two_factor_authentication/config/locales/crowdin/ro.yml b/modules/two_factor_authentication/config/locales/crowdin/ro.yml
index 662e921a5098..688a1bc9e848 100644
--- a/modules/two_factor_authentication/config/locales/crowdin/ro.yml
+++ b/modules/two_factor_authentication/config/locales/crowdin/ro.yml
@@ -58,7 +58,7 @@ ro:
label_enforced: "Aplicați 2FA"
label_remember: "Rețineți autentificarea 2FA"
text_configuration: |
- Notă: Aceste valori reprezintă configurația curentă la nivelul întregii aplicații. Nu puteți dezactiva setările impuse de configurație sau modifica strategiile active curente, deoarece acestea necesită o repornire a serverului.
+ Notă: Aceste valori reprezintă configurația curentă la nivelul întregii aplicații. Nu poți dezactiva setările impuse de configurație sau modifica strategiile active curente, deoarece acestea necesită o repornire a serverului.
text_configuration_guide: Pentru mai multe informații, consultați ghidul de configurare.
text_enforced: "Activează această setare pentru a forța toți utilizatorii să înregistreze un dispozitiv 2FA la următoarea autentificare. Poate fi dezactivată numai atunci când nu este impusă prin configurare."
text_remember: |
@@ -79,7 +79,7 @@ ro:
text_2fa_disabled: "The user did not set up a 2FA device through their 'My account page'"
only_sms_allowed: "Only SMS delivery can be set up for other users."
upsale:
- title: "Autentificare cu doi factori"
+ title: "Autentificare doi factori"
description: "Consolidarea securității instanței OpenProject prin oferirea (sau impunerea) autentificării cu doi factori pentru toți membrii proiectului."
backup_codes:
none_found: Nu există coduri de rezervă pentru acest cont.
diff --git a/modules/two_factor_authentication/config/locales/crowdin/tr.yml b/modules/two_factor_authentication/config/locales/crowdin/tr.yml
index 3afe9edc685d..a6a5acf7cfcb 100644
--- a/modules/two_factor_authentication/config/locales/crowdin/tr.yml
+++ b/modules/two_factor_authentication/config/locales/crowdin/tr.yml
@@ -79,7 +79,7 @@ tr:
text_2fa_disabled: "The user did not set up a 2FA device through their 'My account page'"
only_sms_allowed: "Only SMS delivery can be set up for other users."
upsale:
- title: "İki faktörlü kimlik doğrulama"
+ title: "İki aşamalı kimlik doğrulama"
description: "Tüm proje üyelerine iki faktörlü kimlik doğrulama sunarak (veya zorunlu kılarak) OpenProject bulut sunucunuzun güvenliğini güçlendirin."
backup_codes:
none_found: Bu hesap için yedek kod yok.
diff --git a/modules/two_factor_authentication/openproject-two_factor_authentication.gemspec b/modules/two_factor_authentication/openproject-two_factor_authentication.gemspec
index d36ba061eea9..c0cb74181ac2 100644
--- a/modules/two_factor_authentication/openproject-two_factor_authentication.gemspec
+++ b/modules/two_factor_authentication/openproject-two_factor_authentication.gemspec
@@ -14,6 +14,6 @@ Gem::Specification.new do |s|
s.add_dependency "rotp", "~> 6.1"
s.add_dependency "webauthn", "~> 3.0"
- s.add_dependency "aws-sdk-sns", "~> 1.88.0"
+ s.add_dependency "aws-sdk-sns", "~> 1.92.0"
s.metadata["rubygems_mfa_required"] = "true"
end
diff --git a/modules/two_factor_authentication/spec/features/account_activation_spec.rb b/modules/two_factor_authentication/spec/features/account_activation_spec.rb
index 3c5ac03cf980..a9a2a406c0c8 100644
--- a/modules/two_factor_authentication/spec/features/account_activation_spec.rb
+++ b/modules/two_factor_authentication/spec/features/account_activation_spec.rb
@@ -1,5 +1,5 @@
require_relative "../spec_helper"
-require_relative "shared_2fa_examples"
+require_relative "shared_two_factor_examples"
RSpec.describe "activating an invited account",
:js,
@@ -7,6 +7,8 @@
with_settings: {
plugin_openproject_two_factor_authentication: { "active_strategies" => [:developer] }
} do
+ include SharedTwoFactorExamples
+
let(:user) do
user = build(:user, first_login: true)
UserInvitation.invite_user! user
diff --git a/modules/two_factor_authentication/spec/features/backup_codes/generate_backup_codes_spec.rb b/modules/two_factor_authentication/spec/features/backup_codes/generate_backup_codes_spec.rb
index 681720b9900e..674aebd2ee2a 100644
--- a/modules/two_factor_authentication/spec/features/backup_codes/generate_backup_codes_spec.rb
+++ b/modules/two_factor_authentication/spec/features/backup_codes/generate_backup_codes_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Generate 2FA backup codes", :js, with_config: { "2fa": { active_strategies: [:developer] } } do
let(:user_password) { "bob!" * 4 }
diff --git a/modules/two_factor_authentication/spec/features/backup_codes/login_with_backup_code_spec.rb b/modules/two_factor_authentication/spec/features/backup_codes/login_with_backup_code_spec.rb
index a8b1cd7fcefb..f858e7f7aae1 100644
--- a/modules/two_factor_authentication/spec/features/backup_codes/login_with_backup_code_spec.rb
+++ b/modules/two_factor_authentication/spec/features/backup_codes/login_with_backup_code_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login with 2FA backup code",
:js,
@@ -7,6 +7,8 @@
with_settings: {
plugin_openproject_two_factor_authentication: { "active_strategies" => [:developer] }
} do
+ include SharedTwoFactorExamples
+
let(:user_password) { "bob!" * 4 }
let(:user) do
create(:user,
diff --git a/modules/two_factor_authentication/spec/features/login/login_enforced_2fa_spec.rb b/modules/two_factor_authentication/spec/features/login/login_enforced_2fa_spec.rb
index 6701fd82b24c..1e2dc96b06c7 100644
--- a/modules/two_factor_authentication/spec/features/login/login_enforced_2fa_spec.rb
+++ b/modules/two_factor_authentication/spec/features/login/login_enforced_2fa_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login with enforced 2FA",
:js,
@@ -10,6 +10,7 @@
"enforced" => true
}
} do
+ include SharedTwoFactorExamples
let(:user_password) { "bob!" * 4 }
let(:user) do
create(:user,
diff --git a/modules/two_factor_authentication/spec/features/login/login_with_2fa_spec.rb b/modules/two_factor_authentication/spec/features/login/login_with_2fa_spec.rb
index 8eff3e7d0be9..3b38562cad37 100644
--- a/modules/two_factor_authentication/spec/features/login/login_with_2fa_spec.rb
+++ b/modules/two_factor_authentication/spec/features/login/login_with_2fa_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login with 2FA device",
:js,
@@ -9,6 +9,7 @@
"active_strategies" => [:developer]
}
} do
+ include SharedTwoFactorExamples
let(:user_password) { "bob!" * 4 }
let(:user) do
create(:user,
diff --git a/modules/two_factor_authentication/spec/features/login/login_with_deleted_2fa_spec.rb b/modules/two_factor_authentication/spec/features/login/login_with_deleted_2fa_spec.rb
new file mode 100644
index 000000000000..6eed2c36addc
--- /dev/null
+++ b/modules/two_factor_authentication/spec/features/login/login_with_deleted_2fa_spec.rb
@@ -0,0 +1,54 @@
+require_relative "../../spec_helper"
+require_relative "../shared_two_factor_examples"
+
+RSpec.describe "Login after 2FA deleted 2FA was deleted (REGRESSION)",
+ :js,
+ :with_cuprite,
+ with_settings: {
+ plugin_openproject_two_factor_authentication: {
+ "active_strategies" => %i[developer totp]
+ }
+ } do
+ include SharedTwoFactorExamples
+ let(:user_password) { "bob!" * 4 }
+ let(:user) do
+ create(:user,
+ login: "bob",
+ password: user_password,
+ password_confirmation: user_password)
+ end
+
+ let!(:device1) { create(:two_factor_authentication_device_sms, user:, active: true, default: false) }
+ let!(:device2) { create(:two_factor_authentication_device_totp, user:, active: true, default: true) }
+
+ it "works correctly when not switching 2fa method" do
+ first_login_step
+
+ # ensure that no 2fa device is stored in the session
+ session_data = Sessions::UserSession.last.data
+ expect(session_data["two_factor_authentication_device_id"]).to be_nil
+
+ # destroy all 2fa devices
+ user.otp_devices.destroy_all
+
+ # make sure we can sign in without 2fa
+ first_login_step
+ expect_logged_in
+ end
+
+ it "works correctly when the 2fa method was switched before deleting" do
+ first_login_step
+ switch_two_factor_device(device1)
+
+ # ensure that the selected 2fa device is stored in the session
+ session_data = Sessions::UserSession.last.data
+ expect(session_data["two_factor_authentication_device_id"]).to eq(device1.id)
+
+ # destroy all 2fa devices
+ user.otp_devices.destroy_all
+
+ # make sure we can sign in without 2fa
+ first_login_step
+ expect_logged_in
+ end
+end
diff --git a/modules/two_factor_authentication/spec/features/login/login_without_2fa_spec.rb b/modules/two_factor_authentication/spec/features/login/login_without_2fa_spec.rb
index 34565fca2a2d..f68745efbd87 100644
--- a/modules/two_factor_authentication/spec/features/login/login_without_2fa_spec.rb
+++ b/modules/two_factor_authentication/spec/features/login/login_without_2fa_spec.rb
@@ -1,10 +1,11 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login with no required OTP",
:js,
:with_cuprite,
with_config: { "2fa": { active_strategies: [:developer] } } do
+ include SharedTwoFactorExamples
let(:user_password) { "bob!" * 4 }
let(:user) do
create(:user,
diff --git a/modules/two_factor_authentication/spec/features/login/switch_available_devices_spec.rb b/modules/two_factor_authentication/spec/features/login/switch_available_devices_spec.rb
index 086daa515d84..a5ff551ac6ce 100644
--- a/modules/two_factor_authentication/spec/features/login/switch_available_devices_spec.rb
+++ b/modules/two_factor_authentication/spec/features/login/switch_available_devices_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login by switching 2FA device",
:js,
@@ -7,6 +7,8 @@
with_settings: {
plugin_openproject_two_factor_authentication: { "active_strategies" => %i[developer totp] }
} do
+ include SharedTwoFactorExamples
+
let(:user_password) { "bob!" * 4 }
let(:user) do
create(:user,
diff --git a/modules/two_factor_authentication/spec/features/my_two_factor_devices_spec.rb b/modules/two_factor_authentication/spec/features/my_two_factor_devices_spec.rb
index b76eded6d0ba..ae343baaa2a2 100644
--- a/modules/two_factor_authentication/spec/features/my_two_factor_devices_spec.rb
+++ b/modules/two_factor_authentication/spec/features/my_two_factor_devices_spec.rb
@@ -15,6 +15,8 @@
password_confirmation: user_password)
end
+ include Flash::Expectations
+
before do
login_as user
end
@@ -118,6 +120,8 @@
find(".two-factor--mark-default-button").click
dialog.confirm_flow_with user_password, should_fail: false
+ expect_and_dismiss_flash(message: "Successful update")
+
expect(page).to have_css(".mobile-otp--two-factor-device-row", count: 2)
rows = page.all(".mobile-otp--two-factor-device-row")
expect(rows[0]).to have_css(".mobile-otp--two-factor-device-row td .icon-yes", count: 1)
diff --git a/modules/two_factor_authentication/spec/features/remember_cookie/login_with_remember_cookie_spec.rb b/modules/two_factor_authentication/spec/features/remember_cookie/login_with_remember_cookie_spec.rb
index 5a02d1438094..c936c8d58c9c 100644
--- a/modules/two_factor_authentication/spec/features/remember_cookie/login_with_remember_cookie_spec.rb
+++ b/modules/two_factor_authentication/spec/features/remember_cookie/login_with_remember_cookie_spec.rb
@@ -1,5 +1,5 @@
require_relative "../../spec_helper"
-require_relative "../shared_2fa_examples"
+require_relative "../shared_two_factor_examples"
RSpec.describe "Login with 2FA remember cookie",
:js,
@@ -10,6 +10,8 @@
allow_remember_for_days: 30
}
} do
+ include SharedTwoFactorExamples
+
let(:user_password) do
"user!user!"
end
diff --git a/modules/two_factor_authentication/spec/features/shared_2fa_examples.rb b/modules/two_factor_authentication/spec/features/shared_two_factor_examples.rb
similarity index 66%
rename from modules/two_factor_authentication/spec/features/shared_2fa_examples.rb
rename to modules/two_factor_authentication/spec/features/shared_two_factor_examples.rb
index 2d0a94a24659..3ef56d75a8af 100644
--- a/modules/two_factor_authentication/spec/features/shared_2fa_examples.rb
+++ b/modules/two_factor_authentication/spec/features/shared_two_factor_examples.rb
@@ -1,29 +1,38 @@
-def first_login_step
- visit signin_path
- within("#login-form") do
- fill_in("username", with: user.login)
- fill_in("password", with: user_password)
- click_link_or_button I18n.t(:button_login)
+module SharedTwoFactorExamples
+ def first_login_step
+ visit signin_path
+ within("#login-form") do
+ fill_in("username", with: user.login)
+ fill_in("password", with: user_password)
+ click_link_or_button I18n.t(:button_login)
+ end
+ wait_for_network_idle
end
- wait_for_network_idle
-end
-def two_factor_step(token)
- expect(page).to have_css("input#otp")
- fill_in "otp", with: token
- click_button I18n.t(:button_login)
- wait_for_network_idle
-end
+ def switch_two_factor_device(device)
+ within("#login-form") do
+ click_link_or_button I18n.t(:text_otp_not_receive)
+ click_link_or_button device.redacted_identifier
+ end
+ end
-def expect_logged_in
- visit my_account_path
- wait_for_network_idle
- expect(page).to have_css(".form--field-container", text: user.login)
-end
+ def two_factor_step(token)
+ expect(page).to have_css("input#otp")
+ fill_in "otp", with: token
+ click_button I18n.t(:button_login)
+ wait_for_network_idle
+ end
-def expect_not_logged_in
- visit my_account_path
- expect(page).to have_no_css(".form--field-container", text: user.login)
+ def expect_logged_in
+ visit my_account_path
+ wait_for_network_idle
+ expect(page).to have_css(".form--field-container", text: user.login)
+ end
+
+ def expect_not_logged_in
+ visit my_account_path
+ expect(page).to have_no_css(".form--field-container", text: user.login)
+ end
end
RSpec.shared_examples "login without 2FA" do
diff --git a/modules/xls_export/lib/open_project/xls_export/spreadsheet_builder.rb b/modules/xls_export/lib/open_project/xls_export/spreadsheet_builder.rb
index ddf0e0c5b337..b75de5b0f1e5 100644
--- a/modules/xls_export/lib/open_project/xls_export/spreadsheet_builder.rb
+++ b/modules/xls_export/lib/open_project/xls_export/spreadsheet_builder.rb
@@ -215,7 +215,7 @@ def raw_sheet
end
def currency_sign
- Setting.plugin_costs["costs_currency"]
+ Setting.costs_currency
end
def escaped_worksheet_name(name)
diff --git a/modules/xls_export/lib/open_project/xls_export/xls_views.rb b/modules/xls_export/lib/open_project/xls_export/xls_views.rb
index 4b9dba6566b9..859039037cbf 100644
--- a/modules/xls_export/lib/open_project/xls_export/xls_views.rb
+++ b/modules/xls_export/lib/open_project/xls_export/xls_views.rb
@@ -32,7 +32,7 @@ def show_result(row, unit_id = @unit_id, as_text = false)
def cost_type_unit_label(cost_type_id, cost_type_inst = nil, plural = true)
case cost_type_id
when -1 then l_hours(2).split[1..-1].join(" ") # get the plural for hours
- when 0 then Setting.plugin_costs["costs_currency"]
+ when 0 then Setting.costs_currency
else cost_type_label(cost_type_id, cost_type_inst, plural)
end
end
@@ -61,7 +61,7 @@ def set_title
end
def currency_format
- "#,##0.00 [$#{Setting.plugin_costs['costs_currency']}]"
+ "#,##0.00 [$#{Setting.costs_currency}]"
end
def number_format
diff --git a/modules/xls_export/spec/models/xls_export/work_package/exporter/xls_integration_spec.rb b/modules/xls_export/spec/models/xls_export/work_package/exporter/xls_integration_spec.rb
index 99e2d51463fe..9e79822d7492 100644
--- a/modules/xls_export/spec/models/xls_export/work_package/exporter/xls_integration_spec.rb
+++ b/modules/xls_export/spec/models/xls_export/work_package/exporter/xls_integration_spec.rb
@@ -161,7 +161,7 @@
end
end
- describe "with cost and time entries" do
+ describe "with cost and time entries", with_settings: { costs_currency: "EUR", costs_currency_format: "%n %u" } do
# Since this test has to work without the actual costs plugin we'll just add
# a custom field called 'costs' to emulate it.
@@ -198,12 +198,6 @@
end
let(:column_names) { ["subject", "status", "estimated_hours", custom_field.column_name] }
- before do
- allow(Setting)
- .to receive(:plugin_costs)
- .and_return("costs_currency" => "EUR", "costs_currency_format" => "%n %u")
- end
-
it "exports successfully the work packages with a cost column" do
expect(sheet.rows.size).to eq(4 + 1)
diff --git a/packaging/addons/openproject-edition/bin/preinstall b/packaging/addons/openproject-edition/bin/preinstall
index 1ed1fb690d23..d74638850e61 100755
--- a/packaging/addons/openproject-edition/bin/preinstall
+++ b/packaging/addons/openproject-edition/bin/preinstall
@@ -70,10 +70,10 @@ if [ "$edition" = "bim" ]; then
fi
if ! ${CLI} run which IfcConvert &>/dev/null ||
- ! echo "83eed7f2f12079df5f6a55a07f812d27b28620bd $(${CLI} run which IfcConvert)" | sha1sum -c - ; then
+ ! echo "2d48d5df36371fc5920a71f0d74b29449b0b166a $(${CLI} run which IfcConvert)" | sha1sum -c - ; then
echo "Fetching and installing IfcConvert..."
- wget --quiet https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-517b819-linux64.zip
- unzip -qq IfcConvert-v0.6.0-517b819-linux64.zip
+ wget --quiet https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-fea8e3a-linux64.zip
+ unzip -qq IfcConvert-v0.7.11-fea8e3a-linux64.zip
mv IfcConvert "$APP_HOME/bin/IfcConvert"
fi
diff --git a/script/pdf_export/generate_styles_doc b/script/pdf_export/generate_styles_doc
index 9e154c190ae5..df4e7be55060 100755
--- a/script/pdf_export/generate_styles_doc
+++ b/script/pdf_export/generate_styles_doc
@@ -7,7 +7,7 @@ require "yaml"
require "json"
root = File.expand_path("../../", __dir__)
-schema_file = File.join(root, "app", "models", "work_package", "pdf_export", "schema.json")
+schema_file = File.join(root, "app", "models", "work_package", "pdf_export", "export", "schema.json")
schema = JSON::load_file(schema_file)
generator = MarkdownToPDF::StyleSchemaDocsGenerator.new(schema)
markdown = generator.generate_markdown
diff --git a/spec/contracts/custom_fields/hierarchy/insert_item_contract_spec.rb b/spec/contracts/custom_fields/hierarchy/insert_item_contract_spec.rb
index 592774414496..36050663dd7a 100644
--- a/spec/contracts/custom_fields/hierarchy/insert_item_contract_spec.rb
+++ b/spec/contracts/custom_fields/hierarchy/insert_item_contract_spec.rb
@@ -67,15 +67,31 @@
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
- expect(result.errors.to_h).to include(label: ["must be unique within the same hierarchy level."])
+ expect(result.errors.to_h).to include(label: [I18n.t("dry_validation.errors.rules.label.not_unique")])
end
- context "if locale is set to 'de'" do
+ context "if another locale is set" do
+ let(:mordor) { "agh burzum-ishi krimpatul" }
+
+ before do
+ I18n.config.enforce_available_locales = false
+ I18n.backend.store_translations(
+ :mo,
+ { dry_validation: {
+ errors: { rules: { label: { not_unique: mordor } } }
+ } }
+ )
+ end
+
+ after do
+ I18n.config.enforce_available_locales = true
+ end
+
it "is invalid with localized validation errors" do
- I18n.with_locale(:de) do
+ I18n.with_locale(:mo) do
result = subject.call(params)
expect(result).to be_failure
- expect(result.errors.to_h).to include(label: ["muss innerhalb der gleichen Hierarchieebene eindeutig sein"])
+ expect(result.errors.to_h).to include(label: [mordor])
end
end
end
@@ -89,7 +105,7 @@
it "is invalid with localized validation errors" do
result = subject.call(params)
expect(result).to be_failure
- expect(result.errors.to_h).to include(short: ["must be unique within the same hierarchy level."])
+ expect(result.errors.to_h).to include(short: [I18n.t("dry_validation.errors.rules.short.not_unique")])
end
end
@@ -108,7 +124,7 @@
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
- expect(result.errors.to_h).to include(short: ["must be a string"])
+ expect(result.errors.to_h).to include(short: [I18n.t("dry_validation.errors.str?")])
end
end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 76029f45513f..488c72fcecbd 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -176,7 +176,7 @@
perform_enqueued_jobs
- expect_work_packages([parent, child, other_child], <<~TABLE)
+ expect_work_packages_after_reload([parent, child, other_child], <<~TABLE)
subject | status | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
parent | New | | | | 10h | 10h | 0%
child | Rejected | 10h | 5h | 50% | | |
@@ -245,7 +245,7 @@
perform_enqueued_jobs
- expect_work_packages([parent, child, other_child], <<~TABLE)
+ expect_work_packages_after_reload([parent, child, other_child], <<~TABLE)
subject | status | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
parent | New | | | 0% | 10h | 10h | 0%
child | Rejected | 10h | 3h | 70% | | |
@@ -271,7 +271,7 @@
perform_enqueued_jobs
- expect_work_packages([parent, child, other_child], <<~TABLE)
+ expect_work_packages_after_reload([parent, child, other_child], <<~TABLE)
subject | status | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
parent | New | | | 0% | 10h | 10h | 0%
child | Rejected | 10h | 6h | 40% | | |
diff --git a/spec/controllers/work_package_children_controller_spec.rb b/spec/controllers/work_package_children_controller_spec.rb
new file mode 100644
index 000000000000..57d87f2995d8
--- /dev/null
+++ b/spec/controllers/work_package_children_controller_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe WorkPackageChildrenController do
+ shared_let(:user) { create(:admin) }
+ shared_let(:project) { create(:project) }
+ shared_let(:work_package) { create(:work_package, project:) }
+ shared_let(:child_work_package) { create(:work_package, parent: work_package, project:) }
+
+ current_user { user }
+
+ describe "GET /work_packages/:work_package_id/children/new" do
+ before do
+ allow(WorkPackageRelationsTab::AddWorkPackageChildDialogComponent)
+ .to receive(:new)
+ .with(work_package:)
+ .and_call_original
+ end
+
+ it "renders the new template" do
+ get("new", params: { work_package_id: work_package.id }, as: :turbo_stream)
+ expect(response).to be_successful
+ expect(WorkPackageRelationsTab::AddWorkPackageChildDialogComponent)
+ .to have_received(:new)
+ .with(work_package:)
+ end
+ end
+
+ describe "DELETE /work_packages/:work_package_id/children/:id" do
+ before do
+ allow(WorkPackageRelationsTab::IndexComponent).to receive(:new).and_call_original
+ allow(controller).to receive(:replace_via_turbo_stream).and_call_original
+ end
+
+ it "deletes the child relationship" do
+ delete("destroy",
+ params: { work_package_id: work_package.id,
+ id: child_work_package.id },
+ as: :turbo_stream)
+
+ expect(response).to be_successful
+
+ expect(WorkPackageRelationsTab::IndexComponent).to have_received(:new)
+ .with(work_package:, relations: [], children: [])
+ expect(controller).to have_received(:replace_via_turbo_stream)
+ .with(component: an_instance_of(WorkPackageRelationsTab::IndexComponent))
+ expect(child_work_package.reload.parent).to be_nil
+ end
+ end
+end
diff --git a/spec/controllers/work_package_relations_controller_spec.rb b/spec/controllers/work_package_relations_controller_spec.rb
new file mode 100644
index 000000000000..f6f8b36b2d84
--- /dev/null
+++ b/spec/controllers/work_package_relations_controller_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe WorkPackageRelationsController do
+ shared_let(:user) { create(:admin) }
+ shared_let(:project) { create(:project) }
+ shared_let(:work_package) { create(:work_package, project:) }
+ shared_let(:related_work_package) { create(:work_package, project:) }
+ shared_let(:unrelated_work_package) { create(:work_package, project:) }
+ shared_let(:relation) do
+ create(:relation,
+ from: work_package,
+ to: related_work_package,
+ relation_type: Relation::TYPE_RELATES)
+ end
+ shared_let(:children) do
+ create_list(:work_package, 2, parent: work_package, project:)
+ end
+
+ current_user { user }
+
+ describe "GET /work_packages/:work_package_id/relations/new" do
+ let(:new_relation) do
+ build(:relation,
+ from: work_package,
+ to: nil,
+ relation_type: Relation::TYPE_RELATES)
+ end
+
+ before do
+ allow(WorkPackageRelationsTab::WorkPackageRelationDialogComponent)
+ .to receive(:new)
+ .and_call_original
+ allow(controller).to receive(:respond_with_dialog).and_call_original
+ end
+
+ it "renders the relations new dialog" do
+ get("new",
+ params: { work_package_id: work_package.id,
+ relation_type: Relation::TYPE_RELATES },
+ as: :turbo_stream)
+
+ expect(WorkPackageRelationsTab::WorkPackageRelationDialogComponent)
+ .to have_received(:new)
+ expect(controller).to have_received(:respond_with_dialog)
+ end
+ end
+
+ describe "GET /work_packages/:work_package_id/relations/:id/edit" do
+ before do
+ allow(WorkPackageRelationsTab::WorkPackageRelationDialogComponent)
+ .to receive(:new)
+ .and_call_original
+ allow(controller).to receive(:respond_with_dialog).and_call_original
+ end
+
+ it "renders the relations edit dialog" do
+ get("edit",
+ params: { work_package_id: work_package.id, id: relation.id },
+ as: :turbo_stream)
+
+ expect(WorkPackageRelationsTab::WorkPackageRelationDialogComponent)
+ .to have_received(:new)
+ .with(work_package:, relation:)
+
+ expect(controller).to have_received(:respond_with_dialog)
+ .with(an_instance_of(WorkPackageRelationsTab::WorkPackageRelationDialogComponent))
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "POST /work_packages/:work_package_id/relations" do
+ before do
+ allow(WorkPackageRelationsTab::IndexComponent).to receive(:new).and_call_original
+ allow(controller).to receive(:replace_via_turbo_stream).and_call_original
+ end
+
+ it "creates the relation" do
+ post("create",
+ params: { work_package_id: work_package.id,
+ relation: { to_id: unrelated_work_package.id,
+ relation_type: Relation::TYPE_RELATES } },
+ as: :turbo_stream)
+
+ expect(response).to be_successful
+
+ new_relation = Relation.last
+
+ expect(WorkPackageRelationsTab::IndexComponent).to have_received(:new)
+ .with(work_package:, relations: [relation, new_relation], children:)
+ expect(controller).to have_received(:replace_via_turbo_stream)
+ .with(component: an_instance_of(WorkPackageRelationsTab::IndexComponent))
+ end
+ end
+
+ describe "PATCH /work_packages/:work_package_id/relations/:id" do
+ before do
+ relation.update!(description: "Old relation description")
+ allow(WorkPackageRelationsTab::IndexComponent).to receive(:new).and_call_original
+ allow(controller).to receive(:replace_via_turbo_stream).and_call_original
+ end
+
+ it "updates the relation description" do
+ patch("update",
+ params: { work_package_id: work_package.id,
+ id: relation.id,
+ relation: { description: "New fancy relation description" } },
+ as: :turbo_stream)
+
+ expect(relation.reload.description).to eq("New fancy relation description")
+
+ expect(response).to be_successful
+
+ expect(WorkPackageRelationsTab::IndexComponent).to have_received(:new)
+ .with(work_package:, relations: [relation], children:)
+ expect(controller).to have_received(:replace_via_turbo_stream)
+ .with(component: an_instance_of(WorkPackageRelationsTab::IndexComponent))
+ end
+ end
+
+ describe "DELETE /work_packages/:work_package_id/relations/:id" do
+ before do
+ allow(WorkPackageRelationsTab::IndexComponent).to receive(:new).and_call_original
+ allow(controller).to receive(:replace_via_turbo_stream).and_call_original
+ end
+
+ it "deletes the relation" do
+ delete("destroy", params: { work_package_id: work_package.id, id: relation.id }, as: :turbo_stream)
+
+ expect(response).to be_successful
+
+ expect(WorkPackageRelationsTab::IndexComponent).to have_received(:new)
+ .with(work_package:, relations: [], children:)
+ expect(controller).to have_received(:replace_via_turbo_stream)
+ .with(component: an_instance_of(WorkPackageRelationsTab::IndexComponent))
+ expect { relation.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/controllers/work_package_relations_tab_controller_spec.rb b/spec/controllers/work_package_relations_tab_controller_spec.rb
new file mode 100644
index 000000000000..e9b3f8b4b79a
--- /dev/null
+++ b/spec/controllers/work_package_relations_tab_controller_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe WorkPackageRelationsTabController do
+ shared_let(:user) { create(:admin) }
+ shared_let(:project) { create(:project) }
+ shared_let(:work_package) { create(:work_package, project:) }
+ shared_let(:related_work_package) { create(:work_package, project:) }
+ shared_let(:relations) do
+ create_list(:relation,
+ 1,
+ from: work_package,
+ to: related_work_package,
+ relation_type: Relation::TYPE_RELATES)
+ end
+ shared_let(:children) do
+ create_list(:work_package, 2, parent: work_package, project:)
+ end
+
+ current_user { user }
+
+ describe "GET /work_packages/:work_package_id/relations_tab" do
+ before do
+ allow(WorkPackageRelationsTab::IndexComponent).to receive(:new).and_call_original
+ end
+
+ it "renders the relations tab" do
+ get("index", params: { work_package_id: work_package.id }, as: :turbo_stream)
+ expect(WorkPackageRelationsTab::IndexComponent).to have_received(:new).with(
+ work_package:,
+ relations:,
+ children:
+ )
+
+ expect(response).to be_successful
+ end
+ end
+end
diff --git a/spec/factories/custom_field_factory.rb b/spec/factories/custom_field_factory.rb
index ace2dada9551..a849b8432dab 100644
--- a/spec/factories/custom_field_factory.rb
+++ b/spec/factories/custom_field_factory.rb
@@ -165,6 +165,10 @@
field_format { "link" }
end
+ trait :hierarchy do
+ field_format { "hierarchy" }
+ end
+
factory :project_custom_field, class: "ProjectCustomField" do
project_custom_field_section
@@ -227,6 +231,7 @@
factory :user_wp_custom_field, traits: [:user]
factory :multi_user_wp_custom_field, traits: [:multi_user]
factory :link_wp_custom_field, traits: [:link]
+ factory :hierarchy_wp_custom_field, traits: [:hierarchy]
end
factory :issue_custom_field, class: "WorkPackageCustomField" do
diff --git a/spec/factories/project_life_cycle_step_definition_factory.rb b/spec/factories/project_life_cycle_step_definition_factory.rb
new file mode 100644
index 000000000000..f629dcb4018a
--- /dev/null
+++ b/spec/factories/project_life_cycle_step_definition_factory.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+FactoryBot.define do
+ factory :project_life_cycle_step_definition, class: "Project::LifeCycleStepDefinition" do
+ color
+
+ factory :project_gate_definition, class: "Project::GateDefinition" do
+ sequence(:name) { |n| "Gate Definition No. #{n}" }
+ end
+
+ factory :project_stage_definition, class: "Project::StageDefinition" do
+ sequence(:name) { |n| "Stage Definition No. #{n}" }
+ end
+ end
+end
diff --git a/spec/factories/project_life_cycle_step_factory.rb b/spec/factories/project_life_cycle_step_factory.rb
new file mode 100644
index 000000000000..cc64e09b02e7
--- /dev/null
+++ b/spec/factories/project_life_cycle_step_factory.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+FactoryBot.define do
+ factory :project_life_cycle_step, class: "Project::LifeCycleStep" do
+ project
+ active { true }
+
+ factory :project_stage, class: "Project::Stage" do
+ definition factory: :project_stage_definition
+ start_date { Date.current - 2.days }
+ end_date { Date.current + 2.days }
+ end
+
+ factory :project_gate, class: "Project::Gate" do
+ definition factory: :project_gate_definition
+ date { Date.current + 2.days }
+ end
+ end
+end
diff --git a/spec/features/activities/work_package/activities_spec.rb b/spec/features/activities/work_package/activities_spec.rb
index 94b98f9d259e..c5daaa023279 100644
--- a/spec/features/activities/work_package/activities_spec.rb
+++ b/spec/features/activities/work_package/activities_spec.rb
@@ -27,8 +27,11 @@
#++
require "spec_helper"
+require "support/flash/expectations"
RSpec.describe "Work package activity", :js, :with_cuprite, with_flag: { primerized_work_package_activities: true } do
+ include Flash::Expectations
+
let(:project) { create(:project) }
let(:admin) { create(:admin) }
let(:member_role) do
@@ -79,6 +82,14 @@
member_with_roles: { project => user_role_with_editing_permission })
end
+ let(:comment_work_package_role) { create(:comment_work_package_role) }
+ let(:user_with_commenting_permission_via_a_work_package_share) do
+ create(:user,
+ firstname: "A",
+ lastname: "Commenter",
+ member_with_roles: { work_package => comment_work_package_role })
+ end
+
let(:work_package) { create(:work_package, project:, author: admin) }
let(:first_comment) do
create(:work_package_journal, user: admin, notes: "First comment by admin", journable: work_package,
@@ -202,6 +213,22 @@
end
end
end
+
+ context "when a user has been shared a work package with at least comment rights" do
+ current_user { user_with_commenting_permission_via_a_work_package_share }
+
+ before do
+ wp_page.visit!
+ wp_page.wait_for_activity_tab
+ end
+
+ it "allows commenting on the work package" do
+ activity_tab.expect_input_field
+
+ activity_tab.add_comment(text: "First comment by user with commenting permission via a work package share")
+ activity_tab.expect_journal_notes(text: "First comment by user with commenting permission via a work package share")
+ end
+ end
end
context "when a workpackage is created and visited by the same user" do
@@ -369,7 +396,7 @@
version: 2)
# the comment is shown without browser reload
- wait_for { page }.to have_test_selector("op-journal-notes-body", text: "First comment by member")
+ activity_tab.expect_journal_notes(text: "First comment by member")
# simulate comments made within the polling interval
create(:work_package_journal, user: member, notes: "Second comment by member", journable: work_package, version: 3)
@@ -387,7 +414,7 @@
first_journal.update!(notes: "First comment by member updated")
# properly updates the comment when the comment is updated
- wait(delay: 0.5).for { page }.to have_test_selector("op-journal-notes-body", text: "First comment by member updated")
+ activity_tab.expect_journal_notes(text: "First comment by member updated")
end
end
@@ -1239,4 +1266,111 @@
end
end
end
+
+ describe "error handling" do
+ let(:work_package) { create(:work_package, project:, author: admin) }
+
+ current_user { admin }
+
+ before do
+ wp_page.visit!
+ wp_page.wait_for_activity_tab
+ end
+
+ context "when adding a comment" do
+ context "when the creation call raises an unknown server error" do
+ before do
+ allow_any_instance_of(WorkPackages::ActivitiesTabController) # rubocop:disable RSpec/AnyInstance
+ .to receive(:create_journal_service_call)
+ .and_raise(StandardError.new("Test error"))
+ end
+
+ it "shows an error banner when the server returns an error" do
+ activity_tab.add_comment(text: "First comment by admin", save: false)
+
+ page.find_test_selector("op-submit-work-package-journal-form").click
+
+ expect_flash(message: "Test error", type: :error)
+
+ # expect the editor content not to be lost
+ within_test_selector("op-work-package-journal-form-element") do
+ editor = FormFields::Primerized::EditorFormField.new("notes", selector: "#work-package-journal-form-element")
+ editor.expect_value("First comment by admin")
+ end
+ end
+ end
+
+ context "when the creation call fails with a validation error" do
+ before do
+ allow_any_instance_of(AddWorkPackageNoteService) # rubocop:disable RSpec/AnyInstance
+ .to receive(:call)
+ .and_return(
+ ServiceResult.failure(errors: ActiveModel::Errors.new(Journal.new).tap do |e|
+ e.add(:notes, "Validation error")
+ end)
+ )
+ end
+
+ it "shows a validation error banner" do
+ activity_tab.add_comment(text: "First comment by admin", save: false)
+
+ page.find_test_selector("op-submit-work-package-journal-form").click
+
+ expect_flash(message: "Validation error", type: :error)
+
+ # expect the editor content not to be lost
+ within_test_selector("op-work-package-journal-form-element") do
+ editor = FormFields::Primerized::EditorFormField.new("notes", selector: "#work-package-journal-form-element")
+ editor.expect_value("First comment by admin")
+ end
+ end
+ end
+ end
+
+ context "when editing a comment" do
+ let!(:first_comment_by_admin) do
+ create(:work_package_journal, user: admin, notes: "First comment by admin", journable: work_package, version: 2)
+ end
+
+ context "when the update call raises an unknown server error" do
+ before do
+ allow_any_instance_of(WorkPackages::ActivitiesTabController) # rubocop:disable RSpec/AnyInstance
+ .to receive(:update_journal_service_call)
+ .and_raise(StandardError.new("Test error"))
+ end
+
+ it "shows an error banner" do
+ activity_tab.edit_comment(first_comment_by_admin, text: "First comment by admin edited", save: false)
+
+ page.within_test_selector("op-work-package-journal-form-element") do
+ page.find_test_selector("op-submit-work-package-journal-form").click
+ end
+
+ expect_flash(message: "Test error", type: :error)
+ end
+ end
+
+ context "when the update call fails with a validation error" do
+ before do
+ allow_any_instance_of(Journals::UpdateService) # rubocop:disable RSpec/AnyInstance
+ .to receive(:call)
+ .and_return(
+ ServiceResult.failure(errors: ActiveModel::Errors.new(Journal.new).tap do |e|
+ e.add(:notes, "Validation error")
+ end)
+ )
+ end
+
+ it "shows a validation error banner" do
+ activity_tab.edit_comment(first_comment_by_admin, text: "First comment by admin edited", save: false)
+
+ page.within_test_selector("op-work-package-journal-form-element") do
+ page.find_test_selector("op-submit-work-package-journal-form").click
+ end
+
+ expect_flash(message: "Validation error", type: :error)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/admin/custom_fields/user_custom_field_spec.rb b/spec/features/admin/custom_fields/user_custom_field_spec.rb
index 58a76a7363cf..146c5def776f 100644
--- a/spec/features/admin/custom_fields/user_custom_field_spec.rb
+++ b/spec/features/admin/custom_fields/user_custom_field_spec.rb
@@ -31,6 +31,7 @@
RSpec.describe "User custom fields edit", :js, :with_cuprite do
shared_let(:admin) { create(:admin) }
let(:cf_page) { Pages::CustomFields::IndexPage.new }
+ let(:new_cf_page) { Pages::CustomFields::NewPage.new }
before do
login_as(admin)
@@ -39,30 +40,30 @@
it "can create and edit user custom fields (#48725)" do
# Create CF
- click_link "Create a new custom field"
+ click_on "New custom field"
+ new_cf_page.expect_current_path
- wait_for_reload
-
- fill_in "custom_field_name", with: "My User CF"
- select "User", from: "custom_field_field_format"
+ fill_in "Name", with: "My User CF"
+ select "User", from: "Format"
expect(page).to have_no_field("custom_field_custom_options_attributes_0_value")
click_on "Save"
# Expect field to be created
- cf = CustomField.last
- expect(cf.name).to eq("My User CF")
+ cf_page.expect_current_path("tab=WorkPackageCustomField")
+ expect(page).to have_list_item("My User CF")
# Edit again
- find("a", text: "My User CF").click
+ click_on "My User CF"
expect(page).to have_no_field("custom_field_custom_options_attributes_0_value")
- fill_in "custom_field_name", with: "My User CF (edited)"
+ fill_in "Name", with: "My User CF (edited)"
click_on "Save"
# Expect field to be saved
+ expect(page).to have_css(".PageHeader-title", text: "My User CF (edited)")
cf = CustomField.last
expect(cf.name).to eq("My User CF (edited)")
end
diff --git a/spec/features/custom_fields/hierarchy_custom_field_spec.rb b/spec/features/custom_fields/hierarchy_custom_field_spec.rb
index d7632338978d..68542e168e23 100644
--- a/spec/features/custom_fields/hierarchy_custom_field_spec.rb
+++ b/spec/features/custom_fields/hierarchy_custom_field_spec.rb
@@ -31,14 +31,18 @@
require "spec_helper"
RSpec.describe "custom fields of type hierarchy", :js, :with_cuprite do
- let(:user) { create(:admin) }
+ let(:admin) { create(:admin) }
let(:custom_field_index_page) { Pages::CustomFields::IndexPage.new }
let(:new_custom_field_page) { Pages::CustomFields::NewPage.new }
let(:hierarchy_page) { Pages::CustomFields::HierarchyPage.new }
+ before do
+ allow(EnterpriseToken).to receive(:allows_to?).and_return(true)
+ end
+
it "lets you create, update and delete a custom field of type hierarchy",
with_flag: { custom_field_of_type_hierarchy: true } do
- login_as user
+ login_as admin
# region CustomField creation
diff --git a/spec/features/members/pagination_spec.rb b/spec/features/members/pagination_spec.rb
index d9b972ed8c7d..854c7ef307e7 100644
--- a/spec/features/members/pagination_spec.rb
+++ b/spec/features/members/pagination_spec.rb
@@ -28,7 +28,7 @@
require "spec_helper"
-RSpec.describe "members pagination", :js do
+RSpec.describe "members pagination", :js, :with_cuprite do
shared_let(:admin) { create(:admin) }
let(:project) do
create(:project,
diff --git a/spec/features/news/creation_and_commenting_spec.rb b/spec/features/news/creation_and_commenting_spec.rb
index 36cf66c114d6..e99afdc2a61a 100644
--- a/spec/features/news/creation_and_commenting_spec.rb
+++ b/spec/features/news/creation_and_commenting_spec.rb
@@ -43,6 +43,8 @@
member_with_permissions: { project => %i[manage_news comment_news] })
end
+ include Flash::Expectations
+
it "allows creating new and commenting it all of which will result in notifications and mails" do
visit project_news_index_path(project)
@@ -81,8 +83,11 @@
perform_enqueued_jobs do
click_button "Add comment"
+ wait_for_network_idle
end
+ expect_and_dismiss_flash message: "Comment added"
+
# The new comment is visible on the show page
expect(page)
.to have_content "A new text"
diff --git a/spec/features/work_packages/copy_spec.rb b/spec/features/work_packages/copy_spec.rb
index d9e48240eec3..9479edfb8c21 100644
--- a/spec/features/work_packages/copy_spec.rb
+++ b/spec/features/work_packages/copy_spec.rb
@@ -84,6 +84,8 @@
project:)
end
+ let(:relations_tab) { Components::WorkPackages::Relations.new(original_work_package) }
+
before do
login_as(user)
original_work_package.save!
@@ -123,8 +125,11 @@
work_package_page.visit_tab! :relations
expect_angular_frontend_initialized
- expect(page).to have_css(".relation-group--header", text: "RELATED TO", wait: 20)
- expect(page).to have_test_selector("op-relation--row-subject", text: original_work_package.subject)
+ work_package_page.expect_subject
+ loading_indicator_saveguard
+
+ relations_tab.expect_relation_group(:relates)
+ relations_tab.expect_relation_by_text(original_work_package.subject)
end
describe "when source work package has an attachment" do
@@ -176,7 +181,10 @@
work_package_page.visit_tab!("relations")
expect_angular_frontend_initialized
- expect(page).to have_css(".relation-group--header", text: "RELATED TO", wait: 20)
- expect(page).to have_test_selector("op-relation--row-subject", text: original_work_package.subject)
+ work_package_page.expect_subject
+ loading_indicator_saveguard
+
+ relations_tab.expect_relation_group(:relates)
+ relations_tab.expect_relation_by_text(original_work_package.subject)
end
end
diff --git a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb
index df731c9e6009..e8bab886179a 100644
--- a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb
+++ b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb
@@ -231,7 +231,7 @@
end
# adding existing from relations tab
- relations.add_relation type: Relation::TYPES[relation_type.to_s][:sym], to: independent_work_package
+ relations.add_relation type: Relation::TYPES[relation_type.to_s][:sym], relatable: independent_work_package
within(embedded_table.table_container) do
embedded_table.expect_work_package_listed(independent_work_package)
end
@@ -254,7 +254,7 @@
end
# adding existing from relations tab will show work package also in the embedded table
- relations.add_relation type: Relation::TYPES[relation_type.to_s][:sym], to: independent_work_package
+ relations.add_relation type: Relation::TYPES[relation_type.to_s][:sym], relatable: independent_work_package
within(embedded_table.table_container) do
embedded_table.expect_work_package_listed(independent_work_package)
end
diff --git a/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb b/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb
deleted file mode 100644
index cf7cea9d4e32..000000000000
--- a/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-#-- copyright
-# OpenProject is an open source project management software.
-# Copyright (C) the OpenProject GmbH
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License version 3.
-#
-# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-# Copyright (C) 2006-2013 Jean-Philippe Lang
-# Copyright (C) 2010-2013 the ChiliProject Team
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-# See COPYRIGHT and LICENSE files for more details.
-#++
-
-require "spec_helper"
-
-RSpec.describe "creating a child directly after the wp itself was created", :js do
- let(:user) { create(:admin) }
- let(:project) { create(:project, types: [type]) }
- let(:wp_page) { Pages::FullWorkPackageCreate.new }
-
- let!(:status) { create(:status, is_default: true) }
- let!(:priority) { create(:priority, is_default: true) }
- let(:type) { create(:type, custom_fields: [custom_field]) }
- let(:custom_field) do
- create(:work_package_custom_field,
- field_format: "int",
- is_for_all: true)
- end
- let(:relations_tab) { find(".op-tab-row--link", text: "RELATIONS") }
-
- before do
- login_as user
- visit new_project_work_packages_path(project.identifier, type: type.id)
- expect_angular_frontend_initialized
- loading_indicator_saveguard
- end
-
- it "keeps its custom field values (regression #29511, #29446)" do
- # Set subject
- subject = wp_page.edit_field :subject
- subject.set_value "My subject"
-
- # Set CF
- cf = wp_page.edit_field custom_field.attribute_name(:camel_case)
- cf.set_value "42"
-
- # Save WP
- wp_page.save!
- wp_page.expect_and_dismiss_toaster(message: "Successful creation.")
-
- # Add child
- scroll_to_and_click relations_tab
- find_test_selector("op-wp-inline-create").click
- fill_in "wp-new-inline-edit--field-subject", with: "A child WP"
- find_by_id("wp-new-inline-edit--field-subject").native.send_keys(:return)
-
- # Expect CF value to be still visible
- wp_page.expect_and_dismiss_toaster(message: "Successful creation.")
- expect(wp_page).to have_test_selector("tab-count", text: "(1)")
- wp_page.expect_attributes "customField#{custom_field.id}": "42"
- end
-end
diff --git a/spec/features/work_packages/details/relations/hierarchy_spec.rb b/spec/features/work_packages/details/relations/hierarchy_spec.rb
index a66690e17fa8..ca29971fc182 100644
--- a/spec/features/work_packages/details/relations/hierarchy_spec.rb
+++ b/spec/features/work_packages/details/relations/hierarchy_spec.rb
@@ -62,99 +62,31 @@ def visit_relations
it "allows to manage hierarchy" do
# Add parent
relations.add_parent(parent.id, parent)
+ wp_page.expect_and_dismiss_toaster(message: "Successful update.")
relations.expect_parent(parent)
##
# Add child #1
- relations.open_children_autocompleter
-
relations.add_existing_child(child)
+ expect_and_dismiss_flash(message: "Successful update.")
relations.expect_child(child)
##
# Add child #2
- relations.open_children_autocompleter
-
relations.add_existing_child(child2)
+ expect_and_dismiss_flash(message: "Successful update.")
relations.expect_child(child2)
# Count child relations in split view
- tabs.expect_counter(relations_tab, 2)
- end
-
- context "when switching to custom field with required CF" do
- let(:custom_field) do
- create(
- :work_package_custom_field,
- field_format: "string",
- default_value: nil,
- is_required: true,
- is_for_all: true
- )
- end
- let(:type2) { create(:type, custom_fields: [custom_field]) }
- let(:relations) { Components::WorkPackages::Relations.new(parent) }
- let!(:status) { create(:status, is_default: true) }
- let!(:priority) { create(:priority, is_default: true) }
-
- before do
- project.types << type2
- project.save!
- custom_field
- end
-
- it "shows the required field when switching" do
- relations.inline_create_child "my new child"
- table = relations.children_table
-
- table.expect_work_package_subject "my new child"
- wp = WorkPackage.find_by!(subject: "my new child")
- type_field = table.edit_field(wp, :type)
-
- type_field.activate!
- type_field.set_value type2.name
-
- wp_page.expect_toast message: "#{custom_field.name} can't be blank.",
- type: "error"
-
- cf_field = wp_page.edit_field(custom_field.attribute_name(:camel_case))
- cf_field.expect_active!
- cf_field.expect_value("")
-
- cf_field.set_value "my value"
- cf_field.save!
-
- wp_page.expect_toast message: "Successful update.",
- type: "success"
+ # Marking as "visible: :all" because the counter
+ # is hidden by some weird white element only in TEST mode
+ # in the header.
- wp.reload
- expect(wp.custom_value_for(custom_field).value).to eq "my value"
- end
- end
-
- describe "inline create" do
- let!(:status) { create(:status, is_default: true) }
- let!(:priority) { create(:priority, is_default: true) }
- let(:type_bug) { create(:type_bug) }
- let!(:project) do
- create(:project, types: [type_bug])
- end
-
- it "can inline-create children" do
- relations.inline_create_child "my new child"
- table = relations.children_table
-
- table.expect_work_package_subject "my new child"
- work_package.reload
- expect(work_package.children.count).to eq(1)
-
- # If new child is inline created, counter should increase
- tabs.expect_counter(relations_tab, 1)
- end
+ tabs.expect_counter(relations_tab, 2)
end
end
- describe "relation group-by toggler" do
+ describe "as non-admin" do
let(:project) { create(:project, types: [type1, type2]) }
let(:type1) { create(:type) }
let(:type2) { create(:type) }
@@ -175,7 +107,6 @@ def visit_relations
relation_type: Relation::TYPE_RELATES)
end
- let(:toggle_btn_selector) { "#wp-relation-group-by-toggle" }
let(:visit) { false }
before do
@@ -202,19 +133,12 @@ def visit_relations
let!(:work_package) { create(:work_package, parent:, project:, subject: "Child WP") }
it "shows no links to create relations" do
- # No create buttons should exist
- expect(page).to have_no_css(".wp-relations-create-button")
-
- # Test for add relation
- expect(page).to have_no_css("#relation--add-relation")
+ # No create buttons should exist (relation or children)
+ relations.expect_no_add_relation_button
# Test for add parent
expect(page).to have_no_css(".wp-relation--parent-change")
- # Test for add children
- expect(page).to have_no_css("#hierarchy--add-existing-child")
- expect(page).to have_no_css("#hierarchy--add-new-child")
-
# But it should show the linked parent
expect(page).to have_test_selector("op-wp-breadcrumb-parent", text: parent.subject)
@@ -236,10 +160,8 @@ def visit_relations
##
# Add child
- relations.open_children_autocompleter
-
relations.add_existing_child(child)
- wp_page.expect_and_dismiss_toaster(message: "Successful update.")
+ expect_and_dismiss_flash(message: "Successful update.")
relations.expect_child(child)
# Expect counter to add up child to the existing relations
@@ -253,6 +175,7 @@ def visit_relations
# Remove child
relations.remove_child(child)
# Should also check for successful update but no message is shown, yet.
+ expect_and_dismiss_flash(message: "Successful update.")
relations.expect_not_child(child)
# Expect counter to count the two relations
diff --git a/spec/features/work_packages/details/relations/primerized_relations_spec.rb b/spec/features/work_packages/details/relations/primerized_relations_spec.rb
new file mode 100644
index 000000000000..7fa9a4236185
--- /dev/null
+++ b/spec/features/work_packages/details/relations/primerized_relations_spec.rb
@@ -0,0 +1,332 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe "Primerized work package relations tab",
+ :js, :with_cuprite do
+ include Components::Autocompleter::NgSelectAutocompleteHelpers
+
+ shared_let(:user) { create(:admin) }
+ shared_let(:project) { create(:project) }
+
+ before_all do
+ set_factory_default(:user, user)
+ set_factory_default(:project, project)
+ set_factory_default(:project_with_types, project)
+ end
+
+ shared_let(:work_package) { create(:work_package, subject: "main") }
+ shared_let(:type1) { create(:type) }
+ shared_let(:type2) { create(:type) }
+
+ shared_let(:wp_predecessor) do
+ create(:work_package, type: type1, subject: "predecessor of main",
+ start_date: Date.current, due_date: Date.current + 1.week)
+ end
+ shared_let(:wp_related) { create(:work_package, type: type2, subject: "related to main") }
+ shared_let(:wp_blocker) { create(:work_package, type: type1, subject: "blocks main") }
+
+ shared_let(:relation_follows) do
+ create(:relation,
+ from: work_package,
+ to: wp_predecessor,
+ relation_type: Relation::TYPE_FOLLOWS)
+ end
+ shared_let(:relation_relates) do
+ create(:relation,
+ from: work_package,
+ to: wp_related,
+ relation_type: Relation::TYPE_RELATES)
+ end
+ shared_let(:relation_blocked) do
+ create(:relation,
+ from: wp_blocker,
+ to: work_package,
+ relation_type: Relation::TYPE_BLOCKED)
+ end
+ shared_let(:child_wp) do
+ create(:work_package,
+ parent: work_package,
+ type: type1,
+ project: project)
+ end
+ shared_let(:not_yet_child_wp) do
+ create(:work_package,
+ type: type1,
+ project:)
+ end
+
+ let(:full_wp_view) { Pages::FullWorkPackage.new(work_package) }
+ let(:relations_tab) { Components::WorkPackages::Relations.new(work_package) }
+ let(:relations_panel_selector) { ".detail-panel--relations" }
+ let(:relations_panel) { find(relations_panel_selector) }
+ let(:work_packages_page) { Pages::PrimerizedSplitWorkPackage.new(work_package) }
+ let(:tabs) { Components::WorkPackages::PrimerizedTabs.new }
+
+ current_user { user }
+
+ def label_for_relation_type(relation_type)
+ I18n.t("work_package_relations_tab.relations.label_#{relation_type}_plural").capitalize
+ end
+
+ before do
+ work_packages_page.visit_tab!("relations")
+ expect_angular_frontend_initialized
+ work_packages_page.expect_subject
+ loading_indicator_saveguard
+ end
+
+ describe "rendering" do
+ it "renders the relations tab" do
+ scroll_to_element relations_panel
+ expect(page).to have_css(relations_panel_selector)
+
+ tabs.expect_counter("relations", 4)
+
+ relations_tab.expect_relation(relation_follows)
+ relations_tab.expect_relation(relation_relates)
+ relations_tab.expect_relation(relation_blocked)
+ end
+ end
+
+ describe "deletion" do
+ it "can delete relations" do
+ scroll_to_element relations_panel
+
+ relations_tab.remove_relation(relation_follows)
+
+ expect { relation_follows.reload }.to raise_error(ActiveRecord::RecordNotFound)
+
+ tabs.expect_counter("relations", 3)
+ end
+
+ it "can delete children" do
+ scroll_to_element relations_panel
+
+ relations_tab.remove_child(child_wp)
+ expect(child_wp.reload.parent).to be_nil
+
+ tabs.expect_counter("relations", 3)
+ end
+ end
+
+ describe "editing" do
+ it "renders an edit form" do
+ scroll_to_element relations_panel
+
+ relation_row = relations_tab.expect_relation(relation_follows)
+
+ relations_tab.edit_relation_description(relation_follows, "Discovered relations have descriptions!")
+
+ relations_tab.edit_lag_of_relation(relation_follows, 5)
+
+ # Reflects new description and lag
+ expect(relation_row).to have_text("Discovered relations have descriptions!")
+ expect(relation_row).to have_text("5 days")
+
+ # Unchanged
+ tabs.expect_counter("relations", 4)
+
+ # Edit again
+ relations_tab.edit_relation_description(relation_follows, "And they can be edited!")
+
+ # Reflects new description
+ expect(relation_row).to have_text("And they can be edited!")
+
+ # Unchanged
+ tabs.expect_counter("relations", 4)
+ end
+
+ it "does not have an edit action for children" do
+ scroll_to_element relations_panel
+
+ child_row = relations_panel.find("[data-test-selector='op-relation-row-#{child_wp.id}']")
+
+ within(child_row) do
+ page.find("[data-test-selector='op-relation-row-#{child_wp.id}-action-menu']").click
+ expect(page).to have_no_css("[data-test-selector='op-relation-row-#{child_wp.id}-edit-button']")
+ end
+ end
+
+ it "does not show the lag field for all relation types" do
+ scroll_to_element relations_panel
+
+ relations_tab.open_relation_dialog(relation_relates)
+
+ within "##{WorkPackageRelationsTab::WorkPackageRelationDialogComponent::DIALOG_ID}" do
+ expect(page).to have_field("Work package", readonly: true)
+ expect(page).to have_no_field("Lag")
+ end
+ end
+
+ context "with the shown WorkPackage being the 'to' relation part" do
+ let(:another_wp) { create(:work_package, type: type2, subject: "related to main") }
+
+ let(:relation_to) do
+ create(:relation,
+ from: another_wp,
+ to: work_package,
+ relation_type: Relation::TYPE_FOLLOWS)
+ end
+
+ it "shows the correct related WorkPackage in the dialog (regression #59771)" do
+ scroll_to_element relations_panel
+
+ relations_tab.open_relation_dialog(relation_to)
+
+ within "##{WorkPackageRelationsTab::WorkPackageRelationDialogComponent::DIALOG_ID}" do
+ expect(page).to have_field("Work package",
+ readonly: true,
+ with: "#{another_wp.type.name.upcase} ##{another_wp.id} - #{another_wp.subject}")
+ end
+ end
+ end
+ end
+
+ describe "creating a relation" do
+ let(:wp_successor) { create(:work_package, type: type1, subject: "successor of main") }
+
+ it "renders the new relation form for the selected type and creates the relation" do
+ scroll_to_element relations_panel
+
+ relations_tab.add_relation(type: :precedes, relatable: wp_successor,
+ description: "Discovered relations have descriptions!")
+ relations_tab.expect_relation(wp_successor)
+
+ # Bumped by one
+ tabs.expect_counter("relations", 5)
+ # Relation is created
+ expect(Relation.follows.where(from: wp_successor, to: work_package)).to exist
+ end
+
+ it "does not autocomplete unrelatable work packages" do
+ # wp_predecessor is already related to work_package as relation_follows
+ # in a predecessor relation, so it should not be autocompleteable anymore
+ # under the "Predecessor (before)" type
+ scroll_to_element relations_panel
+
+ relations_panel.find("[data-test-selector='new-relation-action-menu']").click
+
+ within page.find_by_id("new-relation-action-menu-list") do # Primer appends "list" to the menu id automatically
+ click_link_or_button "Predecessor (before)"
+ end
+
+ wait_for_reload
+
+ within "##{WorkPackageRelationsTab::WorkPackageRelationFormComponent::DIALOG_ID}" do
+ expect(page).to have_text("Add predecessor (before)")
+
+ autocomplete_field = page.find("[data-test-selector='work-package-relation-form-to-id']")
+ search_autocomplete(autocomplete_field,
+ query: wp_predecessor.subject,
+ results_selector: "body")
+ expect_no_ng_option(autocomplete_field,
+ wp_predecessor.subject,
+ results_selector: "body")
+ end
+ end
+ end
+
+ describe "attaching a child" do
+ it "renders the new child form and creates the child relationship" do
+ scroll_to_element relations_panel
+
+ tabs.expect_counter("relations", 4)
+
+ relations_tab.add_existing_child(not_yet_child_wp)
+ relations_tab.expect_child(not_yet_child_wp)
+
+ # Bumped by one
+ tabs.expect_counter("relations", 5)
+ end
+ end
+
+ describe "with limited permissions" do
+ let(:no_permissions_role) { create(:project_role, permissions: %i[view_work_packages]) }
+ let(:user_without_permissions) do
+ create(:user,
+ member_with_roles: { project => no_permissions_role })
+ end
+ let(:current_user) { user_without_permissions }
+
+ it "does not show options to add or edit relations" do
+ scroll_to_element relations_panel
+
+ tabs.expect_counter("relations", 4)
+
+ relations_tab.expect_no_add_relation_button
+ relations_tab.expect_no_relatable_action_menu(wp_related)
+ relations_tab.expect_no_relatable_action_menu(child_wp)
+ end
+
+ context "with manage_relations permissions" do
+ let(:no_permissions_role) do
+ create(:project_role, permissions: %i(view_work_packages edit_work_packages manage_work_package_relations))
+ end
+
+ it "does not show the option to delete the child" do
+ scroll_to_element relations_panel
+
+ tabs.expect_counter("relations", 4)
+
+ # The menu is shown as the user can add a relation
+ relations_tab.expect_add_relation_button
+
+ # The relation can be edited and deleted
+ relations_tab.expect_relatable_action_menu(wp_related)
+ relations_tab.relatable_action_menu(wp_related).click
+ relations_tab.expect_relatable_delete_button(wp_related)
+
+ # The child cannot be changed
+ relations_tab.expect_no_relatable_action_menu(child_wp)
+ end
+ end
+
+ context "with manage_subtasks permissions" do
+ let(:no_permissions_role) { create(:project_role, permissions: %i(view_work_packages edit_work_packages manage_subtasks)) }
+
+ it "does not show the option to edit the relation but only the child" do
+ scroll_to_element relations_panel
+
+ tabs.expect_counter("relations", 4)
+
+ # The menu is shown as the user can add a child
+ relations_tab.expect_add_relation_button
+
+ # The relation cannot be edited
+ relations_tab.expect_no_relatable_action_menu(wp_related)
+
+ # The child can be removed
+ relations_tab.expect_relatable_action_menu(child_wp)
+ relations_tab.relatable_action_menu(child_wp).click
+ relations_tab.expect_relatable_delete_button(child_wp)
+ end
+ end
+ end
+end
diff --git a/spec/features/work_packages/details/relations/relations_spec.rb b/spec/features/work_packages/details/relations/relations_spec.rb
deleted file mode 100644
index ba513eb55ce9..000000000000
--- a/spec/features/work_packages/details/relations/relations_spec.rb
+++ /dev/null
@@ -1,293 +0,0 @@
-#-- copyright
-# OpenProject is an open source project management software.
-# Copyright (C) the OpenProject GmbH
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License version 3.
-#
-# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-# Copyright (C) 2006-2013 Jean-Philippe Lang
-# Copyright (C) 2010-2013 the ChiliProject Team
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-#
-# See COPYRIGHT and LICENSE files for more details.
-#++
-
-require "spec_helper"
-
-RSpec.shared_examples "Work package relations tab", :js, :selenium do
- include_context "ng-select-autocomplete helpers"
-
- let(:user) { create(:admin) }
-
- let(:project) { create(:project) }
- let(:work_package) { create(:work_package, project:) }
- let(:full_wp) { Pages::FullWorkPackage.new(work_package) }
- let(:relations) { Components::WorkPackages::Relations.new(work_package) }
-
- let(:visit) { true }
-
- before do
- login_as user
-
- if visit
- visit_relations
- end
- end
-
- def visit_relations
- work_packages_page.visit_tab!("relations")
- expect_angular_frontend_initialized
- work_packages_page.expect_subject
- loading_indicator_saveguard
- end
-
- describe "relation group-by toggler" do
- let(:project) { create(:project, types: [type1, type2]) }
- let(:type1) { create(:type) }
- let(:type2) { create(:type) }
-
- let(:to1) { create(:work_package, type: type1, project:) }
- let(:to2) { create(:work_package, type: type2, project:) }
-
- let!(:relation1) do
- create(:relation,
- from: work_package,
- to: to1,
- relation_type: Relation::TYPE_FOLLOWS)
- end
- let!(:relation2) do
- create(:relation,
- from: work_package,
- to: to2,
- relation_type: Relation::TYPE_RELATES)
- end
-
- let(:toggle_btn_selector) { "#wp-relation-group-by-toggle" }
- let(:visit) { false }
-
- before do
- visit_relations
-
- work_packages_page.visit_tab!("relations")
- work_packages_page.expect_subject
- loading_indicator_saveguard
-
- scroll_to_element find(".detail-panel--relations")
- end
-
- it "allows to toggle how relations are grouped" do
- # Expect to be grouped by relation type by default
- expect(page).to have_selector(toggle_btn_selector,
- text: "Group by work package type", wait: 20)
-
- expect(page).to have_css(".relation-group--header", text: "FOLLOWS")
- expect(page).to have_css(".relation-group--header", text: "RELATED TO")
-
- expect(page).to have_css(".relation-row--type", text: type1.name.upcase)
- expect(page).to have_css(".relation-row--type", text: type2.name.upcase)
-
- find(toggle_btn_selector).click
- expect(page).to have_selector(toggle_btn_selector, text: "Group by relation type", wait: 10)
-
- expect(page).to have_css(".relation-group--header", text: type1.name.upcase)
- expect(page).to have_css(".relation-group--header", text: type2.name.upcase)
-
- expect(page).to have_css(".relation-row--type", text: "Follows")
- expect(page).to have_css(".relation-row--type", text: "Related To")
- end
-
- it "allows to edit relation types when toggled" do
- find(toggle_btn_selector).click
- expect(page).to have_selector(toggle_btn_selector, text: "Group by relation type", wait: 20)
-
- # Expect current to be follows and other one related
- expect(page).to have_css(".relation-row--type", text: "Follows")
- expect(page).to have_css(".relation-row--type", text: "Related To")
-
- # edit to blocks
- relations.edit_relation_type(to1, to_type: "Blocks")
-
- # the other one should not be altered
- expect(page).to have_css(".relation-row--type", text: "Blocks")
- expect(page).to have_css(".relation-row--type", text: "Related To")
-
- updated_relation = Relation.find(relation1.id)
- expect(updated_relation.relation_type).to eq("blocks")
- expect(updated_relation.from_id).to eq(work_package.id)
- expect(updated_relation.to_id).to eq(to1.id)
-
- relations.edit_relation_type(to1, to_type: "Blocked by")
-
- expect(page).to have_css(".relation-row--type", text: "Blocked by")
- expect(page).to have_css(".relation-row--type", text: "Related To")
-
- updated_relation = Relation.find(relation1.id)
- expect(updated_relation.relation_type).to eq("blocks")
- expect(updated_relation.from_id).to eq(to1.id)
- expect(updated_relation.to_id).to eq(work_package.id)
- end
- end
-
- describe "with limited permissions" do
- let(:permissions) { %i(view_work_packages) }
- let(:user_role) do
- create(:project_role, permissions:)
- end
-
- let(:user) do
- create(:user,
- member_with_roles: { project => user_role })
- end
-
- context "as view-only user, with parent set" do
- let(:work_package) { create(:work_package, project:) }
-
- it "shows no links to create relations" do
- # No create buttons should exist
- expect(page).to have_no_css(".wp-relations-create-button")
-
- # Test for add relation
- expect(page).to have_no_css("#relation--add-relation")
- end
- end
-
- context "with relations permissions" do
- let(:permissions) do
- %i(view_work_packages add_work_packages manage_subtasks manage_work_package_relations)
- end
-
- let!(:relatable) { create(:work_package, project:) }
-
- it "allows to manage relations" do
- relations.add_relation(type: "follows", to: relatable)
-
- # Relations counter badge should increase number of relations
- tabs.expect_counter(relations_tab, 1)
-
- relations.remove_relation(relatable)
- expect(page).to have_no_css(".relation-group--header", text: "FOLLOWS")
-
- # If there are no relations, the counter badge should not be displayed
- tabs.expect_no_counter(relations_tab)
-
- work_package.reload
- expect(work_package.relations).to be_empty
- end
-
- it "allows to move between split and full view (Regression #24194)" do
- relations.add_relation(type: "follows", to: relatable)
- # Relations counter should increase
- tabs.expect_counter(relations_tab, 1)
-
- # Switch to full view
- work_packages_page.switch_to_fullscreen
-
- # Expect to have row
- relations.hover_action(relatable, :delete)
-
- expect(page).to have_no_css(".relation-group--header", text: "FOLLOWS")
- expect(page).to have_no_css(".wp-relations--subject-field", text: relatable.subject)
-
- # Back to split view
- page.execute_script("window.history.back()")
- work_packages_page.expect_subject
-
- expect(page).to have_no_css(".relation-group--header", text: "FOLLOWS")
- expect(page).to have_no_css(".wp-relations--subject-field", text: relatable.subject)
- end
-
- it "follows the relation links (Regression #26794)" do
- relations.add_relation(type: "follows", to: relatable)
-
- relations.click_relation(relatable)
- subject = full_wp.edit_field(:subject)
- subject.expect_state_text relatable.subject
-
- relations.click_relation(work_package)
- subject = full_wp.edit_field(:subject)
- subject.expect_state_text work_package.subject
- end
-
- it "allows to change relation descriptions" do
- relations.add_relation(type: "follows", to: relatable)
-
- ## Toggle description
- relations.hover_action(relatable, :info)
-
- # Open textarea
- created_row = relations.find_row(relatable)
- created_row.find(".wp-relation--description-read-value.-placeholder",
- text: I18n.t("js.placeholders.relation_description")).click
-
- expect(page).to have_focus_on(".wp-relation--description-textarea")
- textarea = created_row.find(".wp-relation--description-textarea")
- textarea.set "my description!"
-
- # Save description
- created_row.find(".inplace-edit--control--save").click
-
- loading_indicator_saveguard
-
- # Wait for the relations table to be present
- sleep 2
- expect(page).to have_test_selector("op-relation--row-subject")
-
- scroll_to_element find(".detail-panel--relations")
-
- ## Toggle description again
- retry_block do
- relations.hover_action(relatable, :info)
- created_row = relations.find_row(relatable)
-
- find ".wp-relation--description-read-value"
- end
-
- created_row.find(".wp-relation--description-read-value",
- text: "my description!").click
-
- # Cancel edition
- created_row.find(".inplace-edit--control--cancel").click
- created_row.find(".wp-relation--description-read-value",
- text: "my description!").click
-
- relation = work_package.relations.first
- expect(relation.description).to eq("my description!")
-
- # Toggle to close
- relations.hover_action(relatable, :info)
- expect(created_row).to have_no_css(".wp-relation--description-read-value")
- end
- end
- end
-end
-
-RSpec.context "within a split screen" do
- let(:work_packages_page) { Pages::SplitWorkPackage.new(work_package) }
- let(:tabs) { Components::WorkPackages::Tabs.new(work_package) }
-
- let(:relations_tab) { find(".op-tab-row--link_selected", text: "RELATIONS") }
-
- it_behaves_like "Work package relations tab"
-end
-
-RSpec.context "within a primerized split screen" do
- let(:work_packages_page) { Pages::PrimerizedSplitWorkPackage.new(work_package) }
- let(:tabs) { Components::WorkPackages::PrimerizedTabs.new }
- let(:relations_tab) { "relations" }
-
- it_behaves_like "Work package relations tab"
-end
diff --git a/spec/features/work_packages/generate_pdf_spec.rb b/spec/features/work_packages/generate_pdf_spec.rb
new file mode 100644
index 000000000000..eae07690cd06
--- /dev/null
+++ b/spec/features/work_packages/generate_pdf_spec.rb
@@ -0,0 +1,137 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+require "features/work_packages/work_packages_page"
+
+RSpec.describe "work package PDF generator", :js, :selenium, with_flag: { generate_pdf_from_work_package: true } do
+ let(:user) { create(:admin) }
+ let(:project) { create(:project) }
+ let(:default_footer_text) { work_package.subject }
+ let(:default_header_text) { "#{work_package.type} ##{work_package.id}" }
+ let(:expected_params) do
+ default_expected_params
+ end
+ let(:default_expected_params) do
+ {
+ header_text_right: default_header_text,
+ footer_text_center: default_footer_text,
+ hyphenation: "",
+ paper_size: "A4"
+ }
+ end
+ let(:download_list) do
+ DownloadList.new
+ end
+ let(:work_package) do
+ build(:work_package,
+ project:,
+ id: 666,
+ assigned_to: user,
+ responsible: user)
+ end
+ let(:document_generator) { instance_double(WorkPackage::PDFExport::DocumentGenerator) }
+
+ RSpec::Matchers.define :has_mandatory_params do |expected|
+ match do |actual|
+ expected.count do |key, value|
+ actual[key.to_sym] == value
+ end == expected.size
+ end
+ end
+
+ def visit_work_package_page!
+ wp_page = Pages::FullWorkPackage.new(work_package)
+ wp_page.visit!
+ end
+
+ def mock_generating_pdf
+ allow(WorkPackage::PDFExport::DocumentGenerator)
+ .to receive(:new)
+ .and_return(document_generator)
+ allow(document_generator)
+ .to receive(:initialize)
+ .with(work_package, has_mandatory_params(expected_params))
+ allow(document_generator)
+ .to receive(:export!)
+ .and_return(
+ Exports::Result.new(
+ format: :pdf, title: "filename.pdf", content: "PDF Content", mime_type: "application/pdf"
+ )
+ )
+ end
+
+ def open_generate_pdf_dialog!
+ click_link_or_button "More"
+ click_link_or_button "Generate PDF"
+ end
+
+ def generate!
+ click_link_or_button "Generate"
+ expect(subject).to have_text("filename.pdf")
+ end
+
+ before do
+ mock_generating_pdf
+ login_as(user)
+ work_package.save!
+ visit_work_package_page!
+ open_generate_pdf_dialog!
+ end
+
+ after do
+ DownloadList.clear
+ end
+
+ subject { download_list.refresh_from(page).latest_download.to_s }
+
+ context "with default parameters" do
+ it "downloads with options" do
+ generate!
+ end
+ end
+
+ context "with custom parameters" do
+ let(:expected_params) do
+ default_expected_params.merge({
+ header_text_right: "Custom Header Text",
+ footer_text_center: "Custom Footer Text",
+ hyphenation: "de",
+ paper_size: "LETTER"
+ })
+ end
+
+ it "downloads with options" do
+ select "Letter", from: "paper_size"
+ select "Deutsch", from: "hyphenation"
+ fill_in "header_text_right", with: "Custom Header Text"
+ fill_in "footer_text_center", with: "Custom Footer Text"
+ generate!
+ end
+ end
+end
diff --git a/spec/features/work_packages/new/new_work_package_spec.rb b/spec/features/work_packages/new/new_work_package_spec.rb
index 88b8b70ec960..00103ff2e120 100644
--- a/spec/features/work_packages/new/new_work_package_spec.rb
+++ b/spec/features/work_packages/new/new_work_package_spec.rb
@@ -509,24 +509,5 @@ def create_work_package_globally(type, project_name)
subject_field = wp_page_create.edit_field(:subject)
subject_field.expect_value "My subtask"
end
-
- it "from the relations tab" do
- wp_page.visit_tab!("relations")
-
- click_button("Create new child")
-
- subject = EditField.new wp_page, :subject
- subject.set_value "Child"
- subject.submit_by_enter
-
- wp_page.expect_and_dismiss_toaster(message: I18n.t("js.notice_successful_create"))
-
- # Move to the newly created child
- wp_page.find("wp-children-query tbody.results-tbody tr").double_click
-
- wp_page.expect_attributes(combinedDate: "#{parent.start_date.strftime('%m/%d/%Y')} - #{parent.due_date.strftime('%m/%d/%Y')}")
-
- expect(wp_page).to have_test_selector("op-wp-breadcrumb", text: "Parent:\n#{parent.subject}")
- end
end
end
diff --git a/spec/features/work_packages/table/queries/hierarchy_cf_filter_spec.rb b/spec/features/work_packages/table/queries/hierarchy_cf_filter_spec.rb
new file mode 100644
index 000000000000..fdcaf803acd4
--- /dev/null
+++ b/spec/features/work_packages/table/queries/hierarchy_cf_filter_spec.rb
@@ -0,0 +1,122 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe "Work package filtering by hierarchy custom field", :js do
+ let(:project) { create(:project) }
+ let(:type) { project.types.first }
+ let(:wp_table) { Pages::WorkPackagesTable.new(project) }
+ let(:filters) { Components::WorkPackages::Filters.new }
+ let(:hierarchy_root) { create(:hierarchy_item) }
+ let!(:hierarchy_cf) do
+ create(:hierarchy_wp_custom_field, hierarchy_root:).tap do |cf|
+ type.custom_fields << cf
+ project.work_package_custom_fields << cf
+ end
+ end
+ let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
+ let!(:luke) { service.insert_item(parent: hierarchy_root, label: "luke").value! }
+ let!(:leia) { service.insert_item(parent: hierarchy_root, label: "leia").value! }
+
+ let!(:wp_luke) do
+ create(:work_package, project:, subject: "Luke's wp").tap do |wp|
+ wp.custom_field_values = { hierarchy_cf.id => luke.id }
+ wp.save!
+ end
+ end
+ let!(:wp_leia) do
+ create(:work_package, project:, subject: "Leia's wp").tap do |wp|
+ wp.custom_field_values = { hierarchy_cf.id => leia.id }
+ wp.save!
+ end
+ end
+
+ let(:admin) { create(:admin) }
+
+ current_user { admin }
+
+ describe "filters" do
+ before do
+ wp_table.visit!
+ wp_table.expect_work_package_listed(wp_luke, wp_leia)
+ end
+
+ context "when any" do
+ it "shows the work package matching the hierarchy cf filter" do
+ filters.open
+
+ # Filtering by hierarchy (=)
+
+ filters.add_filter_by(hierarchy_cf.name, "is", [luke.label], hierarchy_cf.attribute_name(:camel_case))
+
+ wp_table.ensure_work_package_not_listed!(wp_leia)
+ wp_table.expect_work_package_listed(wp_luke)
+ end
+ end
+
+ context "when is not" do
+ it "shows work packages that do not match the hierarchy cf filter" do
+ filters.open
+
+ # Filtering by hierarchy (!)
+
+ filters.add_filter_by(hierarchy_cf.name, "is not", [luke.label], hierarchy_cf.attribute_name(:camel_case))
+
+ wp_table.ensure_work_package_not_listed!(wp_luke)
+ wp_table.expect_work_package_listed(wp_leia)
+ end
+ end
+
+ context "when equals with descendants" do
+ let!(:grogu) { service.insert_item(parent: luke, label: "Grogu").value! }
+ let!(:wp_grogu) do
+ create(:work_package, project:, subject: "Grogu's wp").tap do |wp|
+ wp.custom_field_values = { hierarchy_cf.id => grogu.id }
+ wp.save!
+ end
+ end
+
+ it "shows the work packages with associated hierarchy items to the branch" do
+ filters.open
+
+ # Filtering by hierarchy (!)
+
+ filters.add_filter_by(
+ hierarchy_cf.name,
+ "is any with descendants",
+ [luke.label],
+ hierarchy_cf.attribute_name(:camel_case)
+ )
+
+ wp_table.ensure_work_package_not_listed!(wp_leia)
+ wp_table.expect_work_package_listed(wp_luke, wp_grogu)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/colors_helper_spec.rb b/spec/helpers/colors_helper_spec.rb
new file mode 100644
index 000000000000..b5c0236cd221
--- /dev/null
+++ b/spec/helpers/colors_helper_spec.rb
@@ -0,0 +1,45 @@
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+# ++
+
+require "spec_helper"
+
+RSpec.describe ColorsHelper do
+ let(:model) { Data.define(:id).new(5) }
+
+ describe "#hl_inline_class" do
+ it "returns the correct class name" do
+ expect(helper.hl_inline_class("foo_bar", model)).to eq("__hl_inline_foo_bar_5")
+ end
+ end
+
+ describe "#hl_background_class" do
+ it "returns the correct class name" do
+ expect(helper.hl_background_class("foo_bar", model)).to eq("__hl_background_foo_bar_5")
+ end
+ end
+end
diff --git a/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb b/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb
index 8f5127204a9b..c8d21fde89e2 100644
--- a/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb
+++ b/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb
@@ -33,6 +33,7 @@
shared_let(:project) { create(:project) }
shared_let(:work_package) { create(:work_package, project:, author: user) }
shared_let(:meeting) { create(:meeting, project:, author: user) }
+ shared_let(:notifications) { create_list(:notification, 3, recipient: user, resource: work_package) }
describe ".wrap" do
it "returns wrapped journals with relations eager loaded" do
@@ -42,9 +43,10 @@
wrapped_journals = described_class.wrap(journals)
expect(wrapped_journals.size).to eq(journals.size)
+ expected_associations_cached = %i[journable data]
wrapped_journals.each do |loaded_journal|
- expect(loaded_journal.__getobj__.instance_variables).to include(:@predecessor)
- %i[journable data].each do |association|
+ expect(loaded_journal.__getobj__.instance_variables).to include(:@predecessor, :@unread_notifications)
+ expected_associations_cached.each do |association|
expect(loaded_journal.association_cached?(association)).to be true
end
end
diff --git a/spec/lib/api/v3/custom_fields/hierarchy/hierarchy_item_representer_rendering_spec.rb b/spec/lib/api/v3/custom_fields/hierarchy/hierarchy_item_representer_rendering_spec.rb
index 933145199a6b..4a33d86b16e8 100644
--- a/spec/lib/api/v3/custom_fields/hierarchy/hierarchy_item_representer_rendering_spec.rb
+++ b/spec/lib/api/v3/custom_fields/hierarchy/hierarchy_item_representer_rendering_spec.rb
@@ -121,7 +121,7 @@
it_behaves_like "has a titled link" do
let(:link) { "self" }
let(:href) { api_v3_paths.custom_field_item(item.id) }
- let(:title) { item.label }
+ let(:title) { "#{item.label} (#{item.short})" }
end
it_behaves_like "has a titled link" do
@@ -171,7 +171,7 @@
it_behaves_like "has a titled link" do
let(:link) { "self" }
let(:href) { api_v3_paths.custom_field_item(item.id) }
- let(:title) { item.label }
+ let(:title) { "#{item.label} (#{item.short})" }
end
it_behaves_like "has a titled link" do
diff --git a/spec/lib/api/v3/queries/schemas/filter_dependency_representer_factory_spec.rb b/spec/lib/api/v3/queries/schemas/filter_dependency_representer_factory_spec.rb
index 767dbd11edcf..d57d262df4f0 100644
--- a/spec/lib/api/v3/queries/schemas/filter_dependency_representer_factory_spec.rb
+++ b/spec/lib/api/v3/queries/schemas/filter_dependency_representer_factory_spec.rb
@@ -157,6 +157,17 @@
it_behaves_like "includes the cf json_cache_key mixin"
end
+ describe "type hierarchy" do
+ let(:hierarchy_root) { build_stubbed(:hierarchy_item) }
+ let(:custom_field) { build_stubbed(:custom_field, field_format: "hierarchy", hierarchy_root:) }
+
+ it "is the hierarchy dependency" do
+ expect(subject).to be_a(API::V3::Queries::Schemas::HierarchyFilterDependencyRepresenter)
+ end
+
+ it_behaves_like "includes the cf json_cache_key mixin"
+ end
+
context "type date" do
let(:custom_field) { build_stubbed(:date_wp_custom_field) }
diff --git a/spec/lib/api/v3/queries/schemas/hierarchy_filter_dependency_representer_spec.rb b/spec/lib/api/v3/queries/schemas/hierarchy_filter_dependency_representer_spec.rb
new file mode 100644
index 000000000000..33ac00104897
--- /dev/null
+++ b/spec/lib/api/v3/queries/schemas/hierarchy_filter_dependency_representer_spec.rb
@@ -0,0 +1,106 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe API::V3::Queries::Schemas::HierarchyFilterDependencyRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:project) { build_stubbed(:project) }
+ let(:query) { build_stubbed(:query, project:) }
+ let(:hierarchy_cf) { build_stubbed(:custom_field, field_format: "hierarchy", hierarchy_root:) }
+ let(:hierarchy_root) { build_stubbed(:hierarchy_item) }
+ let(:filter) do
+ Queries::WorkPackages::Filter::CustomFieldFilter.from_custom_field!(
+ custom_field: hierarchy_cf,
+ context: query
+ )
+ end
+ let(:form_embedded) { false }
+
+ let(:instance) { described_class.new(filter, operator, form_embedded:) }
+
+ subject(:generated) { instance.to_json }
+
+ describe "generation" do
+ describe "properties" do
+ describe "values" do
+ let(:path) { "values" }
+ let(:type) { "[]CustomField::Hierarchy::Item" }
+ let(:href) { api_v3_paths.custom_field_items(hierarchy_cf.id) }
+
+ context "for operator 'Queries::Operators::Equals'" do
+ let(:operator) { Queries::Operators::Equals }
+
+ it_behaves_like "filter dependency with allowed link"
+ end
+
+ context "for operator 'Queries::Operators::NotEquals'" do
+ let(:operator) { Queries::Operators::NotEquals }
+
+ it_behaves_like "filter dependency with allowed link"
+ end
+ end
+ end
+
+ describe "caching" do
+ let(:operator) { Queries::Operators::Equals }
+
+ before do
+ # fill the cache
+ instance.to_json
+
+ allow(instance).to receive(:to_hash).and_call_original
+ end
+
+ it "is cached" do
+ instance.to_json
+ expect(instance).not_to have_received(:to_hash)
+ end
+
+ it "busts the cache on a different operator" do
+ instance.send(:operator=, Queries::Operators::NotEquals)
+ instance.to_json
+ expect(instance).to have_received(:to_hash)
+ end
+
+ it "busts the cache on changes to the locale" do
+ I18n.with_locale(:de) do
+ instance.to_json
+ end
+ expect(instance).to have_received(:to_hash)
+ end
+
+ it "busts the cache on different form_embedded" do
+ instance.send(:form_embedded=, true)
+ instance.to_json
+ expect(instance).to have_received(:to_hash)
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
index fe62052c5c99..ad8e3533b04e 100644
--- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
+++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
@@ -1267,7 +1267,8 @@
end
end
- describe "fileLinks" do
+ describe "fileLinks",
+ skip: "test setup broken - remove embedding with https://community.openproject.org/wp/59468" do
let(:storage) { build_stubbed(:nextcloud_storage) }
let(:file_link) { build_stubbed(:file_link, storage:, container: work_package) }
diff --git a/spec/migrations/remove_totals_from_childless_work_packages_spec.rb b/spec/migrations/remove_totals_from_childless_work_packages_spec.rb
index b181c3189825..956f7efe5e96 100644
--- a/spec/migrations/remove_totals_from_childless_work_packages_spec.rb
+++ b/spec/migrations/remove_totals_from_childless_work_packages_spec.rb
@@ -69,7 +69,7 @@
end
it "removes totals from childless work packages" do
- expect_work_packages(table_work_packages.map(&:reload), <<~TABLE)
+ expect_work_packages_after_reload(table_work_packages, <<~TABLE)
hierarchy | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
wp totals set | 5h | 3h | 40% | | |
wp only pc set | | | 60% | | |
diff --git a/spec/migrations/update_progress_calculation_spec.rb b/spec/migrations/update_progress_calculation_spec.rb
index 650df99e27ff..bff119408157 100644
--- a/spec/migrations/update_progress_calculation_spec.rb
+++ b/spec/migrations/update_progress_calculation_spec.rb
@@ -55,8 +55,7 @@ def expect_migrates(from:, to:)
run_migration
- table.work_packages.map(&:reload)
- expect_work_packages(table.work_packages, to)
+ expect_work_packages_after_reload(table.work_packages, to)
table.work_packages
end
@@ -829,7 +828,7 @@ def expect_migrates(from:, to:)
end
it "fixes the total values and sets ∑ % complete to nil (not 0) and keeps % complete (unless wrong)" do
- expect_work_packages(table_work_packages.map(&:reload), <<~TABLE)
+ expect_work_packages_after_reload(table_work_packages, <<~TABLE)
subject | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete |
wp zero | | | 0 | | | |
wp correct | 100h | 50h | 50 | 100h | 50h | 50 |
diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb
index cb061fcbab00..91b249efa187 100644
--- a/spec/models/attachment_spec.rb
+++ b/spec/models/attachment_spec.rb
@@ -295,6 +295,17 @@
end
end
+ describe "for an HTML file" do
+ let(:attachment) { described_class.new }
+
+ it "assumes it not to be inlineable" do
+ attachment.content_type = "text/html"
+ expect(attachment).to be_is_html
+ expect(attachment).not_to be_is_text
+ expect(attachment).not_to be_inlineable
+ end
+ end
+
describe "for a binary file" do
before { binary_attachment.save! }
diff --git a/spec/models/custom_actions/actions/custom_field_spec.rb b/spec/models/custom_actions/actions/custom_field_spec.rb
index e8cea930e9ab..05c68c594384 100644
--- a/spec/models/custom_actions/actions/custom_field_spec.rb
+++ b/spec/models/custom_actions/actions/custom_field_spec.rb
@@ -98,7 +98,7 @@
describe ".all" do
before do
allow(WorkPackageCustomField)
- .to receive(:order)
+ .to receive(:usable_as_custom_action)
.and_return(custom_fields)
end
diff --git a/spec/models/custom_field/order_statements_spec.rb b/spec/models/custom_field/order_statements_spec.rb
new file mode 100644
index 000000000000..79f724ee1fd8
--- /dev/null
+++ b/spec/models/custom_field/order_statements_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe CustomField::OrderStatements do
+ # integration tests at spec/models/query/results_cf_sorting_integration_spec.rb
+ context "when hierarchy" do
+ subject(:custom_field) { create(:hierarchy_wp_custom_field) }
+
+ describe "#order_statement" do
+ it { expect(subject.order_statement).to eq("cf_order_#{custom_field.id}.value") }
+ end
+
+ describe "#order_join_statement" do
+ it do
+ expect(custom_field.order_join_statement).to eq(
+ <<-SQL.squish
+ LEFT OUTER JOIN (
+ SELECT DISTINCT ON (cv.customized_id) cv.customized_id , item.label "value"
+ FROM "custom_values" cv
+ INNER JOIN "hierarchical_items" item ON item.id = cv.value::bigint
+ WHERE cv.customized_type = 'WorkPackage'
+ AND cv.custom_field_id = #{custom_field.id}
+ AND cv.value IS NOT NULL
+ AND cv.value != ''
+ ORDER BY cv.customized_id, cv.id
+ ) cf_order_#{custom_field.id} ON cf_order_#{custom_field.id}.customized_id = "work_packages".id
+ SQL
+ )
+ end
+ end
+ end
+end
diff --git a/spec/models/project/gate_definition_spec.rb b/spec/models/project/gate_definition_spec.rb
new file mode 100644
index 000000000000..7d69be5a91ef
--- /dev/null
+++ b/spec/models/project/gate_definition_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "rails_helper"
+require "support/shared/project_life_cycle_helpers"
+
+RSpec.describe Project::GateDefinition do
+ it_behaves_like "a Project::LifeCycleStepDefinition event"
+
+ describe "associations" do
+ it "has many gates" do
+ expect(subject).to have_many(:gates).class_name("Project::Gate")
+ .with_foreign_key(:definition_id)
+ .inverse_of(:definition)
+ .dependent(:destroy)
+ end
+ end
+end
diff --git a/spec/models/project/gate_spec.rb b/spec/models/project/gate_spec.rb
new file mode 100644
index 000000000000..f634c63c1836
--- /dev/null
+++ b/spec/models/project/gate_spec.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "rails_helper"
+require "support/shared/project_life_cycle_helpers"
+
+RSpec.describe Project::Gate do
+ it_behaves_like "a Project::LifeCycleStep event"
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:date) }
+ it { is_expected.to validate_inclusion_of(:type).in_array(["Project::Gate"]).with_message(:must_be_a_gate) }
+
+ it "is invalid if `end_date` is present" do
+ subject.end_date = Time.zone.today
+
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base])
+ .to include("Cannot assign `end_date` to a Project::Gate")
+ end
+
+ it "is valid if `end_date` is not present" do
+ valid_gate = build(:project_gate, end_date: nil)
+ expect(valid_gate).to be_valid
+ end
+ end
+end
diff --git a/app/models/work_package/pdf_export/attachments.rb b/spec/models/project/life_cycle_step_definition_spec.rb
similarity index 67%
rename from app/models/work_package/pdf_export/attachments.rb
rename to spec/models/project/life_cycle_step_definition_spec.rb
index 7caf55ec0257..6251817bb2ef 100644
--- a/app/models/work_package/pdf_export/attachments.rb
+++ b/spec/models/project/life_cycle_step_definition_spec.rb
@@ -26,29 +26,21 @@
# See COPYRIGHT and LICENSE files for more details.
#++
-require "mini_magick"
+require "rails_helper"
-module WorkPackage::PDFExport::Attachments
- def resize_image(file_path)
- tmp_file = Tempfile.new(["temp_image", File.extname(file_path)])
- @resized_images = [] if @resized_images.nil?
-
- @resized_images << tmp_file
- resized_file_path = tmp_file.path
-
- image = MiniMagick::Image.open(file_path)
- image.resize("x800>")
- image.write(resized_file_path)
-
- resized_file_path
+RSpec.describe Project::LifeCycleStepDefinition do
+ it "cannot be instantiated" do
+ expect { described_class.new }.to raise_error(NotImplementedError)
end
- def pdf_embeddable?(content_type)
- %w[image/jpeg image/png].include?(content_type)
- end
+ context "with a Project::StageDefinition" do
+ subject { create :project_stage_definition }
- def delete_all_resized_images
- @resized_images&.each(&:close!)
- @resized_images = []
+ it { is_expected.to have_readonly_attribute(:type) }
end
+
+ # For more specs see:
+ # - spec/support/shared/project_life_cycle_helpers.rb
+ # - spec/models/project/gate_definition_spec.rb
+ # - spec/models/project/stage_definition_spec.rb
end
diff --git a/spec/models/project/life_cycle_step_spec.rb b/spec/models/project/life_cycle_step_spec.rb
new file mode 100644
index 000000000000..03722a2cb66a
--- /dev/null
+++ b/spec/models/project/life_cycle_step_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "rails_helper"
+
+RSpec.describe Project::LifeCycleStep do
+ it "cannot be instantiated" do
+ expect { described_class.new }.to raise_error(NotImplementedError)
+ end
+
+ describe "with an instantiated Gate" do
+ subject { build :project_gate }
+
+ it { is_expected.to have_readonly_attribute(:definition_id) }
+ it { is_expected.to have_readonly_attribute(:type) }
+ end
+
+ # For more specs see:
+ # - spec/support/shared/project_life_cycle_helpers.rb
+ # - spec/models/project/gate_spec.rb
+ # - spec/models/project/stage_spec.rb
+end
diff --git a/spec/models/project/stage_definition_spec.rb b/spec/models/project/stage_definition_spec.rb
new file mode 100644
index 000000000000..f642056d4a34
--- /dev/null
+++ b/spec/models/project/stage_definition_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "rails_helper"
+require "support/shared/project_life_cycle_helpers"
+
+RSpec.describe Project::StageDefinition do
+ it_behaves_like "a Project::LifeCycleStepDefinition event"
+
+ describe "associations" do
+ it "has many stages" do
+ expect(subject).to have_many(:stages).class_name("Project::Stage")
+ .with_foreign_key(:definition_id)
+ .inverse_of(:definition)
+ .dependent(:destroy)
+ end
+ end
+end
diff --git a/spec/models/project/stage_spec.rb b/spec/models/project/stage_spec.rb
new file mode 100644
index 000000000000..6fd727a34b27
--- /dev/null
+++ b/spec/models/project/stage_spec.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "rails_helper"
+require "support/shared/project_life_cycle_helpers"
+
+RSpec.describe Project::Stage do
+ it_behaves_like "a Project::LifeCycleStep event"
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:start_date) }
+ it { is_expected.to validate_presence_of(:end_date) }
+ it { is_expected.to validate_inclusion_of(:type).in_array(["Project::Stage"]).with_message(:must_be_a_stage) }
+
+ it "is valid when `start_date` and `end_date` are present" do
+ valid_stage = build(:project_stage)
+ expect(valid_stage).to be_valid
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 63eca53f4e75..2f191a053f34 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -381,6 +381,10 @@
end
end
+ describe "life_cycles" do
+ it { is_expected.to have_many(:life_cycle_steps).class_name("Project::LifeCycleStep").dependent(:destroy) }
+ end
+
describe "#enabled_module_names=", with_settings: { default_projects_modules: %w(work_package_tracking repository) } do
context "when assigning a new value" do
let(:new_value) { %w(work_package_tracking news) }
diff --git a/spec/models/queries/operators/custom_fields/hierarchies/equals_with_descendants_spec.rb b/spec/models/queries/operators/custom_fields/hierarchies/equals_with_descendants_spec.rb
new file mode 100644
index 000000000000..80e0510556f0
--- /dev/null
+++ b/spec/models/queries/operators/custom_fields/hierarchies/equals_with_descendants_spec.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe Queries::Operators::CustomFields::Hierarchies::EqualsWithDescendants do
+ subject(:sql) { described_class.sql_for_field(values, db_table, db_field) }
+
+ let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
+ let!(:root) { service.generate_root(custom_field).value! }
+ let!(:germany) { service.insert_item(parent: root, label: "Germany", short: "DE").value! }
+ let!(:berlin) { service.insert_item(parent: germany, label: "Berlin").value! }
+ let!(:munich) { service.insert_item(parent: germany, label: "Munich").value! }
+ let!(:portugal) { service.insert_item(parent: root, label: "Portugal", short: "PT").value! }
+ let!(:lisbon) { service.insert_item(parent: portugal, label: "Lisboa").value! }
+ let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
+
+ let(:db_table) { "custom_values" }
+ let(:db_field) { "value" }
+
+ context "when generating for a branch" do
+ let(:values) { [germany.id] }
+
+ it "generates the expected SQL" do
+ expect(sql).to eq("custom_values.value IN ('#{germany.id}','#{berlin.id}','#{munich.id}')")
+ end
+ end
+
+ context "when generating for a leaf" do
+ let(:values) { [lisbon.id] }
+
+ it "generates the expected SQL" do
+ expect(sql).to eq("custom_values.value IN ('#{lisbon.id}')")
+ end
+ end
+end
diff --git a/spec/models/queries/projects/project_query_custom_field_order_spec.rb b/spec/models/queries/projects/project_query_custom_field_order_spec.rb
index 5958575f31b1..87daf7783578 100644
--- a/spec/models/queries/projects/project_query_custom_field_order_spec.rb
+++ b/spec/models/queries/projects/project_query_custom_field_order_spec.rb
@@ -116,9 +116,9 @@ def project_without_cf_value
let(:projects) do
[
+ project_without_cf_value,
project_with_cf_value("6"),
- project_with_cf_value("16"),
- project_without_cf_value # TODO: should be at index 0
+ project_with_cf_value("16")
]
end
end
@@ -130,9 +130,9 @@ def project_without_cf_value
let(:projects) do
[
+ project_without_cf_value,
project_with_cf_value("6.25"),
- project_with_cf_value("16"),
- project_without_cf_value # TODO: should be at index 0
+ project_with_cf_value("16")
]
end
end
@@ -177,11 +177,11 @@ def project_without_cf_value
let(:projects) do
[
+ project_without_cf_value,
# sorting is done by position, and not by value
project_with_cf_value(id_by_value.fetch("100")),
project_with_cf_value(id_by_value.fetch("3")),
project_with_cf_value(id_by_value.fetch("20")),
- project_without_cf_value # TODO: should be at index 0
]
end
end
@@ -193,6 +193,7 @@ def project_without_cf_value
let(:projects) do
[
+ project_without_cf_value,
project_with_cf_value(*id_by_value.fetch_values("100")), # 100
project_with_cf_value(*id_by_value.fetch_values("3", "100")), # 100, 3
project_with_cf_value(*id_by_value.fetch_values("3", "20", "100")), # 100, 3, 20
@@ -205,14 +206,13 @@ def project_without_cf_value
project_with_cf_value(*id_by_value.fetch_values("3")), # 3
project_with_cf_value(*id_by_value.fetch_values("3", "20")), # 3, 20
project_with_cf_value(*id_by_value.fetch_values("20")), # 20
- project_without_cf_value # TODO: decide on order of absent values
]
end
let(:projects_desc) do
indexes = projects.each_index.to_a
- # order of values for a work package is ignored, so ordered by falling back on id asc
- indexes[2...8] = indexes[2...8].reverse
+ # order of work packages with same values in different order falls back on next column (id asc)
+ indexes[3...9] = indexes[3...9].reverse
projects.values_at(*indexes.reverse)
end
end
@@ -248,11 +248,11 @@ def project_without_cf_value
let(:custom_field_values) do
[
+ nil,
id_by_login.fetch("ax"),
id_by_login.fetch("ba"),
id_by_login.fetch("bb1"),
id_by_login.fetch("bb2"),
- nil # TODO: should be at index 0
]
end
@@ -272,13 +272,13 @@ def project_without_cf_value
let(:custom_field_values) do
[
+ [],
id_by_login.fetch_values("ax"), # ax
id_by_login.fetch_values("bb1", "ax"), # ax, bb1
id_by_login.fetch_values("ax", "bb1"), # ax, bb1
id_by_login.fetch_values("ba"), # ba
id_by_login.fetch_values("bb1", "ba"), # ba, bb1
id_by_login.fetch_values("ba", "bb2"), # ba, bb2
- [] # TODO: should be at index 0
]
end
@@ -290,8 +290,8 @@ def project_without_cf_value
let(:projects_desc) do
indexes = projects.each_index.to_a
- # order of values for a work package is ignored, so ordered by falling back on id asc
- indexes[1...3] = indexes[1...3].reverse
+ # order of work packages with same values in different order falls back on next column (id asc)
+ indexes[2...4] = indexes[2...4].reverse
projects.values_at(*indexes.reverse)
end
end
@@ -316,11 +316,11 @@ def project_without_cf_value
let(:projects) do
[
+ project,
project_with_cf_value(id_by_name.fetch("9")),
project_with_cf_value(id_by_name.fetch("10.2")),
project_with_cf_value(id_by_name.fetch("10.10.2")),
project_with_cf_value(id_by_name.fetch("10.10.10")),
- project # TODO: should be at index 0
]
end
end
@@ -332,20 +332,20 @@ def project_without_cf_value
let(:projects) do
[
+ project,
project_with_cf_value(*id_by_name.fetch_values("10.10.2", "9")), # 9, 10.10.2
project_with_cf_value(*id_by_name.fetch_values("10.10.10", "9")), # 9, 10.10.10
project_with_cf_value(*id_by_name.fetch_values("9", "10.10.10")), # 9, 10.10.10
project_with_cf_value(*id_by_name.fetch_values("10.2", "10.10.2")), # 10.2, 10.10.2
project_with_cf_value(*id_by_name.fetch_values("10.10.2")), # 10.10.2
- project_with_cf_value(*id_by_name.fetch_values("10.10.10")), # 10.10.10
- project # TODO: should be at index 0
+ project_with_cf_value(*id_by_name.fetch_values("10.10.10")) # 10.10.10
]
end
let(:projects_desc) do
indexes = projects.each_index.to_a
- # order of values for a work package is ignored, so ordered by falling back on id asc
- indexes[1...3] = indexes[1...3].reverse
+ # order of work packages with same values in different order falls back on next column (id asc)
+ indexes[2...4] = indexes[2...4].reverse
projects.values_at(*indexes.reverse)
end
end
diff --git a/spec/models/queries/work_packages/selects/shared_query_select_specs.rb b/spec/models/queries/work_packages/selects/shared_query_select_specs.rb
index 4fd9758d4c28..8df7f50fddc2 100644
--- a/spec/models/queries/work_packages/selects/shared_query_select_specs.rb
+++ b/spec/models/queries/work_packages/selects/shared_query_select_specs.rb
@@ -29,25 +29,25 @@
RSpec.shared_examples_for "query column" do |sortable_by_default: false|
describe "#groupable" do
it "is the name if true is provided" do
- instance.groupable = true
+ instance.instance_variable_set(:@groupable, true)
expect(instance.groupable).to eql(instance.name.to_s)
end
it "is the value if something truthy is provided" do
- instance.groupable = "lorem ipsum"
+ instance.instance_variable_set(:@groupable, "lorem ipsum")
expect(instance.groupable).to eql("lorem ipsum")
end
it "is false if false is provided" do
- instance.groupable = false
+ instance.instance_variable_set(:@groupable, false)
expect(instance.groupable).to be_falsey
end
- it "is false if nothing is provided" do
- instance.groupable = nil
+ it "is false if nil is provided" do
+ instance.instance_variable_set(:@groupable, nil)
expect(instance.groupable).to be_falsey
end
@@ -55,43 +55,39 @@
describe "#sortable" do
it "is the name if true is provided" do
- instance.sortable = true
+ instance.instance_variable_set(:@sortable, true)
expect(instance.sortable).to eql(instance.name.to_s)
end
it "is the value if something truthy is provided" do
- instance.sortable = "lorem ipsum"
+ instance.instance_variable_set(:@sortable, "lorem ipsum")
expect(instance.sortable).to eql("lorem ipsum")
end
it "is false if false is provided" do
- instance.sortable = false
+ instance.instance_variable_set(:@sortable, false)
expect(instance.sortable).to be_falsey
end
- it "is false if nothing is provided" do
- instance.sortable = nil
+ it "is false if nil is provided" do
+ instance.instance_variable_set(:@sortable, nil)
expect(instance.sortable).to be_falsey
end
end
describe "#groupable?" do
- it "is false by default" do
- expect(instance).not_to be_groupable
- end
-
it "is true if told so" do
- instance.groupable = true
+ instance.instance_variable_set(:@groupable, true)
expect(instance).to be_groupable
end
it "is true if a value is provided (e.g. for specifying sql code)" do
- instance.groupable = "COALESCE(null, 1)"
+ instance.instance_variable_set(:@groupable, "COALESCE(null, 1)")
expect(instance).to be_groupable
end
@@ -107,13 +103,13 @@
end
it "is true if told so" do
- instance.sortable = true
+ instance.instance_variable_set(:@sortable, true)
expect(instance).to be_sortable
end
it "is true if a value is provided (e.g. for specifying sql code)" do
- instance.sortable = "COALESCE(null, 1)"
+ instance.instance_variable_set(:@sortable, "COALESCE(null, 1)")
expect(instance).to be_sortable
end
diff --git a/spec/models/query/results_cf_sorting_integration_spec.rb b/spec/models/query/results_cf_sorting_integration_spec.rb
index 74e2f9865b39..49711dab3365 100644
--- a/spec/models/query/results_cf_sorting_integration_spec.rb
+++ b/spec/models/query/results_cf_sorting_integration_spec.rb
@@ -188,11 +188,11 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
# sorting is done by position, and not by value
wp_with_cf_value(id_by_value.fetch("100")),
wp_with_cf_value(id_by_value.fetch("3")),
wp_with_cf_value(id_by_value.fetch("20")),
- wp_without_cf_value # TODO: should be at index 0
]
end
end
@@ -204,6 +204,7 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
wp_with_cf_value(id_by_value.fetch_values("100")), # 100
wp_with_cf_value(id_by_value.fetch_values("3", "100")), # 100, 3
wp_with_cf_value(id_by_value.fetch_values("3", "20", "100")), # 100, 3, 20
@@ -216,14 +217,13 @@ def wp_without_cf_value
wp_with_cf_value(id_by_value.fetch_values("3")), # 3
wp_with_cf_value(id_by_value.fetch_values("3", "20")), # 3, 20
wp_with_cf_value(id_by_value.fetch_values("20")), # 20
- wp_without_cf_value # TODO: decide on order of absent values
]
end
let(:work_packages_desc) do
indexes = work_packages.each_index.to_a
- # order of values for a project is ignored, so ordered by falling back on id asc
- indexes[2...8] = indexes[2...8].reverse
+ # order of projects with same values in different order falls back on next column (id asc)
+ indexes[3...9] = indexes[3...9].reverse
work_packages.values_at(*indexes.reverse)
end
end
@@ -255,11 +255,11 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
wp_with_cf_value(id_by_login.fetch("ax")),
wp_with_cf_value(id_by_login.fetch("ba")),
wp_with_cf_value(id_by_login.fetch("bb1")),
wp_with_cf_value(id_by_login.fetch("bb2")),
- wp_without_cf_value # TODO: should be at index 0
]
end
end
@@ -271,20 +271,20 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
wp_with_cf_value(id_by_login.fetch_values("ax")), # ax
wp_with_cf_value(id_by_login.fetch_values("bb1", "ax")), # ax, bb1
wp_with_cf_value(id_by_login.fetch_values("ax", "bb1")), # ax, bb1
wp_with_cf_value(id_by_login.fetch_values("ba")), # ba
wp_with_cf_value(id_by_login.fetch_values("bb1", "ba")), # ba, bb1
wp_with_cf_value(id_by_login.fetch_values("ba", "bb2")), # ba, bb2
- wp_without_cf_value # TODO: should be at index 0
]
end
let(:work_packages_desc) do
indexes = work_packages.each_index.to_a
- # order of values for a project is ignored, so ordered by falling back on id asc
- indexes[1...3] = indexes[1...3].reverse
+ # order of projects with same values in different order falls back on next column (id asc)
+ indexes[2...4] = indexes[2...4].reverse
work_packages.values_at(*indexes.reverse)
end
end
@@ -308,11 +308,11 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
wp_with_cf_value(id_by_name.fetch("9")),
wp_with_cf_value(id_by_name.fetch("10.2")),
wp_with_cf_value(id_by_name.fetch("10.10.2")),
- wp_with_cf_value(id_by_name.fetch("10.10.10")),
- wp_without_cf_value # TODO: should be at index 0
+ wp_with_cf_value(id_by_name.fetch("10.10.10"))
]
end
end
@@ -324,23 +324,52 @@ def wp_without_cf_value
let(:work_packages) do
[
+ wp_without_cf_value,
wp_with_cf_value(id_by_name.fetch_values("10.10.2", "9")), # 9, 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.10", "9")), # 9, 10.10.10
wp_with_cf_value(id_by_name.fetch_values("9", "10.10.10")), # 9, 10.10.10
wp_with_cf_value(id_by_name.fetch_values("10.2", "10.10.2")), # 10.2, 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.2")), # 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.10")), # 10.10.10
- wp_without_cf_value # TODO: should be at index 0
]
end
let(:work_packages_desc) do
indexes = work_packages.each_index.to_a
- # order of values for a project is ignored, so ordered by falling back on id asc
- indexes[1...3] = indexes[1...3].reverse
+ # order of projects with same values in different order falls back on next column (id asc)
+ indexes[2...4] = indexes[2...4].reverse
work_packages.values_at(*indexes.reverse)
end
end
end
end
+
+ context "for hierarchy format" do
+ include_examples "it sorts" do
+ let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
+ let(:root) { service.generate_root(custom_field).value! }
+ let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
+
+ let!(:item_first) { service.insert_item(parent: root, label: "aa item").value! }
+ let!(:item_a) { service.insert_item(parent: root, label: "item_a").value! }
+ let!(:item_a1) { service.insert_item(parent: item_a, label: "item_a1").value! }
+ let!(:item_a2) { service.insert_item(parent: item_a, label: "item_a2").value! }
+ let!(:item_c) { service.insert_item(parent: root, label: "item_c").value! }
+ let!(:item_b) { service.insert_item(parent: root, label: "item_b").value! }
+ let!(:item_last) { service.insert_item(parent: root, label: "zz item").value! }
+
+ let(:work_packages) do
+ [
+ wp_without_cf_value,
+ wp_with_cf_value(item_first.id),
+ wp_with_cf_value(item_a.id),
+ wp_with_cf_value(item_a1.id),
+ wp_with_cf_value(item_a2.id),
+ wp_with_cf_value(item_b.id),
+ wp_with_cf_value(item_c.id),
+ wp_with_cf_value(item_last.id)
+ ]
+ end
+ end
+ end
end
diff --git a/spec/models/work_package/pdf_export/document_generator_spec.rb b/spec/models/work_package/pdf_export/document_generator_spec.rb
new file mode 100644
index 000000000000..d5c250d2aad6
--- /dev/null
+++ b/spec/models/work_package/pdf_export/document_generator_spec.rb
@@ -0,0 +1,91 @@
+require "spec_helper"
+require "text/hyphen"
+
+RSpec.describe WorkPackage::PDFExport::DocumentGenerator do
+ include Redmine::I18n
+ include PDFExportSpecUtils
+
+ let(:project) { create(:project) }
+ let(:user) { create(:admin) }
+ let(:description) do
+ "This is a test description with an macro: workPackageValue:assignee"
+ end
+ let(:work_package) do
+ create(:work_package,
+ project:,
+ description:,
+ assigned_to: user,
+ subject: "Document Generator Specs",
+ type:)
+ end
+ let(:type) { create(:type) }
+ let(:options) do
+ {}
+ end
+ let(:shared_options) do
+ {
+ header_text_right: "A text on the right of the header",
+ footer_text_center: "A text in the center of the footer"
+ }
+ end
+ let(:exporter) do
+ described_class.new(work_package, shared_options.merge(options))
+ end
+ let(:export) do
+ login_as(user)
+ exporter
+ end
+ let(:export_time) { DateTime.new(2023, 6, 30, 23, 59) }
+ let(:export_time_formatted) { format_time(export_time, include_date: true) }
+ let(:export_pdf) do
+ Timecop.freeze(export_time) do
+ export.export!
+ end
+ end
+
+ subject(:pdf) do
+ content = export_pdf.content
+ # If you want to actually see the PDF for debugging, uncomment the following line
+ # File.binwrite("WorkPackageDocumentGenerator-test-preview.pdf", content)
+ PDF::Inspector::Text.analyze(content).strings
+ end
+
+ describe "with a request for a PDF" do
+ it "contains correct data" do
+ expected_result = [
+ "This is a test description with an macro:",
+ user.name,
+ export_time_formatted,
+ "A text in the center of the footer",
+ "Page 1 of 1",
+ "A text on the right of the header"
+ ]
+ result = pdf
+ expect(result.join(" ")).to eq(expected_result.join(" "))
+ end
+
+ describe "with a request for a PDF with hyphenation and no header/footer text" do
+ let(:options) do
+ {
+ hyphenation: "en_us",
+ header_text_right: "",
+ footer_text_center: ""
+ }
+ end
+ let(:description) do
+ "honorificabilitudinitatibus " * 6
+ end
+
+ it "contains correct data" do
+ expected_result = [
+ "honorificabilitudinitatibus honorificabilitudinitatibus honorificabilitudinitatibus " \
+ "honorificabili tudinitatibus honorificabilitudinitatibus honorificabilitudinitatibus",
+ export_time_formatted,
+ "Page 1 of 1"
+ ]
+ result = pdf
+ expect(result.join(" ")).to eq(expected_result.join(" "))
+ end
+ end
+ end
+end
diff --git a/spec/models/work_package_spec.rb b/spec/models/work_package_spec.rb
index 68eec331053a..bda8a4caefe6 100644
--- a/spec/models/work_package_spec.rb
+++ b/spec/models/work_package_spec.rb
@@ -70,6 +70,7 @@
it { is_expected.to belong_to(:assigned_to).class_name("Principal").optional }
it { is_expected.to belong_to(:responsible).class_name("Principal").optional }
it { is_expected.to belong_to(:version).optional }
+ it { is_expected.to belong_to(:project_life_cycle_step).class_name("Project::LifeCycleStep").optional }
it { is_expected.to belong_to(:priority).class_name("IssuePriority") }
it { is_expected.to belong_to(:category).optional }
it { is_expected.to have_many(:time_entries).dependent(:delete_all) }
diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
index fbd4a11b2ffb..a516118e6da1 100644
--- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
+++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb
@@ -214,6 +214,7 @@ def get_column_value(column_name)
subject(:pdf) do
content = export_pdf.content
+ # If you want to actually see the PDF for debugging, uncomment the following line
# File.binwrite('WorkPackageToPdf-test-preview.pdf', content)
{ strings: PDF::Inspector::Text.analyze(content).strings,
images: PDF::Inspector::XObject.analyze(content).page_xobjects.flat_map do |o|
diff --git a/spec/requests/api/v3/authentication_spec.rb b/spec/requests/api/v3/authentication_spec.rb
index 24b3042132c5..658232433892 100644
--- a/spec/requests/api/v3/authentication_spec.rb
+++ b/spec/requests/api/v3/authentication_spec.rb
@@ -424,7 +424,7 @@ def set_basic_auth_header(user, password)
headers: {
"Accept" => "*/*",
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
- "User-Agent" => "JSON::JWK::Set::Fetcher 2.7.4"
+ "User-Agent" => "JSON::JWK::Set::Fetcher 2.8.2"
}
)
.to_return(status: 200, body: '{"keys":[{"kid":"CANAG6lJUPKqKDoWxxXL5wAHf2U18BAzm_LJm7RPTGk","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"nqJexS6n-SxKSDUxXp_dsNwDW6cZ4Rtgqq9ut_lp1CNSph5wTnLG3aQQsTEvx5o3-SZ-pHjJ0gtEpg7clAz-w-YQyZoAXkFtQqmZJxsmdS4K0yILxO3WUNdJQlutjmq-Ri50Senn5IV7yEYWLo8St1qzUqWZhp0HKudyty24triC9UJTK03W3_Tr5c1X8vKL8duAjvLB7p_sYUOrnLq5pD5lqwxVSAiN8qS5zVNZMrhGV5aN1vN_vue_tw8c2SVOCLLTrUh3441rYaeo-UwQZF7ZTm30xflqAIfe8qMoB20wtWYAXR0D5iqkkdEH4XanCYVm5vdUFIPPvXZhRDWoNQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeGPzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCeol7FLqf5LEpINTFen92w3ANbpxnhG2Cqr263+WnUI1KmHnBOcsbdpBCxMS/Hmjf5Jn6keMnSC0SmDtyUDP7D5hDJmgBeQW1CqZknGyZ1LgrTIgvE7dZQ10lCW62Oar5GLnRJ6efkhXvIRhYujxK3WrNSpZmGnQcq53K3Lbi2uIL1QlMrTdbf9OvlzVfy8ovx24CO8sHun+xhQ6ucurmkPmWrDFVICI3ypLnNU1kyuEZXlo3W83++57+3DxzZJU4IstOtSHfjjWthp6j5TBBkXtlObfTF+WoAh97yoygHbTC1ZgBdHQPmKqSR0QfhdqcJhWbm91QUg8+9dmFENag1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAB/AGvP0gviPoJszj/oQgBsMpPGRHLpnTmrXnTaa7Xk2sgExAb4zUAwxGjtR347t697cpiKQYBkR2ndswnt93Sx/Ot+yn5BdYcNvZuEh5jb5bkH2V4h6/LrYljTymby+XPBEf+XLhBOjoI3SKtNJk4pEqVNwLuKKbObqJcE3G3VBVSdzRUcIrjZr7yAQeLnhczS3hJ0Ct6Y7S5Q6DK+/PU1+AvlW+7GfzpRMqVfLcqhNpRwdCVGlJYKaUJfIe1vav10D94xA0U1sKex3iA1S+1HlS2BCWx/0rXwgcquMpUZlOAKiT0K6SIFxBFFnM9eQbF97Dz7Bzw+jyqStGUcH9YA="],"x5t":"TuBfrOL00KXDrOWTv3jw7Uxx3hA","x5t#S256":"7su5lOXF5qcMuvp44ynsoyk3B0l9Sr_bOVlg768shpY"},{"kid":"97AmyvoS8BFFRfm585GPgA16G1H2V22EdxxuAYUuoKk","kty":"RSA","alg":"RS256","use":"sig","n":"jMB2r7BG4QJzLnA2_fgG1mxlh2RX_MSx0lc2lrPIVFGYBuAu8irwRLSexX5aQdD_AtnxLD4g9jiG6VEDwmWopEe0fr-QMl0IiES5tJuQMrjhajOkzr8xTYu6zl-knL0tu99iRbmKNYzEcv0TAgY_95n4gD5tPhYvY4gXuHrFKqYkJQPsSgoThlH7hAtfzsDt6yp3P2lQUESGg3pzc_J_NKnQkkggcNB06Hlz4DmcHxhWXK51P1V9cE7qh4PrhsJ-SOH5grcN9PtOZi6f2VlWdFdyisT-YehNklfVqBtdCLm7Ocghhl0HSgLuV-9dHCdwBLUpABsdsd0L3LRCUgRfjQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeFFTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCMwHavsEbhAnMucDb9+AbWbGWHZFf8xLHSVzaWs8hUUZgG4C7yKvBEtJ7FflpB0P8C2fEsPiD2OIbpUQPCZaikR7R+v5AyXQiIRLm0m5AyuOFqM6TOvzFNi7rOX6ScvS2732JFuYo1jMRy/RMCBj/3mfiAPm0+Fi9jiBe4esUqpiQlA+xKChOGUfuEC1/OwO3rKnc/aVBQRIaDenNz8n80qdCSSCBw0HToeXPgOZwfGFZcrnU/VX1wTuqHg+uGwn5I4fmCtw30+05mLp/ZWVZ0V3KKxP5h6E2SV9WoG10Iubs5yCGGXQdKAu5X710cJ3AEtSkAGx2x3QvctEJSBF+NAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIoBCsOO0bXiVspoXkqdOts4+3sULbbp5aEwQscmLX017Zvv5jxdkZxUYk8L08lNB+WlC1ES4VlmtE06D0cWYErGpArJzVBKgYSA3CkA9veBEugHviMqfwg3suNc8S+GtaRBvpbVZtXydjjqA8GZ4eKhPoJLHHCX6X2Ad33Cdt0/ftucjTqAKVzzzgWZejy+ZKP6ybAqYJ+EZoPUXlyWT3uwcpGEJ3nzOYYGTfxOSmAwnH2v5Z/JWr9ex5o/+QBuBhFcg0z8NcHa3Z0E6ZC9GGxV7XztBqYicO+nONHTLCctoJmyXvLM4j8qIG2UQgPIiwIL0Jkz6xQAYyXvsb+LhM8="],"x5t":"BFrni6MoX-CJwtMT4vzij1HBSTI","x5t#S256":"-Ge3y4JRezxhGTDfbkNoz7prkokzYtbKQ9ardPtfcz4"}]}', headers: {})
@@ -495,7 +495,7 @@ def set_basic_auth_header(user, password)
headers: {
"Accept" => "*/*",
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
- "User-Agent" => "JSON::JWK::Set::Fetcher 2.7.4"
+ "User-Agent" => "JSON::JWK::Set::Fetcher 2.8.2"
}
)
.to_return(status: 200, body: '{"keys":[{"kid":"CANAG6lJUPKqKDoWxxXL5wAHf2U18BAzm_LJm7RPTGk","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"nqJexS6n-SxKSDUxXp_dsNwDW6cZ4Rtgqq9ut_lp1CNSph5wTnLG3aQQsTEvx5o3-SZ-pHjJ0gtEpg7clAz-w-YQyZoAXkFtQqmZJxsmdS4K0yILxO3WUNdJQlutjmq-Ri50Senn5IV7yEYWLo8St1qzUqWZhp0HKudyty24triC9UJTK03W3_Tr5c1X8vKL8duAjvLB7p_sYUOrnLq5pD5lqwxVSAiN8qS5zVNZMrhGV5aN1vN_vue_tw8c2SVOCLLTrUh3441rYaeo-UwQZF7ZTm30xflqAIfe8qMoB20wtWYAXR0D5iqkkdEH4XanCYVm5vdUFIPPvXZhRDWoNQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeGPzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCeol7FLqf5LEpINTFen92w3ANbpxnhG2Cqr263+WnUI1KmHnBOcsbdpBCxMS/Hmjf5Jn6keMnSC0SmDtyUDP7D5hDJmgBeQW1CqZknGyZ1LgrTIgvE7dZQ10lCW62Oar5GLnRJ6efkhXvIRhYujxK3WrNSpZmGnQcq53K3Lbi2uIL1QlMrTdbf9OvlzVfy8ovx24CO8sHun+xhQ6ucurmkPmWrDFVICI3ypLnNU1kyuEZXlo3W83++57+3DxzZJU4IstOtSHfjjWthp6j5TBBkXtlObfTF+WoAh97yoygHbTC1ZgBdHQPmKqSR0QfhdqcJhWbm91QUg8+9dmFENag1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAB/AGvP0gviPoJszj/oQgBsMpPGRHLpnTmrXnTaa7Xk2sgExAb4zUAwxGjtR347t697cpiKQYBkR2ndswnt93Sx/Ot+yn5BdYcNvZuEh5jb5bkH2V4h6/LrYljTymby+XPBEf+XLhBOjoI3SKtNJk4pEqVNwLuKKbObqJcE3G3VBVSdzRUcIrjZr7yAQeLnhczS3hJ0Ct6Y7S5Q6DK+/PU1+AvlW+7GfzpRMqVfLcqhNpRwdCVGlJYKaUJfIe1vav10D94xA0U1sKex3iA1S+1HlS2BCWx/0rXwgcquMpUZlOAKiT0K6SIFxBFFnM9eQbF97Dz7Bzw+jyqStGUcH9YA="],"x5t":"TuBfrOL00KXDrOWTv3jw7Uxx3hA","x5t#S256":"7su5lOXF5qcMuvp44ynsoyk3B0l9Sr_bOVlg768shpY"},{"kid":"9755555S8BFFRfm585GPgA16G1H2V22EdxxuAYUuoKk","kty":"RSA","alg":"RS256","use":"sig","n":"jMB2r7BG4QJzLnA2_fgG1mxlh2RX_MSx0lc2lrPIVFGYBuAu8irwRLSexX5aQdD_AtnxLD4g9jiG6VEDwmWopEe0fr-QMl0IiES5tJuQMrjhajOkzr8xTYu6zl-knL0tu99iRbmKNYzEcv0TAgY_95n4gD5tPhYvY4gXuHrFKqYkJQPsSgoThlH7hAtfzsDt6yp3P2lQUESGg3pzc_J_NKnQkkggcNB06Hlz4DmcHxhWXK51P1V9cE7qh4PrhsJ-SOH5grcN9PtOZi6f2VlWdFdyisT-YehNklfVqBtdCLm7Ocghhl0HSgLuV-9dHCdwBLUpABsdsd0L3LRCUgRfjQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeFFTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCMwHavsEbhAnMucDb9+AbWbGWHZFf8xLHSVzaWs8hUUZgG4C7yKvBEtJ7FflpB0P8C2fEsPiD2OIbpUQPCZaikR7R+v5AyXQiIRLm0m5AyuOFqM6TOvzFNi7rOX6ScvS2732JFuYo1jMRy/RMCBj/3mfiAPm0+Fi9jiBe4esUqpiQlA+xKChOGUfuEC1/OwO3rKnc/aVBQRIaDenNz8n80qdCSSCBw0HToeXPgOZwfGFZcrnU/VX1wTuqHg+uGwn5I4fmCtw30+05mLp/ZWVZ0V3KKxP5h6E2SV9WoG10Iubs5yCGGXQdKAu5X710cJ3AEtSkAGx2x3QvctEJSBF+NAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIoBCsOO0bXiVspoXkqdOts4+3sULbbp5aEwQscmLX017Zvv5jxdkZxUYk8L08lNB+WlC1ES4VlmtE06D0cWYErGpArJzVBKgYSA3CkA9veBEugHviMqfwg3suNc8S+GtaRBvpbVZtXydjjqA8GZ4eKhPoJLHHCX6X2Ad33Cdt0/ftucjTqAKVzzzgWZejy+ZKP6ybAqYJ+EZoPUXlyWT3uwcpGEJ3nzOYYGTfxOSmAwnH2v5Z/JWr9ex5o/+QBuBhFcg0z8NcHa3Z0E6ZC9GGxV7XztBqYicO+nONHTLCctoJmyXvLM4j8qIG2UQgPIiwIL0Jkz6xQAYyXvsb+LhM8="],"x5t":"BFrni6MoX-CJwtMT4vzij1HBSTI","x5t#S256":"-Ge3y4JRezxhGTDfbkNoz7prkokzYtbKQ9ardPtfcz4"}]}', headers: {})
diff --git a/spec/seeders/admin_user_seeder_spec.rb b/spec/seeders/admin_user_seeder_spec.rb
index 0dcb79608102..db1f4d1de5ef 100644
--- a/spec/seeders/admin_user_seeder_spec.rb
+++ b/spec/seeders/admin_user_seeder_spec.rb
@@ -56,6 +56,7 @@
seeder.seed!
admin = User.admin.last
+ expect(admin).to be_active
expect(admin.firstname).to eq "foo"
expect(admin.lastname).to eq "bar"
expect(admin.mail).to eq "foobar@example.com"
diff --git a/spec/seeders/basic_data/life_cycle_color_seeder_spec.rb b/spec/seeders/basic_data/life_cycle_color_seeder_spec.rb
new file mode 100644
index 000000000000..c63c2d625253
--- /dev/null
+++ b/spec/seeders/basic_data/life_cycle_color_seeder_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe BasicData::LifeCycleColorSeeder do
+ include_context "with basic seed data"
+ subject(:seeder) { described_class.new(seed_data) }
+
+ let(:seed_data) { basic_seed_data.merge(Source::SeedData.new(data_hash)) }
+
+ before do
+ seeder.seed!
+ end
+
+ context "with some life cycle colors defined" do
+ let(:data_hash) do
+ YAML.load <<~SEEDING_DATA_YAML
+ life_cycle_colors:
+ - reference: :default_color_pm2_orange
+ name: PM2 Orange
+ hexcode: "#F7983A"
+ - reference: :default_color_pm2_red
+ name: PM2 Red
+ hexcode: "#F05823"
+ - reference: :default_color_pm2_purple
+ name: PM2 Purple
+ hexcode: "#682D91"
+ - reference: :default_color_pm2_magenta
+ name: PM2 Magenta
+ hexcode: "#EC038A"
+ - reference: :default_color_pm2_green_yellow
+ name: PM2 Green Yellow
+ hexcode: "#B1D13A"
+ SEEDING_DATA_YAML
+ end
+
+ shared_examples "creates the life cycle color seeds" do
+ it "creates the corresponding life cycle colors with the given attributes" do
+ expect(Color.find_by(name: "PM2 Orange")).to have_attributes(hexcode: "#F7983A")
+ expect(Color.find_by(name: "PM2 Red")).to have_attributes(hexcode: "#F05823")
+ expect(Color.find_by(name: "PM2 Purple")).to have_attributes(hexcode: "#682D91")
+ expect(Color.find_by(name: "PM2 Magenta")).to have_attributes(hexcode: "#EC038A")
+ expect(Color.find_by(name: "PM2 Green Yellow")).to have_attributes(hexcode: "#B1D13A")
+ end
+
+ it "references the life cycle colors in the seed data" do
+ color_names = data_hash["life_cycle_colors"].pluck("name")
+ Color.where(name: color_names).find_each do |expected_stage|
+ reference = :"default_color_#{expected_stage.name.downcase.gsub(/\s+/, '_')}"
+ expect(seed_data.find_reference(reference)).to eq(expected_stage)
+ end
+ end
+ end
+
+ it_behaves_like "creates the life cycle color seeds"
+
+ context "when some colors already exist" do
+ # Typical usecase for running the seeders after an upgrade: Some data exist from
+ # previous seed runs, and new seed data is added to the configuration YML.
+ before do
+ Color.create!(name: "Other color", hexcode: "#F7983A")
+ end
+
+ it_behaves_like "creates the life cycle color seeds"
+ end
+
+ context "when seeding a second time" do
+ subject(:second_seeder) { described_class.new(second_seed_data) }
+
+ let(:second_seed_data) { basic_seed_data.merge(Source::SeedData.new(data_hash)) }
+
+ it "registers existing matching life cycle colors as references in the seed data" do
+ expect { second_seeder.seed! }.not_to change(Color, :count)
+
+ # using the first seed data as the expected value
+ expect(second_seed_data.find_reference(:default_color_pm2_orange))
+ .to eq(seed_data.find_reference(:default_color_pm2_orange))
+ expect(second_seed_data.find_reference(:default_color_pm2_red))
+ .to eq(seed_data.find_reference(:default_color_pm2_red))
+ expect(second_seed_data.find_reference(:default_color_pm2_purple))
+ .to eq(seed_data.find_reference(:default_color_pm2_purple))
+ expect(second_seed_data.find_reference(:default_color_pm2_magenta))
+ .to eq(seed_data.find_reference(:default_color_pm2_magenta))
+ expect(second_seed_data.find_reference(:default_color_pm2_green_yellow))
+ .to eq(seed_data.find_reference(:default_color_pm2_green_yellow))
+ end
+ end
+ end
+
+ context "without life cycle colors defined" do
+ let(:data_hash) do
+ YAML.load <<~SEEDING_DATA_YAML
+ nothing here: ''
+ SEEDING_DATA_YAML
+ end
+
+ it "creates no life cycle colors" do
+ expect(Color.find_by(name: "PM2 Orange")).to be_nil
+ expect(Color.find_by(name: "PM2 Red")).to be_nil
+ expect(Color.find_by(name: "PM2 Purple")).to be_nil
+ expect(Color.find_by(name: "PM2 Magenta")).to be_nil
+ expect(Color.find_by(name: "PM2 Green Yellow")).to be_nil
+ end
+ end
+end
diff --git a/spec/seeders/basic_data/life_cycle_step_definition_seeder_spec.rb b/spec/seeders/basic_data/life_cycle_step_definition_seeder_spec.rb
new file mode 100644
index 000000000000..367f1fdcc5aa
--- /dev/null
+++ b/spec/seeders/basic_data/life_cycle_step_definition_seeder_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe BasicData::LifeCycleStepDefinitionSeeder do
+ include_context "with basic seed data"
+ subject(:seeder) { described_class.new(seed_data) }
+
+ let(:seed_data) { basic_seed_data.merge(Source::SeedData.new(data_hash)) }
+
+ before do
+ seeder.seed!
+ end
+
+ context "with some life cycles defined" do
+ let(:data_hash) do
+ YAML.load <<~SEEDING_DATA_YAML
+ life_cycles:
+ - reference: :default_life_cycle_initiating
+ name: Initiating
+ type: Project::StageDefinition
+ color_name: :default_color_pm2_orange
+ - reference: :default_life_cycle_ready_for_executing
+ name: Ready for Executing
+ type: Project::GateDefinition
+ color_name: :default_color_pm2_purple
+ - reference: :default_life_cycle_planning
+ name: Planning
+ type: Project::StageDefinition
+ color_name: :default_color_pm2_red
+ - reference: :default_life_cycle_executing
+ name: Executing
+ type: Project::StageDefinition
+ color_name: :default_color_pm2_magenta
+ - reference: :default_life_cycle_closing
+ name: Closing
+ type: Project::StageDefinition
+ color_name: :default_color_pm2_green_yellow
+ SEEDING_DATA_YAML
+ end
+
+ it "creates the corresponding life cycles with the given attributes" do
+ expect(Project::LifeCycleStepDefinition.count).to eq(5)
+ expect(Project::StageDefinition.find_by(name: "Initiating")).to have_attributes(
+ color: have_attributes(name: "PM2 Orange")
+ )
+ expect(Project::GateDefinition.find_by(name: "Ready for Executing")).to have_attributes(
+ color: have_attributes(name: "PM2 Purple")
+ )
+ expect(Project::StageDefinition.find_by(name: "Planning")).to have_attributes(
+ color: have_attributes(name: "PM2 Red")
+ )
+ expect(Project::StageDefinition.find_by(name: "Executing")).to have_attributes(
+ color: have_attributes(name: "PM2 Magenta")
+ )
+ expect(Project::StageDefinition.find_by(name: "Closing")).to have_attributes(
+ color: have_attributes(name: "PM2 Green Yellow")
+ )
+ end
+
+ it "references the life cycles in the seed data" do
+ Project::LifeCycleStepDefinition.find_each do |expected_stage|
+ reference = :"default_life_cycle_#{expected_stage.name.downcase.gsub(/\s+/, '_')}"
+ expect(seed_data.find_reference(reference)).to eq(expected_stage)
+ end
+ end
+
+ context "when seeding a second time" do
+ subject(:second_seeder) { described_class.new(second_seed_data) }
+
+ let(:second_seed_data) { basic_seed_data.merge(Source::SeedData.new(data_hash)) }
+
+ before do
+ second_seeder.seed!
+ end
+
+ it "registers existing matching life cycles as references in the seed data" do
+ # using the first seed data as the expected value
+ expect(second_seed_data.find_reference(:default_life_cycle_initiating))
+ .to eq(seed_data.find_reference(:default_life_cycle_initiating))
+ expect(second_seed_data.find_reference(:default_life_cycle_ready_for_executing))
+ .to eq(seed_data.find_reference(:default_life_cycle_ready_for_executing))
+ expect(second_seed_data.find_reference(:default_life_cycle_planning))
+ .to eq(seed_data.find_reference(:default_life_cycle_planning))
+ expect(second_seed_data.find_reference(:default_life_cycle_executing))
+ .to eq(seed_data.find_reference(:default_life_cycle_executing))
+ expect(second_seed_data.find_reference(:default_life_cycle_closing))
+ .to eq(seed_data.find_reference(:default_life_cycle_closing))
+ end
+ end
+ end
+
+ context "without life cycles defined" do
+ let(:data_hash) do
+ YAML.load <<~SEEDING_DATA_YAML
+ nothing here: ''
+ SEEDING_DATA_YAML
+ end
+
+ it "creates no life cycles" do
+ expect(Project::LifeCycleStepDefinition.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/seeders/root_seeder_standard_edition_spec.rb b/spec/seeders/root_seeder_standard_edition_spec.rb
index 2d5f48c85d9f..9f416a03afe6 100644
--- a/spec/seeders/root_seeder_standard_edition_spec.rb
+++ b/spec/seeders/root_seeder_standard_edition_spec.rb
@@ -63,6 +63,8 @@
expect(VersionSetting.count).to eq 4
expect(Boards::Grid.count).to eq 5
expect(Boards::Grid.count { |grid| grid.options.has_key?(:filters) }).to eq 1
+ expect(Project::StageDefinition.count).to eq 4
+ expect(Project::GateDefinition.count).to eq 3
end
it "links work packages to their version" do
@@ -124,7 +126,7 @@
)
end
- include_examples "it creates records", model: Color, expected_count: 144
+ include_examples "it creates records", model: Color, expected_count: 149
include_examples "it creates records", model: DocumentCategory, expected_count: 3
include_examples "it creates records", model: GlobalRole, expected_count: 2
include_examples "it creates records", model: WorkPackageRole, expected_count: 3
@@ -171,6 +173,8 @@
expect(Version.count).to eq 4
expect(VersionSetting.count).to eq 4
expect(Boards::Grid.count).to eq 5
+ expect(Project::StageDefinition.count).to eq 4
+ expect(Project::GateDefinition.count).to eq 3
end
end
end
@@ -282,4 +286,27 @@
include_examples "no email deliveries"
end
+
+ context "when admin user creation is locked with OPENPROJECT_SEED_ADMIN_USER_LOCKED=true",
+ :settings_reset do
+ shared_let(:root_seeder) { described_class.new }
+
+ before_all do
+ with_env("OPENPROJECT_SEED_ADMIN_USER_LOCKED" => "true") do
+ with_edition("standard") do
+ reset(:seed_admin_user_locked)
+ root_seeder.seed_data!
+ end
+ end
+ ensure
+ reset(:seed_admin_user_locked)
+ RequestStore.clear! # resets `User.current` cached result
+ end
+
+ it "seeds without any errors, but locks the admin user", :aggregate_failures do
+ expect(Project.count).to eq 2
+ expect(WorkPackage.count).to eq 36
+ expect(root_seeder.admin_user).to be_locked
+ end
+ end
end
diff --git a/spec/seeders/source/seed_data_spec.rb b/spec/seeders/source/seed_data_spec.rb
index 2e701c2376e7..7da540555b92 100644
--- a/spec/seeders/source/seed_data_spec.rb
+++ b/spec/seeders/source/seed_data_spec.rb
@@ -62,11 +62,11 @@
it "raises an error if the reference is not found" do
expect { seed_data.find_reference(:ref) }
- .to raise_error(ArgumentError, "Nothing registered with reference :ref")
+ .to raise_error(ArgumentError, /Nothing registered with reference :ref/)
expect { seed_data.find_reference(:ref, :other_ref) }
- .to raise_error(ArgumentError, "Nothing registered with references :ref and :other_ref")
+ .to raise_error(ArgumentError, /Nothing registered with references :ref and :other_ref/)
expect { seed_data.find_reference(:ref, :other_ref, :yet_another_ref) }
- .to raise_error(ArgumentError, "Nothing registered with references :ref, :other_ref, and :yet_another_ref")
+ .to raise_error(ArgumentError, /Nothing registered with references :ref, :other_ref, and :yet_another_ref/)
end
it "returns the given default value if the reference is not found" do
diff --git a/spec/services/custom_fields/hierarchy/hierarchical_item_service_spec.rb b/spec/services/custom_fields/hierarchy/hierarchical_item_service_spec.rb
index a8c7df560826..e88ed99bf4e1 100644
--- a/spec/services/custom_fields/hierarchy/hierarchical_item_service_spec.rb
+++ b/spec/services/custom_fields/hierarchy/hierarchical_item_service_spec.rb
@@ -165,6 +165,42 @@
end
end
+ describe "#get_descendants" do
+ let!(:subitem) { service.insert_item(parent: mara, label: "Sub1").value! }
+ let!(:subitem2) { service.insert_item(parent: mara, label: "sub two").value! }
+ let!(:unrelated_subitem) { service.insert_item(parent: luke, label: "not related").value! }
+
+ context "with a non-root node" do
+ it "returns all the descendants to that item" do
+ result = service.get_descendants(item: mara)
+ expect(result).to be_success
+
+ descendants = result.value!
+ expect(descendants.size).to eq(3)
+ expect(descendants).to contain_exactly(mara, subitem, subitem2)
+ end
+ end
+
+ context "with a leaf node" do
+ it "returns just the leaf node" do
+ result = service.get_descendants(item: subitem2)
+ expect(result).to be_success
+ expect(result.value!).to match_array(subitem2)
+ end
+ end
+
+ context "when does not include self" do
+ it "returns all descendants not including the item passed" do
+ result = service.get_descendants(item: mara, include_self: false)
+ expect(result).to be_success
+
+ descendants = result.value!
+ expect(descendants.size).to eq(2)
+ expect(descendants).to contain_exactly(subitem, subitem2)
+ end
+ end
+ end
+
describe "#move_item" do
let(:lando) { service.insert_item(parent: root, label: "lando").value! }
diff --git a/spec/services/work_packages/set_schedule_service_working_days_spec.rb b/spec/services/work_packages/set_schedule_service_working_days_spec.rb
index 26aa92a63cfb..40fea3fc684d 100644
--- a/spec/services/work_packages/set_schedule_service_working_days_spec.rb
+++ b/spec/services/work_packages/set_schedule_service_working_days_spec.rb
@@ -42,8 +42,8 @@
context "with a single successor" do
context "when moving successor will cover non-working days" do
- let_schedule(<<~CHART)
- days | MTWTFSS |
+ let_work_packages(<<~CHART)
+ subject | MTWTFSS | properties
work_package | XX |
follower | XXX | follows work_package
CHART
@@ -56,8 +56,8 @@
end
it "extends to a later due date to keep the same duration" do
- expect_schedule(subject.all_results, <<~CHART)
- days | MTWTFSS |
+ expect_work_packages(subject.all_results, <<~CHART)
+ subject | MTWTFSS |
work_package | XXXX |
follower | X..XX |
CHART
diff --git a/spec/services/work_packages/update_ancestors_service_spec.rb b/spec/services/work_packages/update_ancestors_service_spec.rb
index 12ff0bc8f5ee..1590a5af0d88 100644
--- a/spec/services/work_packages/update_ancestors_service_spec.rb
+++ b/spec/services/work_packages/update_ancestors_service_spec.rb
@@ -78,7 +78,7 @@ def call_update_ancestors_service(work_package)
set_attributes_on(child, field => value)
call_update_ancestors_service(child)
- expect_work_packages([parent, child], <<~TABLE)
+ expect_work_packages_after_reload([parent, child], <<~TABLE)
| subject | status | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete |
| parent | Open | 10h | 15h | 10h | 10h | 0% | 33% |
| child | Closed | 5h | | 0h | | 100% | |
@@ -95,7 +95,7 @@ def call_update_ancestors_service(work_package)
end
it "still recomputes child remaining work and updates ancestors total % complete excluding it" do
- expect_work_packages([parent, child], <<~TABLE)
+ expect_work_packages_after_reload([parent, child], <<~TABLE)
| subject | status | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete |
| parent | Open | 10h | 10h | 10h | 10h | 0% | 0% |
| child | Rejected | 5h | | 4h | | 20% | |
@@ -119,7 +119,7 @@ def call_update_ancestors_service(work_package)
end
it "sets parent total % complete to 100% and its total remaining work to 0h" do
- expect_work_packages(table_work_packages, <<~TABLE)
+ expect_work_packages_after_reload(table_work_packages, <<~TABLE)
hierarchy | status | work | ∑ work | remaining work | ∑ remaining work | % complete | ∑ % complete
parent | Open | | 15h | | 0h | 0% | 100%
child1 | Closed | 10h | | 0h | | 100% |
@@ -386,7 +386,7 @@ def call_update_ancestors_service(work_package)
it "updates the totals of the ancestors" do
subject
- expect_work_packages([grandparent, parent, sibling, work_package], <<~TABLE)
+ expect_work_packages_after_reload([grandparent, parent, sibling, work_package], <<~TABLE)
hierarchy | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
grandparent | 3h | 1.5h | 50% | 13h | 5h | 62%
parent | 3h | 0h | 100% | 10h | 3.5h | 65%
@@ -447,7 +447,7 @@ def call_update_ancestors_service(work_package)
it "updates the totals of the ancestors" do
subject
- expect_work_packages([grandparent, parent, work_package], <<~TABLE)
+ expect_work_packages_after_reload([grandparent, parent, work_package], <<~TABLE)
hierarchy | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
grandparent | | | | 10h | 5h | 50%
parent | 3h | 1.5h | 50% | 10h | 5h | 50%
@@ -494,7 +494,7 @@ def call_update_ancestors_service(work_package)
it "updates the totals of the new parent and the former parent" do
subject
- expect_work_packages([grandparent, old_parent, new_parent, work_package], <<~TABLE)
+ expect_work_packages_after_reload([grandparent, old_parent, new_parent, work_package], <<~TABLE)
hierarchy | work | remaining work | % complete | ∑ work | ∑ remaining work | ∑ % complete
grandparent | | | | 10h | 2h | 80%
old parent | | | | | |
@@ -597,7 +597,7 @@ def call_update_ancestors_service(work_package)
it "does not update the parent total work" do
expect(call_result).to be_success
expect(call_result.dependent_results).to be_empty
- expect_work_packages([parent.reload], <<~TABLE)
+ expect_work_packages_after_reload([parent], <<~TABLE)
subject | total work |
parent | |
TABLE
@@ -693,7 +693,7 @@ def call_update_ancestors_service(work_package)
it "does not update the parent total remaining work" do
expect(call_result).to be_success
expect(call_result.dependent_results).to be_empty
- expect_work_packages([parent.reload], <<~TABLE)
+ expect_work_packages_after_reload([parent], <<~TABLE)
subject | total remaining work
parent |
TABLE
diff --git a/spec/support/components/work_packages/activities.rb b/spec/support/components/work_packages/activities.rb
index 9facc2b02d8e..5bbfc1fa4131 100644
--- a/spec/support/components/work_packages/activities.rb
+++ b/spec/support/components/work_packages/activities.rb
@@ -98,7 +98,7 @@ def expect_no_journal_notes_header(text: nil)
end
def expect_journal_notes(text: nil)
- expect(page).to have_test_selector("op-journal-notes-body", text:)
+ expect(page).to have_test_selector("op-journal-notes-body", text:, wait: 10)
end
def expect_notification_bubble
@@ -178,18 +178,20 @@ def add_comment(text: nil, save: true)
wait_for_network_idle
end
- def edit_comment(journal, text: nil)
+ def edit_comment(journal, text: nil, save: true)
within_journal_entry(journal) do
page.find_test_selector("op-wp-journal-#{journal.id}-action-menu").click
page.find_test_selector("op-wp-journal-#{journal.id}-edit").click
page.within_test_selector("op-work-package-journal-form-element") do
FormFields::Primerized::EditorFormField.new("notes", selector: "#work-package-journal-form-element").set_value(text)
- page.find_test_selector("op-submit-work-package-journal-form").click
+ page.find_test_selector("op-submit-work-package-journal-form").click if save
end
- # wait for the comment to be loaded
- wait_for { page }.to have_test_selector("op-journal-notes-body", text:)
+ if save
+ # wait for the comment to be loaded
+ wait_for { page }.to have_test_selector("op-journal-notes-body", text:)
+ end
end
end
diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb
index ea49e3969c31..cceac6de06af 100644
--- a/spec/support/components/work_packages/relations.rb
+++ b/spec/support/components/work_packages/relations.rb
@@ -34,6 +34,7 @@ class Relations
include Capybara::DSL
include Capybara::RSpecMatchers
include RSpec::Matchers
+ include RSpec::Wait
include ::Components::Autocompleter::NgSelectAutocompleteHelpers
attr_reader :work_package
@@ -42,96 +43,183 @@ def initialize(work_package)
@work_package = work_package
end
+ def find_relatable(relatable)
+ case relatable
+ when WorkPackage
+ relatable
+ when Relation
+ relatable.other_work_package(work_package)
+ else
+ raise "Unknown relatable type: #{relatable.class}"
+ end
+ end
+
+ def expect_add_relation_button
+ expect(page).to have_test_selector("new-relation-action-menu")
+ end
+
+ def expect_no_add_relation_button
+ expect(page).not_to have_test_selector("new-relation-action-menu")
+ end
+
def find_row(relatable)
- page.find(".relation-row-#{relatable.id}")
+ actual_relatable = find_relatable(relatable)
+ page.find_test_selector("op-relation-row-#{actual_relatable.id}")
end
- def click_relation(relatable)
- SeleniumHubWaiter.wait
- page.find(".relation-row-#{relatable.id} [data-test-selector='op-relation--row-id']").click
+ def find_some_row(text:)
+ page.find("[data-test-selector^='op-relation-row']", text:)
end
- def edit_relation_type(relatable, to_type:)
- row = find_row(relatable)
- SeleniumHubWaiter.wait
- row.find(".relation-row--type").click
+ def expect_row(work_package)
+ find_row(work_package)
+ end
- expect(row).to have_css("select.inline-edit--field")
- row.find(".inline-edit--field option", text: to_type).select_option
+ def expect_no_row(relatable)
+ actual_relatable = find_relatable(relatable)
+ expect(page).not_to have_test_selector("op-relation-row-#{actual_relatable.id}")
end
- def hover_action(relatable, action)
- retry_block do
- # Focus type edit to expose buttons
- span = page.find(".relation-row-#{relatable.id} [data-test-selector='op-relation--row-type']", wait: 20)
- scroll_to_element(span)
- page.driver.browser.action.move_to(span.native).perform
+ def remove_relation(relatable)
+ actual_relatable = find_relatable(relatable)
+ relatable_row = find_row(actual_relatable)
- # Click the corresponding action button
+ retry_block do
SeleniumHubWaiter.wait
- row = find_row(relatable)
- case action
- when :delete
- row.find(".relation-row--remove-btn").click
- when :info
- row.find(".wp-relations--description-btn").click
+ within(relatable_row) do
+ relatable_action_menu(actual_relatable).click
+ relatable_delete_button(actual_relatable).click
end
+
+ # Expect relation to be gone
+ expect_no_row(relatable)
end
end
- def remove_relation(relatable)
- ## Delete relation
- hover_action(relatable, :delete)
+ def relatable_action_menu(relatable)
+ actual_relatable = find_relatable(relatable)
+ page.find_test_selector("op-relation-row-#{actual_relatable.id}-action-menu")
+ end
+
+ def expect_relatable_action_menu(relatable)
+ actual_relatable = find_relatable(relatable)
+ expect(page).to have_test_selector("op-relation-row-#{actual_relatable.id}-action-menu")
+ end
+
+ def expect_no_relatable_action_menu(relatable)
+ actual_relatable = find_relatable(relatable)
+ expect(page).not_to have_test_selector("op-relation-row-#{actual_relatable.id}-action-menu")
+ end
- # Expect relation to be gone
- expect_no_relation(relatable)
+ def relatable_delete_button(relatable)
+ actual_relatable = find_relatable(relatable)
+ page.find_test_selector("op-relation-row-#{actual_relatable.id}-delete-button")
end
- def add_relation(type:, to:)
+ def expect_relatable_delete_button(relatable)
+ actual_relatable = find_relatable(relatable)
+ expect(page).to have_test_selector("op-relation-row-#{actual_relatable.id}-delete-button")
+ end
+
+ def expect_no_relatable_delete_button(relatable)
+ actual_relatable = find_relatable(relatable)
+ expect(page).not_to have_test_selector("op-relation-row-#{actual_relatable.id}-delete-button")
+ end
+
+ def add_relation(type:, relatable:, description: nil)
+ i18n_namespace = "#{WorkPackageRelationsTab::IndexComponent::I18N_NAMESPACE}.relations"
# Open create form
+
SeleniumHubWaiter.wait
- find_by_id("relation--add-relation").click
+ page.find_test_selector("new-relation-action-menu").click
- # Select relation type
- container = find(".wp-relations-create--form", wait: 10)
+ label_text_for_relation_type = I18n.t("#{i18n_namespace}.label_#{type}_singular")
+ within page.find_by_id("new-relation-action-menu-list") do # Primer appends "list" to the menu id automatically
+ click_link_or_button label_text_for_relation_type.capitalize
+ end
- # Labels to expect
- relation_label = I18n.t("js.relation_labels.#{type}")
+ wait_for_reload if using_cuprite?
- select relation_label, from: "relation-type--select"
+ # Labels to expect
+ modal_heading_label = "Add #{label_text_for_relation_type}"
+ expect(page).to have_text(modal_heading_label)
# Enter the query and select the child
- autocomplete = container.find("[data-test-selector='wp-relations-autocomplete']")
- select_autocomplete autocomplete,
- results_selector: ".ng-dropdown-panel-items",
- query: to.subject,
- select_text: to.subject
+ autocomplete_field = page.find_test_selector("work-package-relation-form-to-id")
+ select_autocomplete(autocomplete_field,
+ query: relatable.subject,
+ results_selector: "body")
+
+ if description.present?
+ fill_in "Description", with: description
+ end
+
+ click_link_or_button "Save"
+
+ wait_for_reload if using_cuprite?
+
+ label_text_for_relation_type_pluralized = I18n.t("#{i18n_namespace}.label_#{type}_plural").capitalize
+
+ wait_for { page }.to have_no_text(modal_heading_label)
+ wait_for { page }.to have_text(label_text_for_relation_type_pluralized)
+
+ new_relation = work_package.reload.relations.last
+ target_wp = new_relation.other_work_package(work_package)
+ find_row(target_wp)
+ end
+
+ def edit_relation_description(relatable, description)
+ open_relation_dialog(relatable)
+
+ within "##{WorkPackageRelationsTab::WorkPackageRelationDialogComponent::DIALOG_ID}" do
+ expect(page).to have_field("Work package", readonly: true)
+ expect(page).to have_field("Description")
- expect(page).to have_css(".relation-group--header",
- text: relation_label.upcase,
- wait: 10)
+ fill_in "Description", with: description
- expect(page).to have_css("[data-test-selector='op-relation--row-type']", text: to.type.name.upcase)
+ click_link_or_button "Save"
- expect(page).to have_css("[data-test-selector='op-relation--row-subject']", text: to.subject)
+ wait_for_reload if using_cuprite?
+ end
+ end
+
+ def edit_lag_of_relation(relatable, lag)
+ open_relation_dialog(relatable)
+
+ within "##{WorkPackageRelationsTab::WorkPackageRelationDialogComponent::DIALOG_ID}" do
+ expect(page).to have_field("Work package", readonly: true)
+ expect(page).to have_field("Lag")
+
+ fill_in "Lag", with: lag
+
+ click_link_or_button "Save"
+
+ wait_for_reload if using_cuprite?
+ end
+ end
+
+ def open_relation_dialog(relatable)
+ actual_relatable = find_relatable(relatable)
+ relation_row = find_row(actual_relatable)
- ## Test if relation exist
- work_package.reload
- relation = work_package.relations.last
- expect(relation.label_for(work_package).to_s).to eq("label_#{type}")
- expect(relation.other_work_package(work_package).id).to eq(to.id)
+ within relation_row do
+ page.find_test_selector("op-relation-row-#{actual_relatable.id}-action-menu").click
+ page.find_test_selector("op-relation-row-#{actual_relatable.id}-edit-button").click
+ end
+
+ wait_for_reload if using_cuprite?
end
def expect_relation(relatable)
- expect(relations_group).to have_css("[data-test-selector='op-relation--row-subject']", text: relatable.subject)
+ find_row(relatable)
end
def expect_relation_by_text(text)
- expect(relations_group).to have_css("[data-test-selector='op-relation--row-subject']", text:)
+ find_some_row(text:)
end
def expect_no_relation(relatable)
- expect(page).to have_no_css("[data-test-selector='op-relation--row-subject']", text: relatable.subject)
+ expect_no_row(relatable)
end
def add_parent(query, work_package)
@@ -141,7 +229,7 @@ def add_parent(query, work_package)
# Enter the query and select the child
SeleniumHubWaiter.wait
- autocomplete = find("[data-test-selector='wp-relations-autocomplete']")
+ autocomplete = page.find_test_selector("wp-relations-autocomplete")
select_autocomplete autocomplete,
query:,
results_selector: ".ng-dropdown-panel-items",
@@ -149,13 +237,13 @@ def add_parent(query, work_package)
end
def expect_parent(work_package)
- expect(page).to have_css '[data-test-selector="op-wp-breadcrumb-parent"]',
- text: work_package.subject,
- wait: 10
+ expect(page).to have_test_selector "op-wp-breadcrumb-parent",
+ text: work_package.subject,
+ wait: 10
end
def expect_no_parent
- expect(page).to have_no_css '[data-test-selector="op-wp-breadcrumb-parent"]', wait: 10
+ expect(page).not_to have_test_selector "op-wp-breadcrumb-parent", wait: 10
end
def remove_parent
@@ -163,72 +251,69 @@ def remove_parent
find(".wp-relation--parent-remove").click
end
- def inline_create_child(subject_text)
- container = find(".wp-relations--children")
- scroll_to_and_click(container.find('[data-test-selector="op-wp-inline-create"]'))
-
- subject = ::EditField.new(container, "subject")
- subject.expect_active!
- subject.update subject_text
- end
-
def open_children_autocompleter
retry_block do
next if page.has_selector?(".wp-relations--children .ng-input input")
SeleniumHubWaiter.wait
- find('[data-test-selector="op-wp-inline-create-reference"]',
- text: I18n.t("js.relation_buttons.add_existing_child")).click
+ page.find_test_selector("op-wp-inline-create-reference",
+ text: I18n.t("js.relation_buttons.add_existing_child")).click
# Security check to be sure that the autocompleter has finished loading
page.find ".wp-relations--children .ng-input input"
end
end
- def add_existing_child(work_package)
- # Enter the query and select the child
- autocomplete = page.find(".wp-relations--add-form [data-test-selector='wp-relations-autocomplete']")
- select_autocomplete autocomplete,
- query: work_package.id,
- results_selector: ".ng-dropdown-panel-items",
- select_text: work_package.subject
+ def children_table
+ page.find_test_selector("op-relation-group-children")
end
- def expect_child(work_package)
- container = find("wp-relations-hierarchy wp-children-query")
+ def add_existing_child(work_package)
+ SeleniumHubWaiter.wait
+
+ retry_block do
+ page.find_test_selector("new-relation-action-menu").click
- within container do
- expect(page)
- .to have_css(".wp-table--cell-td.subject", text: work_package.subject, wait: 10)
+ within page.find_by_id("new-relation-action-menu-list") do # Primer appends "list" to the menu id automatically
+ click_link_or_button "Child"
+ end
end
- end
- def expect_not_child(work_package)
- page.within("wp-relations-tab .work-packages-embedded-view--container") do
- row = ".wp-row-#{work_package.id}-table"
+ within "##{WorkPackageRelationsTab::AddWorkPackageChildFormComponent::DIALOG_ID}" do
+ autocomplete_field = page.find_test_selector("work-package-child-form-id")
+ select_autocomplete(autocomplete_field,
+ query: work_package.subject,
+ results_selector: "body")
- expect(page).to have_no_selector(row)
+ click_link_or_button "Save"
end
end
- def children_table
- ::Pages::EmbeddedWorkPackagesTable.new find("wp-relations-tab .work-packages-embedded-view--container")
+ def relations_group
+ page.find_by_id("work-package-relations-tab-content")
end
- def relations_group
- find("wp-relations-tab wp-relations-group")
+ def expect_relation_group(group_type)
+ expect(page).to have_test_selector("op-relation-group-#{group_type}", wait: 20)
+ end
+
+ def expect_child(work_package)
+ expect_row(work_package)
+ end
+
+ def expect_not_child(work_package)
+ expect_no_row(work_package)
end
def remove_child(work_package)
- page.within(".work-packages-embedded-view--container") do
- row = ".wp-row-#{work_package.id}-table"
+ child_wp_row = find_row(work_package)
- SeleniumHubWaiter.wait
- retry_block do
- find(row).hover
- find("#{row} .wp-table-action--unlink").click
- end
+ within(child_wp_row) do
+ relatable_action_menu(work_package).click
+ relatable_delete_button(work_package).click
end
+
+ expect_no_row(work_package)
end
end
end
diff --git a/spec/support/pages/admin/placeholder_users/index.rb b/spec/support/pages/admin/placeholder_users/index.rb
index 6176b4183cac..27939b60f995 100644
--- a/spec/support/pages/admin/placeholder_users/index.rb
+++ b/spec/support/pages/admin/placeholder_users/index.rb
@@ -37,8 +37,9 @@ def path
end
def expect_listed(*placeholder_users)
- rows = page.all "td.name"
- expect(rows.map(&:text)).to include(*placeholder_users.map(&:name))
+ placeholder_users.each do |user|
+ expect(page).to have_css("td.name", text: user.name)
+ end
end
def expect_ordered(*placeholder_users)
@@ -47,8 +48,9 @@ def expect_ordered(*placeholder_users)
end
def expect_not_listed(*users)
- rows = page.all "td.name"
- expect(rows.map(&:text)).not_to include(*users.map(&:name))
+ users.each do |user|
+ expect(page).to have_no_css("td.name", text: user.name)
+ end
end
def expect_non_listed
diff --git a/spec/support/rspec_retry.rb b/spec/support/rspec_retry.rb
index 80c3a0e75912..281fe9779792 100644
--- a/spec/support/rspec_retry.rb
+++ b/spec/support/rspec_retry.rb
@@ -21,7 +21,7 @@
# Retry JS feature specs, but not during single runs
if ENV["CI"]
config.around :each, :js do |ex|
- ex.run_with_retry retry: 2
+ ex.run_with_retry retry: ENV["RSPEC_RETRY_RETRY_COUNT"].to_i
end
end
end
diff --git a/spec/support/shared/project_life_cycle_helpers.rb b/spec/support/shared/project_life_cycle_helpers.rb
new file mode 100644
index 000000000000..44e8ecca80cc
--- /dev/null
+++ b/spec/support/shared/project_life_cycle_helpers.rb
@@ -0,0 +1,72 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+RSpec.shared_examples_for "a Project::LifeCycleStepDefinition event" do
+ it "inherits from LifeCycleStepDefinition" do
+ expect(described_class).to be < Project::LifeCycleStepDefinition
+ end
+
+ describe "associations" do
+ it { is_expected.to have_many(:life_cycle_steps).inverse_of(:definition).dependent(:destroy) }
+ it { is_expected.to have_many(:projects).through(:life_cycle_steps) }
+ it { is_expected.to belong_to(:color).required }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:name) }
+
+ it "is invalid if type is not Project::StageDefinition or Project::GateDefinition" do
+ life_cycle = described_class.new
+ life_cycle.type = "InvalidType"
+ expect(life_cycle).not_to be_valid
+ expect(life_cycle.errors[:type])
+ .to include("must be either Project::StageDefinition or Project::GateDefinition")
+ end
+ end
+end
+
+RSpec.shared_examples_for "a Project::LifeCycleStep event" do
+ it "inherits from Project::LifeCycleStep" do
+ expect(described_class).to be < Project::LifeCycleStep
+ end
+
+ describe "associations" do
+ it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:definition).required }
+ it { is_expected.to have_many(:work_packages) }
+ end
+
+ describe "validations" do
+ it "is invalid if type is not Project::Stage or Project::Gate" do
+ life_cycle = described_class.new
+ life_cycle.type = "InvalidType"
+ expect(life_cycle).not_to be_valid
+ expect(life_cycle.errors[:type]).to include("must be either Project::Stage or Project::Gate")
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column.rb b/spec/support/table_helpers/column.rb
index 30a28e825562..49700ba01bcf 100644
--- a/spec/support/table_helpers/column.rb
+++ b/spec/support/table_helpers/column.rb
@@ -29,195 +29,106 @@
#++
require_relative "identifier"
+require_relative "column_type/generic"
+require_relative "column_type/with_identifier_metadata"
+require_relative "column_type/duration"
+require_relative "column_type/hierarchy"
+require_relative "column_type/percentage"
+require_relative "column_type/properties"
+require_relative "column_type/schedule"
+require_relative "column_type/status"
+require_relative "column_type/subject"
module TableHelpers
- module Column
+ class Column
extend Identifier
+ COLUMN_TYPES = {
+ estimated_hours: ColumnType::Duration,
+ derived_estimated_hours: ColumnType::Duration,
+ remaining_hours: ColumnType::Duration,
+ derived_remaining_hours: ColumnType::Duration,
+ done_ratio: ColumnType::Percentage,
+ derived_done_ratio: ColumnType::Percentage,
+ hierarchy: ColumnType::Hierarchy,
+ properties: ColumnType::Properties,
+ schedule: ColumnType::Schedule,
+ status: ColumnType::Status,
+ subject: ColumnType::Subject,
+ __fallback__: ColumnType::Generic
+ }.freeze
+
def self.for(header)
+ new(header:, attribute: attribute_for(header))
+ end
+
+ def self.attribute_for(header)
case header
when /\A\s*estimated hours/i
raise ArgumentError, 'Please use "work" instead of "estimated hours"'
when /derived estimated hours/i
- raise ArgumentError, 'Please use "derived work" instead of "derived estimated hours"'
- when /derived remaining hours/i
- raise ArgumentError, 'Please use "derived remaining work" instead of "derived remaining hours"'
+ raise ArgumentError, 'Please use "∑ work" instead of "derived estimated hours"'
when /\A\s*remaining hours/i
raise ArgumentError, 'Please use "remaining work" instead of "remaining hours"'
+ when /derived remaining hours/i
+ raise ArgumentError, 'Please use "∑ remaining work" instead of "derived remaining hours"'
when /\A\s*work/i
- Duration.new(header:, attribute: :estimated_hours)
+ :estimated_hours
when /(∑|derived|total) work/i
- Duration.new(header:, attribute: :derived_estimated_hours)
+ :derived_estimated_hours
when /\A\s*remaining work/i
- Duration.new(header:, attribute: :remaining_hours)
+ :remaining_hours
when /(∑|derived|total) remaining work/i
- Duration.new(header:, attribute: :derived_remaining_hours)
+ :derived_remaining_hours
when /\A\s*% complete/i
- Percentage.new(header:, attribute: :done_ratio)
+ :done_ratio
when /(∑|derived|total) % complete/i
- Percentage.new(header:, attribute: :derived_done_ratio)
+ :derived_done_ratio
when /end date/i
- Generic.new(header:, attribute: :due_date)
- when /status/
- Status.new(header:)
- when /subject/
- Subject.new(header:)
- when /hierarchy/
- Hierarchy.new(header:)
+ :due_date
+ when /.*MTWTFSS.*/
+ :schedule
+ when /\s*properties\s*/
+ :properties
+ when /status/, /hierarchy/
+ to_identifier(header)
else
- assert_work_package_attribute_exists(header)
- Generic.new(header:)
- end
- end
-
- def self.assert_work_package_attribute_exists(attribute)
- attribute = to_identifier(attribute).to_s
- return if WorkPackage.attribute_names.include?(attribute)
-
- raise ArgumentError, "WorkPackage does not have an attribute named #{attribute.inspect}"
- end
-
- class Generic
- include Identifier
-
- attr_reader :attribute, :title, :raw_header
-
- def initialize(header:, attribute: nil)
- @raw_header = header
- @title = header.strip
- @attribute = attribute || to_identifier(title)
- end
-
- def format(value)
- value.to_s
- end
-
- def cell_format(value, size)
- format(value).send(text_align, size)
- end
-
- def parse(raw_value)
- raw_value.strip
- end
-
- def text_align
- :ljust
- end
-
- def attribute_value_for(work_package)
- work_package.read_attribute(attribute)
- end
-
- def read_and_update_work_packages_data(work_packages_data)
- work_packages_data.each do |work_package_data|
- work_package_data => { attributes:, row: }
- raw_value = row[raw_header]
- work_package_data.merge!(metadata_for_value(raw_value))
- attributes.merge!(attributes_for_raw_value(raw_value, work_package_data, work_packages_data))
- end
- end
-
- def attributes_for_work_package(work_package)
- { attribute => work_package.read_attribute(attribute) }
- end
-
- def attributes_for_raw_value(raw_value, _data, _work_packages_data)
- { attribute => parse(raw_value) }
- end
-
- def metadata_for_value(_raw_value)
- {}
+ attribute = to_identifier(header)
+ assert_work_package_attribute_exists(attribute)
+ attribute
end
end
- module WithIdentifierMetadata
- include Identifier
+ attr_reader :attribute, :raw_header, :title
- def metadata_for_value(raw_value, *)
- super.merge(identifier: to_identifier(raw_value))
- end
+ def initialize(header:, attribute:)
+ @raw_header = header
+ @title = header.strip
+ @attribute = attribute
end
- class Duration < Generic
- def text_align
- :rjust
- end
-
- def format(value)
- if value.nil?
- ""
- elsif value == value.truncate
- "%sh" % value.to_i
- else
- "%sh" % value
- end
- end
-
- def parse(raw_value)
- raw_value.blank? ? nil : raw_value.to_f
- end
+ def column_type
+ @column_type ||= COLUMN_TYPES.fetch(attribute, COLUMN_TYPES[:__fallback__]).new
end
- class Percentage < Generic
- def text_align
- :rjust
- end
-
- def format(value)
- if value.nil?
- ""
- else
- "%s%%" % value.to_i
- end
- end
+ delegate :format, :align, to: :column_type
- def parse(raw_value)
- raw_value.blank? ? nil : raw_value.to_i
- end
+ def attributes_for_work_package(work_package)
+ column_type.attributes_for_work_package(attribute, work_package)
end
- class Status < Generic
- def attributes_for_work_package(work_package)
- { status: work_package.status.name }
+ def read_and_update_work_packages_data(work_packages_data)
+ work_packages_data.each do |work_package_data|
+ work_package_data.deep_merge!(
+ column_type.extract_data(attribute, raw_header, work_package_data, work_packages_data)
+ )
end
end
- class Subject < Generic
- include WithIdentifierMetadata
- end
-
- class Hierarchy < Generic
- include WithIdentifierMetadata
-
- def attributes_for_work_package(work_package)
- {
- parent: to_identifier(work_package.parent&.subject),
- subject: work_package.subject
- }
- end
-
- def attributes_for_raw_value(raw_value, data, work_packages_data)
- {
- parent: find_parent(data, work_packages_data),
- subject: parse(raw_value)
- }
- end
-
- def metadata_for_value(raw_value)
- super.merge(hierarchy_indent: raw_value[/\A */].size)
- end
-
- private
-
- def find_parent(data, work_packages_data)
- return if data[:hierarchy_indent] == 0
+ def self.assert_work_package_attribute_exists(attribute)
+ return if WorkPackage.attribute_names.include?(attribute.to_s)
- work_packages_data
- .slice(0, data[:index])
- .reverse
- .find { _1[:hierarchy_indent] < data[:hierarchy_indent] }
- .then { _1&.fetch(:identifier) }
- end
+ raise ArgumentError, "WorkPackage does not have an attribute named #{attribute.inspect}"
end
end
end
diff --git a/spec/support/table_helpers/column_type/duration.rb b/spec/support/table_helpers/column_type/duration.rb
new file mode 100644
index 000000000000..31a7cd8a86c0
--- /dev/null
+++ b/spec/support/table_helpers/column_type/duration.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ # Column type used for values that represent a duration like work and
+ # remaining work.
+ #
+ # Parse hours or plain floats, for instance "2", "2h", or "3.5".
+ # Format to hours, for instance "2h" or "3.5h".
+ class Duration < Generic
+ def text_align
+ :rjust
+ end
+
+ def format(value)
+ if value.nil?
+ ""
+ elsif value == value.truncate
+ "%sh" % value.to_i
+ else
+ "%sh" % value
+ end
+ end
+
+ def parse(raw_value)
+ raw_value.blank? ? nil : raw_value.to_f
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/generic.rb b/spec/support/table_helpers/column_type/generic.rb
new file mode 100644
index 000000000000..c92f90c87449
--- /dev/null
+++ b/spec/support/table_helpers/column_type/generic.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ class Generic
+ def text_align
+ :ljust
+ end
+
+ def format(value)
+ value.to_s
+ end
+
+ def align(text, size)
+ text.send(text_align, size)
+ end
+
+ def parse(raw_value)
+ raw_value.strip
+ end
+
+ # Extracts attributes data and metadata from the work package data read
+ # from a table.
+ def extract_data(attribute, raw_header, work_package_data, work_packages_data)
+ raw_value = work_package_data.dig(:row, raw_header)
+
+ {
+ attributes: attributes_for_raw_value(attribute, raw_value, work_package_data, work_packages_data),
+ **metadata_for_raw_value(raw_value)
+ }
+ end
+
+ # Extracts attribute values data from a work package.
+ #
+ # The values are to be displayed in the table.
+ #
+ # Override if:
+ # - multiple attributes are extracted from a work package for a column
+ # type (for instance `Hierarchy` column type extracts both `subject` and
+ # `parent` attributes)
+ # - or if the value is not read from a work package attribute, but from
+ # one of its relations (for instance, `Status` column type reads
+ # `work_package.status.name` to have a string value, because
+ # `work_package.status` alone is not a string).
+ def attributes_for_work_package(attribute, work_package)
+ { attribute => work_package.read_attribute(attribute) }
+ end
+
+ # Extracts attribute values data from the raw value read from a cell.
+ #
+ # Override if:
+ # - multiple attributes are extracted from a single cell raw value for a
+ # column type (for instance `Hierarchy` column type extracts both
+ # `subject` and `parent` attributes)
+ def attributes_for_raw_value(attribute, raw_value, _data, _work_packages_data)
+ { attribute => parse(raw_value) }
+ end
+
+ # Extracts metadata from the raw value read from a cell.
+ #
+ # The metadata is useful to store additional information about the row.
+ #
+ # Override when needing to store additional metadata. For instance the
+ # `Subject` column type stores the work package identifier as metadata.
+ def metadata_for_raw_value(_raw_value)
+ {}
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/hierarchy.rb b/spec/support/table_helpers/column_type/hierarchy.rb
new file mode 100644
index 000000000000..1fabed0c02d4
--- /dev/null
+++ b/spec/support/table_helpers/column_type/hierarchy.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ # Adds the `:identifier` metadata to the row.
+ #
+ # The `:identifier` value is the key being used to identify a work package
+ # by its variable name. It's also the variable name to be used when using
+ # the work packages in the tests.
+ class Hierarchy < Generic
+ include WithIdentifierMetadata
+
+ def attributes_for_work_package(_attribute, work_package)
+ {
+ parent: to_identifier(work_package.parent&.subject),
+ subject: work_package.subject
+ }
+ end
+
+ def attributes_for_raw_value(_attribute, raw_value, data, work_packages_data)
+ {
+ parent: find_parent(raw_value, data, work_packages_data),
+ subject: parse(raw_value)
+ }
+ end
+
+ def metadata_for_raw_value(raw_value)
+ super.merge(hierarchy_indent: hierarchy_indent(raw_value))
+ end
+
+ private
+
+ def hierarchy_indent(raw_value)
+ raw_value[/\A */].size
+ end
+
+ def find_parent(raw_value, data, work_packages_data)
+ hierarchy_indent = hierarchy_indent(raw_value)
+ return if hierarchy_indent == 0
+
+ work_packages_data
+ .slice(0, data[:index])
+ .reverse
+ .find { _1[:hierarchy_indent] < hierarchy_indent }
+ .then { _1&.fetch(:identifier) }
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/percentage.rb b/spec/support/table_helpers/column_type/percentage.rb
new file mode 100644
index 000000000000..141903b20180
--- /dev/null
+++ b/spec/support/table_helpers/column_type/percentage.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+module TableHelpers
+ module ColumnType
+ # Column type used for values that represent a percentage like % complete.
+ #
+ # Parse percentage or plain integers, for instance "10", "20%", or "100%".
+ # Format to percentage, for instance "2%" or "35%".
+ class Percentage < Generic
+ def text_align
+ :rjust
+ end
+
+ def format(value)
+ if value.nil?
+ ""
+ else
+ "%s%%" % value.to_i
+ end
+ end
+
+ def parse(raw_value)
+ raw_value.blank? ? nil : raw_value.to_i
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/properties.rb b/spec/support/table_helpers/column_type/properties.rb
new file mode 100644
index 000000000000..2a550c8f1b09
--- /dev/null
+++ b/spec/support/table_helpers/column_type/properties.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ # Column to add properties to work packages like "follows wp1 with lag 2".
+ #
+ # Supported properties:
+ # - follows :wp
+ # - follows :wp with lag :int
+ #
+ # Example:
+ #
+ # | subject | properties |
+ # | main | |
+ # | follower | follows main with lag 2 |
+ # | follower2 | follows follower |
+ #
+ # Adapted from original implementation in `spec/support/schedule_helpers/chart_builder.rb`.
+ class Properties < Generic
+ def attributes_for_work_package(_attribute, _work_package)
+ {}
+ end
+
+ def extract_data(_attribute, raw_header, work_package_data, _work_packages_data)
+ properties = work_package_data.dig(:row, raw_header)
+ properties = properties.split(",").map(&:strip).compact_blank
+ parse_properties(properties)
+ end
+
+ def parse_properties(properties)
+ properties.reduce({}) do |data, property|
+ case parse_property(property)
+ in {relations: relation}
+ data[:relations] ||= []
+ data[:relations] << relation
+ end
+ data
+ end
+ end
+
+ def relations_for_raw_value(raw_value)
+ raw_value.split
+ {}
+ end
+
+ def parse_property(property)
+ case property
+ when /^follows (\w+)(?: with lag (\d+))?/
+ {
+ relations: {
+ raw: property,
+ type: :follows,
+ predecessor: $1,
+ lag: $2.to_i
+ }
+ }
+ else
+ spell_checker = DidYouMean::SpellChecker.new(
+ dictionary: [
+ "follows :wp",
+ "follows :wp with lag :int"
+ ]
+ )
+ suggestions = spell_checker.correct(property).map(&:inspect).join(" ")
+ did_you_mean = " Did you mean #{suggestions} instead?" if suggestions.present?
+ raise "unable to parse property #{property.inspect}.#{did_you_mean}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/schedule.rb b/spec/support/table_helpers/column_type/schedule.rb
new file mode 100644
index 000000000000..072691365370
--- /dev/null
+++ b/spec/support/table_helpers/column_type/schedule.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ # Column to specify start date and due date using an ascii calendar.
+ #
+ # The title of the column is "MTWTFSS" to represent the days of the week.
+ #
+ # The Monday is the next occuring Monday meaning the dates can be different
+ # from one test run to another. To always use the same dates please use
+ # `travel_to` with a fixed date.
+ #
+ # Use 'X' to mark the days from start to due date. Only the first and last
+ # 'X' are considered as start and due dates.
+ # Use '[' to mark the start date only and ']' for the due date only.
+ # Any other character is ignored
+ #
+ # Example:
+ #
+ # | subject | MTWTFSS |
+ # | main | XXX |
+ # | crossing non working days | XXXX..XX |
+ # | start date only | [ |
+ # | due date only | ] |
+ # | no dates | |
+ #
+ # Adapted from original implementation in `spec/support/schedule_helpers/chart_builder.rb`.
+ class Schedule < Generic
+ def attributes_for_work_package(_attribute, work_package)
+ {
+ start_date: work_package.start_date,
+ due_date: work_package.due_date
+ }
+ end
+
+ def extract_data(_attribute, raw_header, work_package_data, _work_packages_data)
+ raw_value = work_package_data.dig(:row, raw_header)
+
+ {
+ attributes: scheduling_attributes(raw_header, raw_value),
+ **metadata_for_raw_value(raw_value)
+ }
+ end
+
+ private
+
+ def scheduling_attributes(reference, timespan)
+ nb_days_from_origin_monday = reference.index("M")
+
+ start_pos = timespan.index("[") || timespan.index("X")
+ due_pos = timespan.rindex("]") || timespan.rindex("X")
+ {
+ start_date: start_pos && (current_monday - nb_days_from_origin_monday + start_pos),
+ due_date: due_pos && (current_monday - nb_days_from_origin_monday + due_pos)
+ }
+ end
+
+ def current_monday
+ Date.current.next_occurring(:monday)
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/status.rb b/spec/support/table_helpers/column_type/status.rb
new file mode 100644
index 000000000000..4ac8f7e2cb2c
--- /dev/null
+++ b/spec/support/table_helpers/column_type/status.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ class Status < Generic
+ def attributes_for_work_package(_attribute, work_package)
+ { status: work_package.status.name }
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/subject.rb b/spec/support/table_helpers/column_type/subject.rb
new file mode 100644
index 000000000000..ad8ddb292154
--- /dev/null
+++ b/spec/support/table_helpers/column_type/subject.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ class Subject < Generic
+ include WithIdentifierMetadata
+ end
+ end
+end
diff --git a/spec/support/table_helpers/column_type/with_identifier_metadata.rb b/spec/support/table_helpers/column_type/with_identifier_metadata.rb
new file mode 100644
index 000000000000..e24003067232
--- /dev/null
+++ b/spec/support/table_helpers/column_type/with_identifier_metadata.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module TableHelpers
+ module ColumnType
+ # Adds the `:identifier` metadata to the row.
+ #
+ # The `:identifier` value is the key being used to identify a work package
+ # by its variable name. It's also the variable name to be used when using
+ # the work packages in the tests.
+ module WithIdentifierMetadata
+ include Identifier
+
+ def metadata_for_raw_value(raw_value, *)
+ super.merge(identifier: to_identifier(raw_value))
+ end
+ end
+ end
+end
diff --git a/spec/support/table_helpers/example_methods.rb b/spec/support/table_helpers/example_methods.rb
index cb9f6f04c7fc..e55c693a0355 100644
--- a/spec/support/table_helpers/example_methods.rb
+++ b/spec/support/table_helpers/example_methods.rb
@@ -51,13 +51,42 @@ def create_table(table_representation)
# Expect the given work packages to match a visual table representation.
#
+ # It uses +match_table+ internally. It does not reload the work packages
+ # before comparing. To reload, use `expect_work_packages_after_reload`
+ #
+ # For instance:
+ #
+ # it 'is scheduled' do
+ # expect_work_packages(work_packages, <<~TABLE)
+ # subject | work | derived work |
+ # parent | 1h | 3h |
+ # child | 2h | 2h |
+ # TABLE
+ # end
+ #
+ # is equivalent to:
+ #
+ # it 'is scheduled' do
+ # expect(work_packages).to match_table(<<~TABLE)
+ # subject | work | derived work |
+ # parent | 1h | 3h |
+ # child | 2h | 2h |
+ # TABLE
+ # end
+ def expect_work_packages(work_packages, table_representation)
+ expect(work_packages).to match_table(table_representation)
+ end
+
+ # Expect the given work packages to match a visual table representation
+ # after being reloaded.
+ #
# It uses +match_table+ internally and reloads the work packages from
# database before comparing.
#
# For instance:
#
# it 'is scheduled' do
- # expect_work_packages(work_packages, <<~TABLE)
+ # expect_work_packages_after_reload(work_packages, <<~TABLE)
# subject | work | derived work |
# parent | 1h | 3h |
# child | 2h | 2h |
@@ -74,9 +103,9 @@ def create_table(table_representation)
# child | 2h | 2h |
# TABLE
# end
- def expect_work_packages(work_packages, table_representation)
+ def expect_work_packages_after_reload(work_packages, table_representation)
work_packages.each(&:reload)
- expect(work_packages).to match_table(table_representation)
+ expect_work_packages(work_packages, table_representation)
end
end
end
diff --git a/spec/support/table_helpers/table_data.rb b/spec/support/table_helpers/table_data.rb
index fa4a8a6b572a..136ba7752fba 100644
--- a/spec/support/table_helpers/table_data.rb
+++ b/spec/support/table_helpers/table_data.rb
@@ -101,9 +101,13 @@ def initialize(table_data)
end
def create
- table_data.work_package_identifiers.map do |identifier|
+ table_data.work_package_identifiers.each do |identifier|
create_work_package(identifier)
end
+ # create relations only after having created all work packages
+ table_data.work_package_identifiers.each do |identifier| # rubocop:disable Style/CombinableLoops
+ create_follows_relations(identifier)
+ end
work_packages_by_identifier
end
@@ -118,6 +122,20 @@ def create_work_package(identifier)
end
end
+ def create_follows_relations(identifier)
+ relations = work_package_relations(identifier)
+ relations.each do |relation|
+ predecessor = work_packages_by_identifier[relation[:predecessor].to_sym]
+ follower = work_packages_by_identifier[identifier]
+ FactoryBot.create(
+ :follows_relation,
+ from: follower,
+ to: predecessor,
+ lag: relation[:lag]
+ )
+ end
+ end
+
def lookup_parent(identifier)
if identifier
@work_packages_by_identifier[identifier] || create_work_package(identifier)
@@ -137,9 +155,16 @@ def statuses_by_name
@statuses_by_name ||= Status.all.index_by(&:name)
end
+ def work_package_data(identifier)
+ table_data.work_packages_data.find { |wpa| wpa[:identifier] == identifier.to_sym }
+ end
+
def work_package_attributes(identifier)
- data = table_data.work_packages_data.find { |wpa| wpa[:identifier] == identifier.to_sym }
- data[:attributes]
+ work_package_data(identifier)[:attributes]
+ end
+
+ def work_package_relations(identifier)
+ work_package_data(identifier)[:relations] || []
end
end
end
diff --git a/spec/support/table_helpers/table_parser.rb b/spec/support/table_helpers/table_parser.rb
index f7c8735ea473..33dbe77ec687 100644
--- a/spec/support/table_helpers/table_parser.rb
+++ b/spec/support/table_helpers/table_parser.rb
@@ -31,26 +31,42 @@
module TableHelpers
class TableParser
def parse(representation)
+ work_packages_data = parse_representation(representation)
+ populate_work_package_data(work_packages_data)
+ end
+
+ def parse_representation(representation)
headers, *rows = representation.split("\n").filter_map { |line| split_line_into_cells(line) }
- work_packages_data = rows.map.with_index do |cells, index|
+ rows.map.with_index do |cells, index|
if cells.size > headers.size
raise ArgumentError, "Too many cells in row #{index + 1}, have you forgotten some headers?"
end
+ cells << "" while cells.size < headers.size
{
attributes: {},
index:,
row: headers.zip(cells).to_h
}
end
- headers.each do |header|
- column = Column.for(header)
+ end
+
+ private
+
+ def populate_work_package_data(work_packages_data)
+ columns(work_packages_data).each do |column|
column.read_and_update_work_packages_data(work_packages_data)
end
work_packages_data
end
- private
+ def headers(work_packages_data)
+ work_packages_data.first[:row].keys
+ end
+
+ def columns(work_packages_data)
+ work_packages_data.first[:row].keys.map { |key| Column.for(key) }
+ end
def split_line_into_cells(line)
case line
diff --git a/spec/support/table_helpers/table_representer.rb b/spec/support/table_helpers/table_representer.rb
index 126ea1fca8c5..31b1123c9d42 100644
--- a/spec/support/table_helpers/table_representer.rb
+++ b/spec/support/table_helpers/table_representer.rb
@@ -35,29 +35,107 @@ def initialize(tables_data:, columns:)
@columns = columns
end
- # rubocop:disable Style/MultilineBlockChain
def render(table_data)
- column_and_cell_sizes
- .map do |column, cell_size|
- header = column.title.ljust(cell_size)
- cells = table_data.values_for_attribute(column.attribute).map { column.cell_format(_1, cell_size) }
- [header, *cells]
- end
+ columns
+ .map { |column| formatted_cells_for_column(column, table_data) }
.transpose
.map { |row| "| #{row.join(' | ')} |\n" }
.join
end
- # rubocop:enable Style/MultilineBlockChain
+
+ def formatted_cells_for_column(column, table_data)
+ get_header_and_values(column, table_data)
+ .then { |cells| normalize_width(cells, column) }
+ end
+
+ def get_header_and_values(column, table_data)
+ if column.attribute == :schedule
+ header = schedule_column_representer.column_title
+ start_dates = table_data.values_for_attribute(:start_date)
+ due_dates = table_data.values_for_attribute(:due_date)
+ values = start_dates.zip(due_dates).map do |start_date, due_date|
+ schedule_column_representer.span(start_date, due_date)
+ end
+ else
+ header = column.title
+ values = table_data
+ .values_for_attribute(column.attribute)
+ .map! { column.format(_1) }
+ end
+ [header, *values]
+ end
+
+ def normalize_width(cells, column)
+ header, *values = cells
+ width = column_width(column)
+ header = header.ljust(width)
+ values.map! { column.align(_1, width) }
+ [header, *values]
+ end
private
- def column_and_cell_sizes
- @column_and_cell_sizes ||=
+ def column_width(column)
+ column_widths[column]
+ end
+
+ # Calculate the width of each column given values from all tables data.
+ def column_widths
+ @column_widths ||=
columns.index_with do |column|
- values = tables_data.flat_map { _1.values_for_attribute(column.attribute) }
- values_max_size = values.map { column.format(_1).size }.max
- [column.title.size, values_max_size].max
+ if column.attribute == :schedule
+ schedule_column_representer.column_size
+ else
+ values = tables_data.flat_map { _1.values_for_attribute(column.attribute) }
+ values_max_size = values.map { column.format(_1).size }.max
+ [column.title.size, values_max_size].max
+ end
end
end
+
+ def schedule_column_representer
+ @schedule_column_representer ||= begin
+ start_dates = tables_data.flat_map { _1.values_for_attribute(:start_date) }
+ due_dates = tables_data.flat_map { _1.values_for_attribute(:due_date) }
+ ScheduleColumnFormatter.new(start_dates, due_dates)
+ end
+ end
+
+ class ScheduleColumnFormatter
+ attr_reader :monday, :first_day, :last_day
+
+ def initialize(start_dates, due_dates)
+ @monday = Date.current.next_occurring(:monday)
+ all_dates = start_dates + due_dates + [@monday, @monday + 6]
+ @first_day, @last_day = all_dates.flatten.compact.minmax
+ end
+
+ def column_size
+ (first_day..last_day).count
+ end
+
+ def column_title
+ spaced_at(monday, "MTWTFSS")
+ end
+
+ def span(start_date, due_date)
+ if start_date.nil? && due_date.nil?
+ " " * column_size
+ elsif due_date.nil?
+ spaced_at(start_date, "[")
+ elsif start_date.nil?
+ spaced_at(due_date, "]")
+ else
+ span = "X" * (start_date..due_date).count
+ spaced_at(start_date, span)
+ end
+ end
+
+ def spaced_at(date, text)
+ nb_days = date - first_day
+ spaced = (" " * nb_days) + text
+ spaced.ljust(column_size)
+ end
+ end
end
end
diff --git a/spec/support_spec/table_helpers/column_spec.rb b/spec/support_spec/table_helpers/column_type/duration_spec.rb
similarity index 57%
rename from spec/support_spec/table_helpers/column_spec.rb
rename to spec/support_spec/table_helpers/column_type/duration_spec.rb
index a421b45d6b75..85d0f25b43ae 100644
--- a/spec/support_spec/table_helpers/column_spec.rb
+++ b/spec/support_spec/table_helpers/column_type/duration_spec.rb
@@ -30,57 +30,35 @@
require "spec_helper"
-module TableHelpers::Column
- RSpec.describe Generic do
- subject(:column) { described_class.new(header: "Some header") }
-
- describe "#format" do
- it "renders the value as string" do
- expect(column.format("hello")).to eq "hello"
- expect(column.format(42)).to eq "42"
- expect(column.format(3.5)).to eq "3.5"
- expect(column.format(nil)).to eq ""
- expect(column.format(true)).to eq "true"
- end
- end
-
- describe "#cell_format" do
- it "renders the value on the left side of the cell" do
- expect(column.cell_format("hello", 0)).to eq "hello"
- expect(column.cell_format("hello", 10)).to eq "hello "
- expect(column.cell_format("hello", 20)).to eq "hello "
- end
- end
- end
-
+module TableHelpers::ColumnType
RSpec.describe Duration do
- subject(:column) { described_class.new(header: "Duration in hours") }
+ subject(:column_type) { described_class.new }
describe "#parse" do
it "parses empty string as nil" do
- expect(column.parse("")).to be_nil
+ expect(column_type.parse("")).to be_nil
end
end
describe "#format" do
it 'renders the duration with a "h" suffix' do
- expect(column.format(3.5)).to eq "3.5h"
+ expect(column_type.format(3.5)).to eq "3.5h"
end
it "renders the duration without the decimal part if the decimal part is 0" do
- expect(column.format(3.0)).to eq "3h"
+ expect(column_type.format(3.0)).to eq "3h"
end
it "renders nothing if nil" do
- expect(column.format(nil)).to eq ""
+ expect(column_type.format(nil)).to eq ""
end
end
- describe "#cell_format" do
+ describe "#align" do
it "renders the duration on the right side of the cell" do
- expect(column.cell_format(3.5, 0)).to eq "3.5h"
- expect(column.cell_format(3.5, 10)).to eq " 3.5h"
- expect(column.cell_format(3.5, 20)).to eq " 3.5h"
+ expect(column_type.align("3.5h", 0)).to eq "3.5h"
+ expect(column_type.align("3.5h", 10)).to eq " 3.5h"
+ expect(column_type.align("3.5h", 20)).to eq " 3.5h"
end
end
end
diff --git a/spec/support_spec/table_helpers/column_type/generic_spec.rb b/spec/support_spec/table_helpers/column_type/generic_spec.rb
new file mode 100644
index 000000000000..f9c1f8413731
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/generic_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Generic do
+ subject(:column_type) { described_class.new }
+
+ describe "#format" do
+ it "renders the value as string" do
+ expect(column_type.format("hello")).to eq "hello"
+ expect(column_type.format(42)).to eq "42"
+ expect(column_type.format(3.5)).to eq "3.5"
+ expect(column_type.format(nil)).to eq ""
+ expect(column_type.format(true)).to eq "true"
+ end
+ end
+
+ describe "#align" do
+ it "renders the value on the left side of the cell" do
+ expect(column_type.align("hello", 0)).to eq "hello"
+ expect(column_type.align("hello", 10)).to eq "hello "
+ expect(column_type.align("hello", 20)).to eq "hello "
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/hierarchy_spec.rb b/spec/support_spec/table_helpers/column_type/hierarchy_spec.rb
new file mode 100644
index 000000000000..03c98ea5c887
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/hierarchy_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Hierarchy do
+ subject(:column_type) { described_class.new }
+
+ describe "#extract_data" do
+ let(:attribute) { :hierarchy }
+ let(:subject_raw_header) { "Hierarchy " }
+ let(:work_packages_data) do
+ [
+ {
+ index: 0,
+ row: { subject_raw_header => "Parent " }
+ },
+ {
+ index: 1,
+ row: { subject_raw_header => " Child1 " }
+ },
+ {
+ index: 2,
+ row: { subject_raw_header => " Child2 " }
+ },
+ {
+ index: 3,
+ row: { subject_raw_header => " Grand Child" }
+ }
+ ]
+ end
+
+ it "extracts the identifier metadata along with the :subject attribute value" do
+ # check first row only
+ expect(column_type.extract_data(attribute, subject_raw_header, work_packages_data.first, work_packages_data))
+ .to include({ attributes: a_hash_including(subject: "Parent"),
+ identifier: :parent })
+ end
+
+ it "extracts the hierarchy_indent metadata as the number of spaces before the name, " \
+ "and the :parent attribute value holding the identifier of the parent from previous rows" do
+ # first row
+ work_package_data = work_packages_data.first
+ row_extract = column_type.extract_data(attribute, subject_raw_header, work_package_data, work_packages_data)
+ expect(row_extract).to include({ attributes: a_hash_including(parent: nil),
+ hierarchy_indent: 0 })
+
+ # second row
+ work_package_data.deep_merge!(row_extract)
+ work_package_data = work_packages_data.second
+ row_extract = column_type.extract_data(attribute, subject_raw_header, work_package_data, work_packages_data)
+ expect(row_extract).to include({ attributes: a_hash_including(parent: :parent),
+ hierarchy_indent: 2,
+ identifier: :child1 })
+
+ # third row
+ work_package_data.deep_merge!(row_extract)
+ work_package_data = work_packages_data.third
+ row_extract = column_type.extract_data(attribute, subject_raw_header, work_package_data, work_packages_data)
+ expect(row_extract).to include({ attributes: a_hash_including(parent: :parent),
+ hierarchy_indent: 2,
+ identifier: :child2 })
+
+ # fourth row
+ work_package_data.deep_merge!(row_extract)
+ work_package_data = work_packages_data.fourth
+ row_extract = column_type.extract_data(attribute, subject_raw_header, work_package_data, work_packages_data)
+ expect(row_extract).to include({ attributes: a_hash_including(parent: :child2),
+ hierarchy_indent: 5,
+ identifier: :grand_child })
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/percentage_spec.rb b/spec/support_spec/table_helpers/column_type/percentage_spec.rb
new file mode 100644
index 000000000000..b2b32c72f423
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/percentage_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Percentage do
+ subject(:column_type) { described_class.new }
+
+ describe "#parse" do
+ it "parses empty string as nil" do
+ expect(column_type.parse("")).to be_nil
+ end
+ end
+
+ describe "#format" do
+ it 'renders the percentage with a "%" suffix' do
+ expect(column_type.format(30)).to eq "30%"
+ expect(column_type.format(30.9)).to eq "30%"
+ end
+
+ it "renders nothing if nil" do
+ expect(column_type.format(nil)).to eq ""
+ end
+ end
+
+ describe "#align" do
+ it "renders the percentage on the right side of the cell" do
+ expect(column_type.align("20%", 0)).to eq "20%"
+ expect(column_type.align("35%", 10)).to eq " 35%"
+ expect(column_type.align("57%", 20)).to eq " 57%"
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/properties_spec.rb b/spec/support_spec/table_helpers/column_type/properties_spec.rb
new file mode 100644
index 000000000000..094fa7e305e1
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/properties_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Properties do
+ subject(:column_type) { described_class.new }
+
+ def parsed_data(table)
+ TableHelpers::TableParser.new.parse(table)
+ end
+
+ describe "empty" do
+ it "stores nothing when empty" do
+ work_package_data = parsed_data(<<~TABLE).first
+ | properties |
+ | |
+ TABLE
+ expect(work_package_data[:relations]).to be_nil
+ expect(work_package_data[:attributes]).to be_empty
+
+ work_package_data = parsed_data(<<~TABLE).first
+ | properties
+ |
+ TABLE
+ expect(work_package_data[:relations]).to be_nil
+ expect(work_package_data[:attributes]).to be_empty
+ end
+ end
+
+ describe "follows [with lag ]" do
+ it "stores follows relations in work_package_data" do
+ work_package_data = parsed_data(<<~TABLE).first
+ | properties |
+ | follows main with lag 3 |
+ TABLE
+ expect(work_package_data[:relations])
+ .to eq([{ raw: "follows main with lag 3", type: :follows, predecessor: "main", lag: 3 }])
+ end
+
+ it "has a default lag of 0 days when not specified" do
+ work_package_data = parsed_data(<<~TABLE).first
+ | properties |
+ | follows main |
+ TABLE
+ expect(work_package_data[:relations])
+ .to eq([{ raw: "follows main", type: :follows, predecessor: "main", lag: 0 }])
+ end
+
+ it "can store multiple relations" do
+ work_package_data = parsed_data(<<~TABLE).first
+ | properties |
+ | follows wp1, follows wp2 |
+ TABLE
+ expect(work_package_data[:relations])
+ .to eq([
+ { raw: "follows wp1", type: :follows, predecessor: "wp1", lag: 0 },
+ { raw: "follows wp2", type: :follows, predecessor: "wp2", lag: 0 }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/schedule_spec.rb b/spec/support_spec/table_helpers/column_type/schedule_spec.rb
new file mode 100644
index 000000000000..011a0b55e17c
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/schedule_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Schedule do
+ let(:fake_today) { Date.new(2022, 6, 16) } # Thursday 16 June 2022
+ let(:monday) { Date.new(2022, 6, 20) } # Monday 20 June
+ let(:tuesday) { Date.new(2022, 6, 21) }
+ let(:wednesday) { Date.new(2022, 6, 22) }
+ let(:thursday) { Date.new(2022, 6, 23) }
+ let(:friday) { Date.new(2022, 6, 24) }
+ let(:saturday) { Date.new(2022, 6, 25) }
+ let(:sunday) { Date.new(2022, 6, 26) }
+
+ def parsed_attributes(table)
+ work_packages_data = TableHelpers::TableParser.new.parse(table)
+ work_packages_data.pluck(:attributes)
+ end
+
+ before do
+ travel_to(fake_today)
+ end
+
+ describe "origin day" do
+ it "is identified by the 'M' in 'MTWTFSS' in the header and corresponds to the next monday" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | X |
+ TABLE
+ .to eq([{ start_date: monday, due_date: monday }])
+ end
+
+ it "is not identified by mtwtfss which can be used as documentation instead" do
+ expect(parsed_attributes(<<~TABLE))
+ | mtwtfssMTWTFSSmtwtfss |
+ | X |
+ TABLE
+ .to eq([{ start_date: monday, due_date: monday }])
+ end
+ end
+
+ describe "work package dates extraction" do
+ def parsed_attributes(table)
+ work_packages_data = TableHelpers::TableParser.new.parse(table)
+ work_packages_data.pluck(:attributes)
+ end
+
+ it "recognizes multiple 'X' as the duration spanning from start date and end date" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | XX |
+ TABLE
+ .to eq([{ start_date: monday, due_date: tuesday }])
+ end
+
+ it "recognizes start date and end date outside of the reference week" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | XXXXXX |
+ | XXXXXX |
+ TABLE
+ .to eq([
+ { start_date: thursday, due_date: tuesday + 7.days },
+ { start_date: thursday - 7.days, due_date: tuesday }
+ ])
+ end
+
+ it "recognizes '[' as start date, making the due date nil if there are no 'X' or ']' after it" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | [ |
+ TABLE
+ .to eq([{ start_date: tuesday, due_date: nil }])
+ end
+
+ it "recognizes ']' as end date, making the start date nil if there are no 'X' or '[' before it" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | ] |
+ TABLE
+ .to eq([{ start_date: nil, due_date: thursday }])
+ end
+
+ it "sets start date and due date to nil if there are no 'X', '[', or ']' at all" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | |
+ TABLE
+ .to eq([{ start_date: nil, due_date: nil }])
+ end
+
+ it "ignores characters other than 'X', '[', or ']', allowing to use other characters to " \
+ "represent other things (like '.' for non-working days)" do
+ expect(parsed_attributes(<<~TABLE))
+ | MTWTFSS |
+ | XX..XX |
+ TABLE
+ .to include(start_date: thursday, due_date: tuesday + 7.days)
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/status_spec.rb b/spec/support_spec/table_helpers/column_type/status_spec.rb
new file mode 100644
index 000000000000..0b5ed0fba05e
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/status_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Status do
+ subject(:column_type) { described_class.new }
+
+ describe "#attributes_for_work_package" do
+ let(:status) { build(:status, name: "New") }
+ let(:work_package) { build(:work_package, status:) }
+
+ it "uses the status name as the value for status attribute" do
+ expect(column_type.attributes_for_work_package(:status, work_package))
+ .to eq(status: "New")
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/column_type/subject_spec.rb b/spec/support_spec/table_helpers/column_type/subject_spec.rb
new file mode 100644
index 000000000000..0693157a8b56
--- /dev/null
+++ b/spec/support_spec/table_helpers/column_type/subject_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+module TableHelpers::ColumnType
+ RSpec.describe Subject do
+ subject(:column_type) { described_class.new }
+
+ describe "#extract_data" do
+ it "extracts the identifier metadata along with the :subject attribute value" do
+ attribute = :subject
+ subject_raw_header = "subject "
+ work_packages_data =
+ [
+ {
+ index: 0,
+ row: { subject_raw_header => " Work package 2 " }
+ }
+ ]
+ work_package_data = work_packages_data.first
+
+ expect(column_type.extract_data(attribute, subject_raw_header, work_package_data, work_packages_data))
+ .to eq({ attributes: { subject: "Work package 2" },
+ identifier: :work_package2 })
+ end
+ end
+ end
+end
diff --git a/spec/support_spec/table_helpers/table_data_spec.rb b/spec/support_spec/table_helpers/table_data_spec.rb
index fe3fd0adcffc..edb060e9e61f 100644
--- a/spec/support_spec/table_helpers/table_data_spec.rb
+++ b/spec/support_spec/table_helpers/table_data_spec.rb
@@ -71,6 +71,31 @@ module TableHelpers
expect(table_data.values_for_attribute(:remaining_hours)).to eq([3.0, nil])
expect(table_data.work_package_identifiers).to eq(%i[work_package another_one])
end
+
+ it "can read schedule column data from work packages" do
+ expected_table = <<~TABLE
+ | subject | MTWTFSS |
+ | work package 1 | XXX |
+ | work package 2 | ] |
+ TABLE
+
+ columns = described_class.for(expected_table).columns
+ monday = Date.current.next_occurring(:monday)
+ work_package1 = build(:work_package, subject: "work package 1",
+ start_date: monday - 2, due_date: monday)
+ work_package2 = build(:work_package, subject: "work package 2",
+ start_date: nil, due_date: monday + 4)
+
+ table_data = described_class.from_work_packages([work_package1, work_package2], columns)
+ expect(table_data.work_packages_data.size).to eq(2)
+ expect(table_data.columns.size).to eq(2)
+ expect(table_data.headers).to eq(["subject", "MTWTFSS"])
+ expect(table_data.values_for_attribute(:start_date))
+ .to eq([monday - 2, nil])
+ expect(table_data.values_for_attribute(:due_date))
+ .to eq([monday, monday + 4])
+ expect(table_data.work_package_identifiers).to eq(%i[work_package1 work_package2])
+ end
end
describe "#headers" do
@@ -112,18 +137,43 @@ module TableHelpers
end
describe "#create_work_packages" do
+ let(:monday) { Date.current.next_occurring(:monday) }
+
it "creates work packages out of the table data" do
status = create(:status, name: "To do")
table_representation = <<~TABLE
- subject | status | work |
- My wp | To do | 5h |
+ subject | status | work | MTWTFSS |
+ My wp | To do | 5h | XXX |
TABLE
table_data = described_class.for(table_representation)
table = table_data.create_work_packages
expect(table.work_packages.count).to eq(1)
expect(table.work_package(:my_wp))
- .to have_attributes(subject: "My wp", status:, estimated_hours: 5.0)
+ .to have_attributes(
+ subject: "My wp",
+ status:,
+ estimated_hours: 5.0,
+ start_date: monday,
+ due_date: monday + 2.days
+ )
+ end
+
+ it "creates relations between work packages out of the table data" do
+ table_representation = <<~TABLE
+ subject | properties
+ main |
+ follower | follows main with lag 2
+ TABLE
+
+ table_data = described_class.for(table_representation)
+ table = table_data.create_work_packages
+ expect(table.work_packages.count).to eq(2)
+ main = table.work_package(:main)
+ follower = table.work_package(:follower)
+ expect(follower.follows_relations.count).to eq(1)
+ expect(follower.follows_relations.first.to).to eq(main)
+ expect(follower.follows_relations.first.lag).to eq(2)
end
it "raises an error if a given status name does not exist" do
diff --git a/spec/support_spec/table_helpers/table_parser_spec.rb b/spec/support_spec/table_helpers/table_parser_spec.rb
index 4512646fb1f2..f45cdb63a1cc 100644
--- a/spec/support_spec/table_helpers/table_parser_spec.rb
+++ b/spec/support_spec/table_helpers/table_parser_spec.rb
@@ -82,13 +82,15 @@
.to raise_error(ArgumentError, "Too many cells in row 1, have you forgotten some headers?")
end
- it "is ok to have more headers than cells (value of missing cells will be nil)" do
+ it "is ok to have more headers than cells (raw values of missing cells will be empty strings)" do
table = <<~TABLE
subject | work | remaining work
wp | 4h
TABLE
parsed_data = described_class.new.parse(table)
+ expect(parsed_data.dig(0, :row, " work ")).to eq(" 4h")
expect(parsed_data.dig(0, :attributes, :estimated_hours)).to eq(4.0)
+ expect(parsed_data.dig(0, :row, " remaining work")).to eq("")
expect(parsed_data.dig(0, :attributes, :remaining_hours)).to be_nil
end
diff --git a/spec/support_spec/table_helpers/table_representer_spec.rb b/spec/support_spec/table_helpers/table_representer_spec.rb
index 02db8fa197e9..575b4c91b51e 100644
--- a/spec/support_spec/table_helpers/table_representer_spec.rb
+++ b/spec/support_spec/table_helpers/table_representer_spec.rb
@@ -100,5 +100,68 @@ module TableHelpers
TABLE
end
end
+
+ describe "schedule column" do
+ let(:table) do
+ <<~TABLE
+ | subject | MTWTFSS |
+ | wp1 | X |
+ | wp2 | [ |
+ | wp3 | ] |
+ | wp4 | |
+ | wp5 | XXX |
+ | wp6 | X |
+ | wp7 | X |
+ | wp8 | X |
+ TABLE
+ end
+ let(:columns) { [Column.for("MTWTFSS")] }
+
+ it "is rendered as a schedule" do
+ expect(representer.render(table_data)).to eq <<~TABLE
+ | MTWTFSS |
+ | X |
+ | [ |
+ | ] |
+ | |
+ | XXX |
+ | X |
+ | X |
+ | X |
+ TABLE
+ end
+
+ context "when using a second table for the size" do
+ let(:twin_table) do
+ <<~TABLE
+ | subject | MTWTFSS |
+ | wp5 | XXX |
+ | wp9 | XX |
+ TABLE
+ end
+ let(:twin_table_data) { TableData.for(twin_table) }
+
+ let(:tables_data) { [table_data, twin_table_data] }
+
+ it "adapts the column size to the largest of both tables so they are diffable" do
+ expect(representer.render(table_data)).to eq <<~TABLE
+ | MTWTFSS |
+ | X |
+ | [ |
+ | ] |
+ | |
+ | XXX |
+ | X |
+ | X |
+ | X |
+ TABLE
+ expect(representer.render(twin_table_data)).to eq <<~TABLE
+ | MTWTFSS |
+ | XXX |
+ | XX |
+ TABLE
+ end
+ end
+ end
end
end
diff --git a/spec/workers/work_packages/progress/apply_statuses_change_job_spec.rb b/spec/workers/work_packages/progress/apply_statuses_change_job_spec.rb
index a626c2d08e50..464f3f376746 100644
--- a/spec/workers/work_packages/progress/apply_statuses_change_job_spec.rb
+++ b/spec/workers/work_packages/progress/apply_statuses_change_job_spec.rb
@@ -65,8 +65,7 @@ def expect_performing_job_changes(from:, to:,
job.perform_now(cause_type:, status_name:, status_id:, changes:)
- table.work_packages.map(&:reload)
- expect_work_packages(table.work_packages, to)
+ expect_work_packages_after_reload(table.work_packages, to)
table.work_packages
end
diff --git a/spec/workers/work_packages/progress/apply_total_percent_complete_mode_change_job_spec.rb b/spec/workers/work_packages/progress/apply_total_percent_complete_mode_change_job_spec.rb
index 63527a3a1a23..7dd516021290 100644
--- a/spec/workers/work_packages/progress/apply_total_percent_complete_mode_change_job_spec.rb
+++ b/spec/workers/work_packages/progress/apply_total_percent_complete_mode_change_job_spec.rb
@@ -63,8 +63,7 @@ def expect_performing_job_changes(from:, to:,
job.perform_now(cause_type:, mode:)
- table.work_packages.map(&:reload)
- expect_work_packages(table.work_packages, to)
+ expect_work_packages_after_reload(table.work_packages, to)
table.work_packages
end
diff --git a/spec/workers/work_packages/progress/migrate_remove_totals_from_childless_work_packages_job_spec.rb b/spec/workers/work_packages/progress/migrate_remove_totals_from_childless_work_packages_job_spec.rb
index a9c88dbd37df..3fe40e92d1a6 100644
--- a/spec/workers/work_packages/progress/migrate_remove_totals_from_childless_work_packages_job_spec.rb
+++ b/spec/workers/work_packages/progress/migrate_remove_totals_from_childless_work_packages_job_spec.rb
@@ -46,7 +46,7 @@
def expect_performing_job_changes(from:, to:)
table = create_table(from)
job.perform_now
- expect_work_packages(table.work_packages.map(&:reload), to)
+ expect_work_packages_after_reload(table.work_packages, to)
table.work_packages
end