From 63354212e4cd9ee54bc0b889d420a7509b0b5963 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Tue, 7 May 2024 10:55:39 +0200 Subject: [PATCH 01/51] [#53669] Change default view for meetings module to upcoming invitations https://community.openproject.org/work_packages/53669 From 53e313b4037fa671deb2f2cd6aeefec7ea638fc2 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Tue, 7 May 2024 17:49:26 +0200 Subject: [PATCH 02/51] Update default to show upcoming invitations --- .../app/controllers/meetings_controller.rb | 42 ++++++++++--------- .../meeting/app/helpers/meetings_helper.rb | 15 ++++--- .../meetings_global_menu_item_spec.rb | 4 +- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index d71402442d9c..02ec1c341366 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -58,7 +58,7 @@ class MeetingsController < ApplicationController def index @query = load_query @meetings = load_meetings(@query) - render 'index', locals: { menu_name: project_or_global_menu } + render "index", locals: { menu_name: project_or_global_menu } end current_menu_item :index do @@ -70,11 +70,11 @@ def show if @meeting.is_a?(StructuredMeeting) render(Meetings::ShowComponent.new(meeting: @meeting, project: @project)) elsif @meeting.agenda.present? && @meeting.agenda.locked? - params[:tab] ||= 'minutes' + params[:tab] ||= "minutes" end end - def create + def create # rubocop:disable Metrics/AbcSize call = if @copy_from ::Meetings::CopyService @@ -90,14 +90,15 @@ def create text = I18n.t(:notice_successful_create) if User.current.time_zone.nil? link = I18n.t(:notice_timezone_missing, zone: Time.zone) - text += " #{view_context.link_to(link, { controller: '/my', action: :settings, anchor: 'pref_time_zone' }, class: 'link_to_profile')}" + text += " #{view_context.link_to(link, { controller: '/my', action: :settings, anchor: 'pref_time_zone' }, + class: 'link_to_profile')}" end flash[:notice] = text.html_safe # rubocop:disable Rails/OutputSafety - redirect_to action: 'show', id: call.result + redirect_to action: "show", id: call.result else @meeting = call.result - render template: 'meetings/new', project_id: @project, locals: { copy_from: @copy_from } + render template: "meetings/new", project_id: @project, locals: { copy_from: @copy_from } end end @@ -114,13 +115,13 @@ def copy .call(save: false) @meeting = call.result - render action: 'new', project_id: @project, locals: { copy_from: } + render action: "new", project_id: @project, locals: { copy_from: } end def destroy @meeting.destroy flash[:notice] = I18n.t(:notice_successful_delete) - redirect_to action: 'index', project_id: @project + redirect_to action: "index", project_id: @project end def edit @@ -154,9 +155,9 @@ def update @meeting.attributes = @converted_params if @meeting.save flash[:notice] = I18n.t(:notice_successful_update) - redirect_to action: 'show', id: @meeting + redirect_to action: "show", id: @meeting else - render action: 'edit' + render action: "edit" end end @@ -238,7 +239,7 @@ def notify flash[:notice] = I18n.t(:notice_successful_notification) else flash[:error] = I18n.t(:error_notification_with_errors, - recipients: result.errors.map(&:name).join('; ')) + recipients: result.errors.map(&:name).join("; ")) end redirect_to action: :show, id: @meeting @@ -255,7 +256,7 @@ def load_query query = apply_default_filter_if_none_given(query) if @project - query.where("project_id", '=', @project.id) + query.where("project_id", "=", @project.id) end query @@ -265,6 +266,7 @@ def apply_default_filter_if_none_given(query) return query if query.filters.any? query.where("time", "=", Queries::Meetings::Filters::TimeFilter::FUTURE_VALUE) + query.where("invited_user_id", "=", [User.current.id.to_s]) end def load_meetings(query) @@ -335,15 +337,15 @@ def structured_meeting_params def meeting_type(given_type) case given_type - when 'dynamic' - 'StructuredMeeting' + when "dynamic" + "StructuredMeeting" else - 'Meeting' + "Meeting" end end def verify_activities_module_activated - render_403 if @project && !@project.module_enabled?('activity') + render_403 if @project && !@project.module_enabled?("activity") end def set_activity @@ -370,8 +372,8 @@ def determine_date_range if params[:from] begin - ; @date_to = params[:from].to_date + 1.day; - rescue StandardError; + @date_to = params[:from].to_date + 1.day + rescue StandardError end end @@ -393,8 +395,8 @@ def find_copy_from_meeting def copy_attributes { - copy_agenda: params[:copy_agenda] == '1', - copy_attachments: params[:copy_attachments] == '1', + copy_agenda: params[:copy_agenda] == "1", + copy_attachments: params[:copy_attachments] == "1" } end end diff --git a/modules/meeting/app/helpers/meetings_helper.rb b/modules/meeting/app/helpers/meetings_helper.rb index 0ba105163506..cbf86094711c 100644 --- a/modules/meeting/app/helpers/meetings_helper.rb +++ b/modules/meeting/app/helpers/meetings_helper.rb @@ -44,7 +44,12 @@ def involvement_sidebar_menu_items end def menu_upcoming_meetings_item - path = project_or_global_meetings_path + path = project_or_global_meetings_path( + filters: [ + { time: { operator: "=", values: ["future"] } } + ], + sort: "start_time" + ) menu_link_element path, t(:label_upcoming_meetings) end @@ -59,13 +64,7 @@ def menu_past_meetings_item end def menu_upcoming_invitations_item - path = project_or_global_meetings_path( - filters: [ - { time: { operator: "=", values: ["future"] } }, - { invited_user_id: { operator: "=", values: [User.current.id.to_s] } } - ], - sort: "start_time" - ) + path = project_or_global_meetings_path menu_link_element path, t(:label_upcoming_invitations) end diff --git a/modules/meeting/spec/features/meetings_global_menu_item_spec.rb b/modules/meeting/spec/features/meetings_global_menu_item_spec.rb index d849d29f2e82..c489fb98320a 100644 --- a/modules/meeting/spec/features/meetings_global_menu_item_spec.rb +++ b/modules/meeting/spec/features/meetings_global_menu_item_spec.rb @@ -55,9 +55,9 @@ expect(page).to have_current_path("/meetings") end - specify '"Upcoming meetings" is the default filter set' do + specify '"Upcoming invitations" is the default filter set' do within "#main-menu" do - expect(page).to have_css(".selected", text: I18n.t(:label_upcoming_meetings)) + expect(page).to have_css(".selected", text: I18n.t(:label_upcoming_invitations)) end end end From a537057a589bf0acbf5d9bf88eaa9af5d72e85a0 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Tue, 7 May 2024 17:50:07 +0200 Subject: [PATCH 03/51] Revert date range setting override left in --- modules/meeting/app/controllers/meetings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 02ec1c341366..54118a886a81 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -368,7 +368,7 @@ def activity_scope end def determine_date_range - @days = 31 # Setting.activity_days_default.to_i + @days = Setting.activity_days_default.to_i if params[:from] begin From df473fcf61de6ea03c51bf516e11717266780e4f Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Mon, 13 May 2024 12:14:24 +0200 Subject: [PATCH 04/51] Update spec --- .../meeting/spec/controllers/meetings_controller_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb index 59ba28840d30..838588abed56 100644 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ b/modules/meeting/spec/controllers/meetings_controller_spec.rb @@ -47,9 +47,9 @@ describe "index" do let(:meetings) do [ - create(:meeting, project:), - create(:meeting, project:), - create(:meeting, project: other_project) + create(:meeting, author: user, project:), + create(:meeting, author: user, project:), + create(:meeting, author: user, project: other_project) ] end @@ -58,7 +58,6 @@ before do get "index" end - it { expect(response).to be_successful } it { expect(assigns(:meetings)).to match_array meetings } end From 6f17b46874190170066ef7c7724a932f2ef2e794 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Mon, 13 May 2024 12:19:29 +0200 Subject: [PATCH 05/51] Update spec to be more meaningful --- .../meeting/spec/controllers/meetings_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb index 838588abed56..3a89a0d6f1c2 100644 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ b/modules/meeting/spec/controllers/meetings_controller_spec.rb @@ -47,7 +47,7 @@ describe "index" do let(:meetings) do [ - create(:meeting, author: user, project:), + create(:meeting, project:), create(:meeting, author: user, project:), create(:meeting, author: user, project: other_project) ] @@ -59,7 +59,7 @@ get "index" end it { expect(response).to be_successful } - it { expect(assigns(:meetings)).to match_array meetings } + it { expect(assigns(:meetings)).to match_array meetings[1..2] } end context "when requesting meetings scoped to a project ID" do @@ -68,7 +68,7 @@ end it { expect(response).to be_successful } - it { expect(assigns(:meetings)).to match_array meetings[0..1] } + it { expect(assigns(:meetings)).to match_array meetings[1] } end end end From 70e2a64d1556abb251de7fd613d5192562485af4 Mon Sep 17 00:00:00 2001 From: Mir Bhatia Date: Tue, 14 May 2024 12:20:01 +0200 Subject: [PATCH 06/51] Update and refactor specs --- .../spec/features/meetings_delete_spec.rb | 3 + .../spec/features/meetings_index_spec.rb | 58 +++++++++++-------- .../spec/features/meetings_show_spec.rb | 4 ++ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/modules/meeting/spec/features/meetings_delete_spec.rb b/modules/meeting/spec/features/meetings_delete_spec.rb index 46d59594b09c..cc250b7bf342 100644 --- a/modules/meeting/spec/features/meetings_delete_spec.rb +++ b/modules/meeting/spec/features/meetings_delete_spec.rb @@ -45,6 +45,9 @@ let(:index_path) { project_meetings_path(project) } before do + create(:meeting_participant, :invitee, user:, meeting:) + create(:meeting_participant, :invitee, user:, meeting: other_meeting) + login_as(user) end diff --git a/modules/meeting/spec/features/meetings_index_spec.rb b/modules/meeting/spec/features/meetings_index_spec.rb index 9bda13f026e5..e6ad768f830e 100644 --- a/modules/meeting/spec/features/meetings_index_spec.rb +++ b/modules/meeting/spec/features/meetings_index_spec.rb @@ -81,7 +81,7 @@ create(:meeting, project:, title: "Awesome meeting yesterday!", start_time: 1.day.ago) end - shared_let(:other_project_meeting) do + let(:other_project_meeting) do create(:meeting, project: other_project, title: "Awesome other project meeting!", @@ -89,25 +89,30 @@ duration: 2.0, location: "not-a-url") end + let(:ongoing_meeting) do + create(:meeting, project:, title: "Awesome ongoing meeting!", start_time: 30.minutes.ago) + end def setup_meeting_involvement - create(:meeting_participant, :invitee, user:, meeting: tomorrows_meeting) - create(:meeting_participant, :invitee, user:, meeting: yesterdays_meeting) + invite_to_meeting(tomorrows_meeting) + invite_to_meeting(yesterdays_meeting) create(:meeting_participant, :attendee, user:, meeting:) meeting.update!(author: user) end + def invite_to_meeting(meeting) + create(:meeting_participant, :invitee, user:, meeting:) + end + before do login_as user end shared_examples "sidebar filtering" do |context:| context "when filtering with the sidebar" do - shared_let(:ongoing_meeting) do - create(:meeting, project:, title: "Awesome ongoing meeting!", start_time: 30.minutes.ago) - end - before do + ongoing_meeting + other_project_meeting setup_meeting_involvement meetings_page.visit! end @@ -199,9 +204,10 @@ def setup_meeting_involvement context "when visiting from a global context" do let(:meetings_page) { Pages::Meetings::Index.new(project: nil) } - it "lists all upcoming meetings for all projects the user has access to" do - meeting - yesterdays_meeting + it "lists all upcoming meetings for all projects the user is invited to" do + invite_to_meeting(meeting) + invite_to_meeting(yesterdays_meeting) + invite_to_meeting(other_project_meeting) meetings_page.navigate_by_modules_menu meetings_page.expect_meetings_listed(meeting, other_project_meeting) @@ -209,10 +215,11 @@ def setup_meeting_involvement end it "renders a link to each meeting's location if present and a valid URL" do - meeting - meeting_with_no_location - meeting_with_malicious_location - tomorrows_meeting + invite_to_meeting(meeting) + invite_to_meeting(meeting_with_no_location) + invite_to_meeting(meeting_with_malicious_location) + invite_to_meeting(tomorrows_meeting) + invite_to_meeting(other_project_meeting) meetings_page.visit! @@ -245,7 +252,8 @@ def setup_meeting_involvement describe "sorting" do before do - meeting + invite_to_meeting(meeting) + invite_to_meeting(other_project_meeting) visit meetings_path # Start Time ASC is the default sort order for Upcoming meetings # We can assert the initial sort by expecting the order is @@ -338,16 +346,16 @@ def setup_meeting_involvement include_examples "sidebar filtering", context: :project specify "with 1 meeting listed" do - meeting + invite_to_meeting(meeting) meetings_page.visit! meetings_page.expect_meetings_listed(meeting) end it "with pagination", with_settings: { per_page_options: "1" } do - meeting - tomorrows_meeting - yesterdays_meeting + invite_to_meeting(meeting) + invite_to_meeting(tomorrows_meeting) + invite_to_meeting(yesterdays_meeting) # First page displays the soonest occurring upcoming meeting meetings_page.visit! @@ -365,10 +373,10 @@ def setup_meeting_involvement end it "renders a link to each meeting's location if present and a valid URL" do - meeting - meeting_with_no_location - meeting_with_malicious_location - tomorrows_meeting + invite_to_meeting(meeting) + invite_to_meeting(meeting_with_no_location) + invite_to_meeting(meeting_with_malicious_location) + invite_to_meeting(tomorrows_meeting) meetings_page.visit! meetings_page.expect_link_to_meeting_location(meeting) @@ -379,8 +387,8 @@ def setup_meeting_involvement describe "sorting" do before do - meeting - tomorrows_meeting + invite_to_meeting(meeting) + invite_to_meeting(tomorrows_meeting) meetings_page.visit! # Start Time ASC is the default sort order for Upcoming meetings # We can assert the initial sort by expecting the order is diff --git a/modules/meeting/spec/features/meetings_show_spec.rb b/modules/meeting/spec/features/meetings_show_spec.rb index 922e56c5799e..45b84ccdec3c 100644 --- a/modules/meeting/spec/features/meetings_show_spec.rb +++ b/modules/meeting/spec/features/meetings_show_spec.rb @@ -43,6 +43,10 @@ current_user { user } describe "navigate to meeting page" do + before do + create(:meeting_participant, :invitee, user:, meeting:) + end + let(:permissions) { %i[view_meetings] } it "can visit the meeting" do From 755c3e09aeaadf2ce4e98ef00e966c6ad0a78fba Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Sun, 19 May 2024 14:42:57 +0200 Subject: [PATCH 07/51] Add public flag to ProjectQuery --- app/models/queries/projects/project_query.rb | 3 +++ db/migrate/20240519123921_add_public_to_project_queries.rb | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 db/migrate/20240519123921_add_public_to_project_queries.rb diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index 6a44a497199a..6dab3947a2fa 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -36,6 +36,9 @@ class Queries::Projects::ProjectQuery < ApplicationRecord serialize :orders, coder: Queries::Serialization::Orders.new(self) serialize :selects, coder: Queries::Serialization::Selects.new(self) + scope :public, -> { where(public: true) } + scope :private, ->(user = User.current) { where(public: false, user:) } + def self.model Project end diff --git a/db/migrate/20240519123921_add_public_to_project_queries.rb b/db/migrate/20240519123921_add_public_to_project_queries.rb new file mode 100644 index 000000000000..fa2190b01b9f --- /dev/null +++ b/db/migrate/20240519123921_add_public_to_project_queries.rb @@ -0,0 +1,6 @@ +class AddPublicToProjectQueries < ActiveRecord::Migration[7.1] + def change + add_column :project_queries, :public, :boolean, default: false, null: false + add_index :project_queries, :public + end +end From e57eb91d5a1642d48c614a66cd2417f8c60983a5 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Sun, 19 May 2024 15:04:43 +0200 Subject: [PATCH 08/51] Add `manage_public_project_queries` permission globally --- config/initializers/permissions.rb | 10 +++++++++- config/locales/en.yml | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 0f474ddcbb9d..799b24da2d8f 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -112,7 +112,7 @@ map.permission :select_project_custom_fields, { - 'projects/settings/project_custom_fields': %i[show toggle enable_all_of_section disable_all_of_section] + "projects/settings/project_custom_fields": %i[show toggle enable_all_of_section disable_all_of_section] }, permissible_on: :project, require: :member @@ -178,6 +178,14 @@ permissible_on: :global, require: :loggedin, grant_to_admin: true + + map.permission :manage_public_project_queries, + { + "projects/queries": %i[publish unpublish] + }, + permissible_on: :globa, + require: :loggedin, + grant_to_admin: true end map.project_module :work_package_tracking, order: 90 do |wpt| diff --git a/config/locales/en.yml b/config/locales/en.yml index 258ba4841285..f1334e537ab4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2832,6 +2832,7 @@ Project attributes and sections are defined in the Date: Sun, 19 May 2024 15:04:59 +0200 Subject: [PATCH 09/51] Rename scopes to remove duplication --- app/models/queries/projects/project_query.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index 6dab3947a2fa..7cfba892f808 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -36,8 +36,8 @@ class Queries::Projects::ProjectQuery < ApplicationRecord serialize :orders, coder: Queries::Serialization::Orders.new(self) serialize :selects, coder: Queries::Serialization::Selects.new(self) - scope :public, -> { where(public: true) } - scope :private, ->(user = User.current) { where(public: false, user:) } + scope :public_lists, -> { where(public: true) } + scope :private_lists, ->(user = User.current) { where(public: false, user:) } def self.model Project From 3768e72b6273a7d4aec03b937c721654f3130c0a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Sun, 19 May 2024 16:07:12 +0200 Subject: [PATCH 10/51] Fix typo in permission scope --- config/initializers/permissions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 799b24da2d8f..c92ee82c1f11 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -183,7 +183,7 @@ { "projects/queries": %i[publish unpublish] }, - permissible_on: :globa, + permissible_on: :global, require: :loggedin, grant_to_admin: true end From ca6e583cdbdbd9b6407bd609e33b96e9ebc93e18 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Sun, 19 May 2024 16:07:25 +0200 Subject: [PATCH 11/51] Add methods to publish and unpublish a project list based on permission --- .../index_page_header_component.html.erb | 23 +++++++- .../projects/index_page_header_component.rb | 4 ++ app/components/projects/row_component.rb | 42 +++++++-------- app/components/projects/table_component.rb | 18 ++++--- .../projects/queries/publish_contract.rb | 39 ++++++++++++++ .../projects/queries_controller.rb | 52 +++++++++++-------- app/controllers/projects/query_loading.rb | 10 ++-- .../project_queries/publish_service.rb | 50 ++++++++++++++++++ config/routes.rb | 3 ++ 9 files changed, 187 insertions(+), 54 deletions(-) create mode 100644 app/contracts/projects/queries/publish_contract.rb create mode 100644 app/services/queries/projects/project_queries/publish_service.rb diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index 88189b02fea8..dff8c5e76b1c 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -81,6 +81,28 @@ end if query.persisted? + if can_publish? + if query.public? + menu.with_item( + label: t(:button_unpublish), + scheme: :danger, + href: unpublish_projects_query_path(query), + content_arguments: { data: { method: :post } } + ) do |item| + item.with_leading_visual_icon(icon: 'eye-closed') + end + else + menu.with_item( + label: t(:button_publish), + scheme: :default, + href: publish_projects_query_path(query), + content_arguments: { data: { method: :post } } + ) do |item| + item.with_leading_visual_icon(icon: 'eye') + end + end + end + menu.with_item( label: t(:button_delete), scheme: :danger, @@ -92,7 +114,6 @@ end end %> - <%= render(Projects::ConfigureViewModalComponent.new(query:)) %> <%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %> <%= render(Projects::ExportListModalComponent.new(query:)) %> diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index fc7f6939bef6..15f19a7ba0ce 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -74,6 +74,10 @@ def can_save? = can_save_as? && query.persisted? && query.user == current_user def can_rename? = may_save_as? && query.persisted? && query.user == current_user && !query.changed? + def can_publish? + current_user.allowed_globally?(:manage_public_project_queries) && query.persisted? + end + def show_state? state == :show end diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 6dd80c99c2cd..11b55719c5a5 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -46,19 +46,19 @@ def hierarchy def favored render(Primer::Beta::IconButton.new( - icon: currently_favored? ? "star-fill" : "star", - scheme: :invisible, - mobile_icon: currently_favored? ? "star-fill" : "star", - size: :medium, - tag: :a, - tooltip_direction: :e, - href: helpers.build_favorite_path(project, format: :html), - data: { method: currently_favored? ? :delete : :post }, - classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite", - label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite), - aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) }, - test_selector: 'project-list-favorite-button' - )) + icon: currently_favored? ? "star-fill" : "star", + scheme: :invisible, + mobile_icon: currently_favored? ? "star-fill" : "star", + size: :medium, + tag: :a, + tooltip_direction: :e, + href: helpers.build_favorite_path(project, format: :html), + data: { method: currently_favored? ? :delete : :post }, + classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite", + label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite), + aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) }, + test_selector: "project-list-favorite-button" + )) end def currently_favored? @@ -197,7 +197,7 @@ def column_css_class(column) def additional_css_class(column) if column.attribute == :name "project--hierarchy #{project.archived? ? 'archived' : ''}" - elsif [:status_explanation, :description].include?(column.attribute) + elsif %i[status_explanation description].include?(column.attribute) "project-long-text-container" elsif custom_field_column?(column) cf = column.custom_field @@ -254,7 +254,7 @@ def more_menu_favorite_item href: helpers.build_favorite_path(project, format: :html), data: { method: :post }, label: I18n.t(:button_favorite), - aria: { label: I18n.t(:button_favorite) }, + aria: { label: I18n.t(:button_favorite) } } end @@ -269,7 +269,7 @@ def more_menu_unfavorite_item data: { method: :delete }, classes: "op-primer--star-icon", label: I18n.t(:button_unfavorite), - aria: { label: I18n.t(:button_unfavorite) }, + aria: { label: I18n.t(:button_unfavorite) } } end @@ -302,7 +302,7 @@ def more_menu_activity_item scheme: :default, icon: :check, label: I18n.t(:label_project_activity), - href: project_activity_index_path(project, event_types: ["project_attributes"]), + href: project_activity_index_path(project, event_types: ["project_attributes"]) } end end @@ -317,7 +317,7 @@ def more_menu_archive_item data: { confirm: t("project.archive.are_you_sure", name: project.name), method: :post - }, + } } end end @@ -340,7 +340,7 @@ def more_menu_copy_item scheme: :default, icon: :copy, label: I18n.t(:button_copy), - href: copy_project_path(project), + href: copy_project_path(project) } end end @@ -351,7 +351,7 @@ def more_menu_delete_item scheme: :danger, icon: :trash, label: I18n.t(:button_delete), - href: confirm_destroy_project_path(project), + href: confirm_destroy_project_path(project) } end end @@ -361,7 +361,7 @@ def user_can_view_project? end def custom_field_column?(column) - column.is_a?(Queries::Projects::Selects::CustomField) + column.is_a?(::Queries::Projects::Selects::CustomField) end end end diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index 3c053819422b..a2dea976cff6 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -64,11 +64,18 @@ def build_sort_header(column, options) # We don't return the project row # but the [project, level] array from the helper def rows +<<<<<<< HEAD @rows ||= begin projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } instance_exec(model, &projects_enumerator) end +======= + @rows ||= begin + projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } + instance_exec(model, &projects_enumerator) + end +>>>>>>> 0e1a80b408 (Add methods to publish and unpublish a project list based on permission) end def initialize_sorted_model @@ -113,12 +120,11 @@ def sortable_column?(select) end def columns - @columns ||= - begin - columns = query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) } + @columns ||= begin + columns = query.selects.reject { |select| select.is_a?(::Queries::Selects::NotExistingSelect) } - index = columns.index { |column| column.attribute == :name } - columns.insert(index, Queries::Projects::Selects::Default.new(:hierarchy)) if index + index = columns.index { |column| column.attribute == :name } + columns.insert(index, ::Queries::Projects::Selects::Default.new(:hierarchy)) if index columns end @@ -156,7 +162,7 @@ def projects_with_level(projects, &) end def favored_project_ids - @favored_projects ||= Favorite.where(user: current_user, favored_type: 'Project').pluck(:favored_id) + @favored_projects ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id) end def sorted_by_lft? diff --git a/app/contracts/projects/queries/publish_contract.rb b/app/contracts/projects/queries/publish_contract.rb new file mode 100644 index 000000000000..ccf53f5ff9bf --- /dev/null +++ b/app/contracts/projects/queries/publish_contract.rb @@ -0,0 +1,39 @@ +#-- 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. +#++ + +class Projects::Queries::PublishContract < BaseContract + validate :allowed_to_modify_public_flag + + protected + + def allowed_to_modify_public_flag + unless user.allowed_globally?(:manage_public_project_queries) + errors.add :base, :error_unauthorized + end + end +end diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index 273733778fe8..23439331915f 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -31,7 +31,7 @@ class Projects::QueriesController < ApplicationController # No need for a more specific authorization check. That is carried out in the contracts. before_action :require_login - before_action :find_query, only: %i[rename update destroy] + before_action :find_query, only: %i[rename update destroy publish unpublish] before_action :build_query_or_deny_access, only: %i[new create] current_menu_item [:new, :rename, :create, :update] do @@ -55,17 +55,7 @@ def create .new(from: @query, user: current_user) .call(permitted_query_params) - if call.success? - flash[:notice] = I18n.t("lists.create.success") - - redirect_to projects_path(query_id: call.result.id) - else - flash[:error] = I18n.t("lists.create.failure", errors: call.errors.full_messages.join("\n")) - - render template: "/projects/index", - layout: "global", - locals: { query: call.result, state: :edit } - end + render_result(call, success_i18n_key: "lists.create.success", error_i18n_key: "lists.create.failure") end def update @@ -73,17 +63,23 @@ def update .new(user: current_user, model: @query) .call(permitted_query_params) - if call.success? - flash[:notice] = I18n.t("lists.update.success") + render_result(call, success_i18n_key: "lists.update.success", error_i18n_key: "lists.update.failure") + end - redirect_to projects_path(query_id: call.result.id) - else - flash[:error] = I18n.t("lists.update.failure", errors: call.errors.full_messages.join("\n")) + def publish + call = Queries::Projects::ProjectQueries::PublishService + .new(user: current_user, model: @query) + .call(public: true) - render template: "/projects/index", - layout: "global", - locals: { query: call.result, state: :edit } - end + render_result(call, success_i18n_key: "lists.publish.success", error_i18n_key: "lists.publish.failure") + end + + def unpublish + call = Queries::Projects::ProjectQueries::PublishService + .new(user: current_user, model: @query) + .call(public: false) + + render_result(call, success_i18n_key: "lists.unpublish.success", error_i18n_key: "lists.unpublish.failure") end def destroy @@ -95,6 +91,20 @@ def destroy private + def render_result(service_call, success_i18n_key:, error_i18n_key:) + if service_call.success? + flash[:notice] = I18n.t(success_i18n_key) + + redirect_to projects_path(query_id: service_call.result.id) + else + flash[:error] = I18n.t(error_i18n_key, errors: service_call.errors.full_messages.join("\n")) + + render template: "/projects/index", + layout: "global", + locals: { query: service_call.result, state: :edit } + end + end + def find_query @query = Queries::Projects::ProjectQuery.find(params[:id]) end diff --git a/app/controllers/projects/query_loading.rb b/app/controllers/projects/query_loading.rb index 1d0edfa44eac..c7679d9b9b8a 100644 --- a/app/controllers/projects/query_loading.rb +++ b/app/controllers/projects/query_loading.rb @@ -30,10 +30,10 @@ module QueryLoading private def load_query(duplicate:) - Queries::Projects::Factory.find(params[:query_id], - params: permitted_query_params, - user: current_user, - duplicate:) + ::Queries::Projects::Factory.find(params[:query_id], + params: permitted_query_params, + user: current_user, + duplicate:) end def load_query_or_deny_access @@ -55,7 +55,7 @@ def permitted_query_params query_params.merge!(params.require(:query).permit(:name)) end - query_params.merge!(Queries::ParamsParser.parse(params)) + query_params.merge!(::Queries::ParamsParser.parse(params)) query_params.with_indifferent_access end diff --git a/app/services/queries/projects/project_queries/publish_service.rb b/app/services/queries/projects/project_queries/publish_service.rb new file mode 100644 index 000000000000..ad68a1bc45d1 --- /dev/null +++ b/app/services/queries/projects/project_queries/publish_service.rb @@ -0,0 +1,50 @@ +#-- 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. +#++ + +class Queries::Projects::ProjectQueries::PublishService < BaseServices::BaseContracted + include Contracted + + def initialize(user:, model:, contract_class: Projects::Queries::PublishContract) + super(user:, contract_class:) + self.model = model + end + + private + + def after_validate(params, service_call) + model.public = params[:public] + + service_call + end + + def persist(service_call) + model.save + + service_call + end +end diff --git a/config/routes.rb b/config/routes.rb index 7fda512c194f..80c29d6a84e4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -184,6 +184,9 @@ resources :queries, only: %i[new create update destroy] do member do get :rename + + post :publish + post :unpublish end end end From 1954629b84d1975e48842931026e615034522980 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 12:24:29 +0200 Subject: [PATCH 12/51] Add feature flag for Project List sharing while in development --- app/components/projects/index_page_header_component.html.erb | 2 +- config/initializers/feature_decisions.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index dff8c5e76b1c..ef38c2243013 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -81,7 +81,7 @@ end if query.persisted? - if can_publish? + if can_publish? && OpenProject::FeatureDecisions.project_list_sharing_active? if query.public? menu.with_item( label: t(:button_unpublish), diff --git a/config/initializers/feature_decisions.rb b/config/initializers/feature_decisions.rb index e98381d9be1e..54ed096da78c 100644 --- a/config/initializers/feature_decisions.rb +++ b/config/initializers/feature_decisions.rb @@ -38,3 +38,5 @@ # initializer 'the_engine.feature_decisions' do # OpenProject::FeatureDecisions.add :some_flag # end + +OpenProject::FeatureDecisions.add :project_list_sharing From 76f76d6acb70e32a8eb77f41258c986bfaeb33de Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 12:54:54 +0200 Subject: [PATCH 13/51] Fix Rubocop --- app/components/projects/table_component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index a2dea976cff6..3251b7ad0840 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -162,7 +162,7 @@ def projects_with_level(projects, &) end def favored_project_ids - @favored_projects ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id) + @favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id) end def sorted_by_lft? From 539e2970ea0128fc19385b98a573d7829a4d56e0 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 13:03:08 +0200 Subject: [PATCH 14/51] Add a list item to the sidebar to show public lists --- app/helpers/menus/projects.rb | 11 ++++++++++- app/models/queries/projects/project_query.rb | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/helpers/menus/projects.rb b/app/helpers/menus/projects.rb index fae5464c58ea..dc5e5ea63e7a 100644 --- a/app/helpers/menus/projects.rb +++ b/app/helpers/menus/projects.rb @@ -44,6 +44,8 @@ def first_level_menu_items [ OpenProject::Menu::MenuGroup.new(header: nil, children: main_static_filters), + OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.public"), + children: public_filters), OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.my_private"), children: my_filters), OpenProject::Menu::MenuGroup.new(header: I18n.t(:"activerecord.attributes.project.status_code"), @@ -76,9 +78,16 @@ def static_filters(ids) end end + def public_filters + ::Queries::Projects::ProjectQuery + .public_lists + .order(:name) + .map { |query| query_menu_item(query) } + end + def my_filters ::Queries::Projects::ProjectQuery - .where(user: current_user) + .private_lists(user: current_user) .order(:name) .map { |query| query_menu_item(query) } end diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index 7cfba892f808..cf63a00b060d 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -37,7 +37,7 @@ class Queries::Projects::ProjectQuery < ApplicationRecord serialize :selects, coder: Queries::Serialization::Selects.new(self) scope :public_lists, -> { where(public: true) } - scope :private_lists, ->(user = User.current) { where(public: false, user:) } + scope :private_lists, ->(user: User.current) { where(public: false, user:) } def self.model Project From e13a8f67e36e083030bbafb5c37fcccf03c822af Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 13:03:27 +0200 Subject: [PATCH 15/51] Add i18n keys for everything I added --- config/locales/en.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index f1334e537ab4..941509808088 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -298,6 +298,7 @@ en: my: "My projects" favored: "Favorite projects" archived: "Archived projects" + public: "Public project lists" my_private: "My private project lists" new: placeholder: "New project list" @@ -345,6 +346,12 @@ Project attributes and sections are defined in the Date: Tue, 21 May 2024 15:20:35 +0200 Subject: [PATCH 16/51] Allow access to public queries for everyone --- app/controllers/projects/queries_controller.rb | 2 +- app/models/queries/projects/factory.rb | 2 +- app/models/queries/projects/project_query.rb | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index 23439331915f..11940de55099 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -106,6 +106,6 @@ def render_result(service_call, success_i18n_key:, error_i18n_key:) end def find_query - @query = Queries::Projects::ProjectQuery.find(params[:id]) + @query = Queries::Projects::ProjectQuery.visible(user: current_user).find(params[:id]) end end diff --git a/app/models/queries/projects/factory.rb b/app/models/queries/projects/factory.rb index 818c7dd2594e..81c4b7b02f1a 100644 --- a/app/models/queries/projects/factory.rb +++ b/app/models/queries/projects/factory.rb @@ -133,7 +133,7 @@ def find_static_query_and_set_attributes(id, params, user, duplicate:) end def find_persisted_query_and_set_attributes(id, params, user, duplicate:) - query = Queries::Projects::ProjectQuery.where(user:).find_by(id:) + query = Queries::Projects::ProjectQuery.visible(user:).find_by(id:) return unless query diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index cf63a00b060d..157ba36c2737 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -39,6 +39,14 @@ class Queries::Projects::ProjectQuery < ApplicationRecord scope :public_lists, -> { where(public: true) } scope :private_lists, ->(user: User.current) { where(public: false, user:) } + scope :visible, ->(user: User.current) { + public_lists.or(private_lists(user:)) + } + + def visible?(user: User.current) + public? || user == self.user + end + def self.model Project end From 8a450a4c32313a1fa0d2566ec00cc5d131692d18 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 15:20:48 +0200 Subject: [PATCH 17/51] Only redirect to a query when it is still visibil --- app/controllers/projects/queries_controller.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index 11940de55099..cc37266a0b04 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -91,17 +91,19 @@ def destroy private - def render_result(service_call, success_i18n_key:, error_i18n_key:) + def render_result(service_call, success_i18n_key:, error_i18n_key:) # rubocop:disable Metrics/AbcSize + modified_query = service_call.result + if service_call.success? flash[:notice] = I18n.t(success_i18n_key) - redirect_to projects_path(query_id: service_call.result.id) + redirect_to modified_query.visible? ? projects_path(query_id: modified_query.id) : projects_path else flash[:error] = I18n.t(error_i18n_key, errors: service_call.errors.full_messages.join("\n")) render template: "/projects/index", layout: "global", - locals: { query: service_call.result, state: :edit } + locals: { query: modified_query, state: :edit } end end From cb613396446675bfee5ba52021b6a646fb8bdc43 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 16:20:52 +0200 Subject: [PATCH 18/51] Tests for changes to PRojectQuery model --- .../queries/project_query_factory.rb | 2 + .../queries/projects/project_query_spec.rb | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/spec/factories/queries/project_query_factory.rb b/spec/factories/queries/project_query_factory.rb index a82ddda60a12..969a39389522 100644 --- a/spec/factories/queries/project_query_factory.rb +++ b/spec/factories/queries/project_query_factory.rb @@ -29,6 +29,8 @@ FactoryBot.define do factory :project_query, class: "Queries::Projects::ProjectQuery" do sequence(:name) { |n| "Project query #{n}" } + public { false } + transient do select { [] } end diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index 61c9d1565c2b..f8869774f3df 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -387,4 +387,66 @@ end end end + + describe "scopes" do + describe ".public_lists" do + it "returns only public lists" do + public_query = create(:project_query, public: true) + public_query_other_user = create(:project_query, public: true) + private_query = create(:project_query, public: false) + + expect(described_class.public_lists).to contain_exactly(public_query, public_query_other_user) + end + end + + describe ".private_lists" do + it "returns only private lists owned by the user" do + public_query = create(:project_query, public: true) + private_query = create(:project_query, public: false) + private_query_other_user = create(:project_query, public: false) + + expect(described_class.private_lists(user: private_query.user)).to contain_exactly(private_query) + end + end + + describe ".visible" do + it "returns public and private queries owned by the user" do + public_query = create(:project_query, public: true) + public_query_other_user = create(:project_query, public: true) + private_query = create(:project_query, public: false) + private_query_other_user = create(:project_query, public: false) + + expect(described_class.visible(user: private_query.user)).to contain_exactly(public_query, public_query_other_user, + private_query) + end + end + end + + describe "#visible?" do + let(:public) { false } + + subject { build(:project_query, user: owner, public:) } + + context "when the user is the owner" do + let(:owner) { user } + + it { is_expected.to be_visible(user:) } + end + + context "when the user is not the owner" do + let(:owner) { build(:user) } + + context "and the query is public" do + let(:public) { true } + + it { is_expected.to be_visible(user:) } + end + + context "and the query is private" do + let(:public) { false } + + it { is_expected.not_to be_visible(user:) } + end + end + end end From 62c953558016d2f4a4332ef1a3612d510f052eb5 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 17:01:51 +0200 Subject: [PATCH 19/51] Add a convenience show route that redirects to the queried page --- app/controllers/projects/queries_controller.rb | 6 +++++- config/routes.rb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index cc37266a0b04..37377be694e0 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -31,13 +31,17 @@ class Projects::QueriesController < ApplicationController # No need for a more specific authorization check. That is carried out in the contracts. before_action :require_login - before_action :find_query, only: %i[rename update destroy publish unpublish] + before_action :find_query, only: %i[show rename update destroy publish unpublish] before_action :build_query_or_deny_access, only: %i[new create] current_menu_item [:new, :rename, :create, :update] do :projects end + def show + redirect_to projects_path(query_id: @query.id) + end + def new render template: "/projects/index", layout: "global", diff --git a/config/routes.rb b/config/routes.rb index 80c29d6a84e4..06c8a660b8b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -181,7 +181,7 @@ namespace :projects do resource :menu, only: %i[show] - resources :queries, only: %i[new create update destroy] do + resources :queries, only: %i[show new create update destroy] do member do get :rename From 6a835f4f2a06d1a25cbc175f9db280980b8a5dcf Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 17:02:21 +0200 Subject: [PATCH 20/51] Allow editing public queries when you have the permission --- .../projects/index_page_header_component.rb | 12 ++++++++++- .../projects/project_queries/base_contract.rb | 20 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index 15f19a7ba0ce..34f7ed4b1537 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -70,7 +70,17 @@ def may_save_as? = current_user.logged? def can_save_as? = may_save_as? && query.changed? - def can_save? = can_save_as? && query.persisted? && query.user == current_user + def can_save? + return false unless current_user.logged? + return false unless query.persisted? + return false unless query.changed? + + if query.public? + current_user.allowed_globally?(:manage_public_project_queries) + else + query.user == current_user + end + end def can_rename? = may_save_as? && query.persisted? && query.user == current_user && !query.changed? diff --git a/app/contracts/queries/projects/project_queries/base_contract.rb b/app/contracts/queries/projects/project_queries/base_contract.rb index 4cc8bcd14d29..eae0f2228971 100644 --- a/app/contracts/queries/projects/project_queries/base_contract.rb +++ b/app/contracts/queries/projects/project_queries/base_contract.rb @@ -41,14 +41,26 @@ def self.model presence: true, length: { maximum: 255 } - validate :user_is_current_user_and_logged_in validate :name_select_included validate :existing_selects + validate :allowed_to_modify_private_query + validate :allowed_to_modify_public_query + protected - def user_is_current_user_and_logged_in - unless user.logged? && user == model.user - errors.add :base, :error_unauthorized + def allowed_to_modify_private_query + return if model.public? + + if model.user != user + errors.add :base, :can_only_be_modified_by_owner + end + end + + def allowed_to_modify_public_query + return unless model.public? + + unless user.allowed_globally?(:manage_public_project_queries) + errors.add :base, :need_permission_to_modify_public_query end end From 282d02b078c2874acf959dcced56c3e20d03e888 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 17:02:46 +0200 Subject: [PATCH 21/51] Fix publishing with more knowledge about Contracts --- .../project_queries}/publish_contract.rb | 12 ++------ .../project_queries/publish_service.rb | 29 +++++++++---------- 2 files changed, 17 insertions(+), 24 deletions(-) rename app/contracts/{projects/queries => queries/projects/project_queries}/publish_contract.rb (82%) diff --git a/app/contracts/projects/queries/publish_contract.rb b/app/contracts/queries/projects/project_queries/publish_contract.rb similarity index 82% rename from app/contracts/projects/queries/publish_contract.rb rename to app/contracts/queries/projects/project_queries/publish_contract.rb index ccf53f5ff9bf..98a9fe2844c9 100644 --- a/app/contracts/projects/queries/publish_contract.rb +++ b/app/contracts/queries/projects/project_queries/publish_contract.rb @@ -26,14 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Projects::Queries::PublishContract < BaseContract - validate :allowed_to_modify_public_flag - - protected - - def allowed_to_modify_public_flag - unless user.allowed_globally?(:manage_public_project_queries) - errors.add :base, :error_unauthorized - end +module Queries::Projects::ProjectQueries + class PublishContract < BaseContract + attribute :public end end diff --git a/app/services/queries/projects/project_queries/publish_service.rb b/app/services/queries/projects/project_queries/publish_service.rb index ad68a1bc45d1..e2c26fcda821 100644 --- a/app/services/queries/projects/project_queries/publish_service.rb +++ b/app/services/queries/projects/project_queries/publish_service.rb @@ -26,25 +26,24 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::Projects::ProjectQueries::PublishService < BaseServices::BaseContracted - include Contracted +module Queries::Projects::ProjectQueries + class PublishService < BaseServices::Update + private - def initialize(user:, model:, contract_class: Projects::Queries::PublishContract) - super(user:, contract_class:) - self.model = model - end - - private + def after_validate(params, service_call) + model.public = params[:public] - def after_validate(params, service_call) - model.public = params[:public] + service_call + end - service_call - end + def persist(service_call) + model.save - def persist(service_call) - model.save + service_call + end - service_call + def default_contract_class + Queries::Projects::ProjectQueries::PublishContract + end end end From 6988711217ccc0c471b56370ea2890f0bfa1d1d0 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 21 May 2024 17:06:32 +0200 Subject: [PATCH 22/51] Fix accidentally commited merge --- app/components/projects/table_component.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index 3251b7ad0840..8d68f035dbc8 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -64,18 +64,10 @@ def build_sort_header(column, options) # We don't return the project row # but the [project, level] array from the helper def rows -<<<<<<< HEAD - @rows ||= - begin - projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } - instance_exec(model, &projects_enumerator) - end -======= @rows ||= begin projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } instance_exec(model, &projects_enumerator) end ->>>>>>> 0e1a80b408 (Add methods to publish and unpublish a project list based on permission) end def initialize_sorted_model @@ -126,8 +118,8 @@ def columns index = columns.index { |column| column.attribute == :name } columns.insert(index, ::Queries::Projects::Selects::Default.new(:hierarchy)) if index - columns - end + columns + end end def projects(query) From 438afa8bb51b090206fddd70da5cead6da8089cb Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 22 May 2024 10:04:25 +0200 Subject: [PATCH 23/51] Fix specs for the factory by mocking the new query --- spec/models/queries/projects/factory_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/models/queries/projects/factory_spec.rb b/spec/models/queries/projects/factory_spec.rb index 53a50b4f973a..4d9fc2d5cab1 100644 --- a/spec/models/queries/projects/factory_spec.rb +++ b/spec/models/queries/projects/factory_spec.rb @@ -31,11 +31,11 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_columns: %w[favored name project_status] } do - let!(:query_finder) do + before do scope = instance_double(ActiveRecord::Relation) allow(Queries::Projects::ProjectQuery) - .to receive(:where) + .to receive(:visible) .with(user: current_user) .and_return(scope) @@ -44,6 +44,7 @@ .with(id:) .and_return(persisted_query) end + let(:persisted_query) do build_stubbed(:project_query, name: "My query") do |query| query.order(id: :asc) From eef97fc95969feec899c3d6b111592703c0d4fa9 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 22 May 2024 12:35:28 +0200 Subject: [PATCH 24/51] Fix the QueriesController so that the user is the proper owner of the query --- .../projects/queries_controller_spec.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/spec/controllers/projects/queries_controller_spec.rb b/spec/controllers/projects/queries_controller_spec.rb index 83f1314bb148..e1bd4542e308 100644 --- a/spec/controllers/projects/queries_controller_spec.rb +++ b/spec/controllers/projects/queries_controller_spec.rb @@ -39,7 +39,7 @@ end context "when logged in" do - let(:query) { build_stubbed(:project_query) } + let(:query) { build_stubbed(:project_query, user:) } let(:query_id) { "42" } let(:query_params) { double } @@ -110,7 +110,7 @@ end context "when logged in" do - let(:query) { build_stubbed(:project_query) } + let(:query) { build_stubbed(:project_query, user:) } let(:query_params) { double } let(:service_instance) { instance_double(service_class) } let(:service_result) { instance_double(ServiceResult, success?: success?, result: query) } @@ -181,7 +181,7 @@ end context "when logged in" do - let(:query) { build_stubbed(:project_query) } + let(:query) { build_stubbed(:project_query, user:) } let(:query_id) { "42" } let(:query_params) { double } let(:service_instance) { instance_double(service_class) } @@ -190,7 +190,9 @@ before do allow(controller).to receive(:permitted_query_params).and_return(query_params) - allow(Queries::Projects::ProjectQuery).to receive(:find).with(query_id).and_return(query) + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(scope).to receive(:find).with(query_id).and_return(query) allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) allow(service_instance).to receive(:call).with(query_params).and_return(service_result) @@ -252,12 +254,15 @@ end context "when logged in" do - let(:query) { build_stubbed(:project_query) } + let(:query) { build_stubbed(:project_query, user:) } let(:query_id) { "42" } let(:service_instance) { instance_spy(service_class) } before do - allow(Queries::Projects::ProjectQuery).to receive(:find).with(query_id).and_return(query) + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(scope).to receive(:find).with(query_id).and_return(query) + allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) login_as user From 175bacf0e2e0c7d25f5d6997baa9886ce3d9f11c Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 22 May 2024 12:59:18 +0200 Subject: [PATCH 25/51] Fix specs --- .../projects/project_queries/base_contract.rb | 7 +++ config/locales/en.yml | 2 + .../shared_contract_examples.rb | 46 ++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/contracts/queries/projects/project_queries/base_contract.rb b/app/contracts/queries/projects/project_queries/base_contract.rb index eae0f2228971..c3996e6cb034 100644 --- a/app/contracts/queries/projects/project_queries/base_contract.rb +++ b/app/contracts/queries/projects/project_queries/base_contract.rb @@ -43,11 +43,18 @@ def self.model validate :name_select_included validate :existing_selects + validate :user_is_logged_in validate :allowed_to_modify_private_query validate :allowed_to_modify_public_query protected + def user_is_logged_in + if !user.logged? + errors.add :base, :error_unauthorized + end + end + def allowed_to_modify_private_query return if model.public? diff --git a/config/locales/en.yml b/config/locales/en.yml index 941509808088..8c3d8b8f422b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -991,6 +991,8 @@ Project attributes and sections are defined in the Date: Wed, 22 May 2024 13:12:04 +0200 Subject: [PATCH 26/51] Add conroller tests --- .../projects/queries_controller_spec.rb | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/spec/controllers/projects/queries_controller_spec.rb b/spec/controllers/projects/queries_controller_spec.rb index e1bd4542e308..60dc3a371b86 100644 --- a/spec/controllers/projects/queries_controller_spec.rb +++ b/spec/controllers/projects/queries_controller_spec.rb @@ -31,6 +31,23 @@ RSpec.describe Projects::QueriesController do shared_let(:user) { create(:user) } + describe "#show" do + let(:query) { build_stubbed(:project_query, user:) } + + before do + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).with(user:).and_return(scope) + allow(scope).to receive(:find).with(query.id.to_s).and_return(query) + + login_as user + end + + it "redirects to the projects page" do + get :show, params: { id: query.id } + expect(response).to redirect_to(projects_path(query_id: query.id)) + end + end + describe "#new" do it "requires login" do get "new" @@ -244,6 +261,152 @@ end end + describe "#publish" do + let(:service_class) { Queries::Projects::ProjectQueries::PublishService } + + it "requires login" do + put "publish", params: { id: 42 } + + expect(response).not_to be_successful + end + + context "when logged in" do + let(:query) { build_stubbed(:project_query, user:) } + let(:query_id) { "42" } + let(:query_params) { { public: true } } + let(:service_instance) { instance_double(service_class) } + let(:service_result) { instance_double(ServiceResult, success?: success?, result: query) } + let(:success?) { true } + + before do + allow(controller).to receive(:permitted_query_params).and_return(query_params) + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(scope).to receive(:find).with(query_id).and_return(query) + allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) + allow(service_instance).to receive(:call).with(query_params).and_return(service_result) + + login_as user + end + + it "calls publish service on query" do + put "publish", params: { id: 42 } + + expect(service_instance).to have_received(:call).with(query_params) + end + + context "when service call succeeds" do + it "redirects to projects" do + allow(I18n).to receive(:t).with("lists.publish.success").and_return("foo") + + put "publish", params: { id: 42 } + + expect(flash[:notice]).to eq("foo") + expect(response).to redirect_to(projects_path(query_id: query.id)) + end + end + + context "when service call fails" do + let(:success?) { false } + let(:errors) { instance_double(ActiveModel::Errors, full_messages: ["something", "went", "wrong"]) } + + before do + allow(service_result).to receive(:errors).and_return(errors) + end + + it "renders projects/index" do + allow(I18n).to receive(:t).with("lists.publish.failure", errors: "something\nwent\nwrong").and_return("bar") + + put "publish", params: { id: 42 } + + expect(flash[:error]).to eq("bar") + expect(response).to render_template("projects/index") + end + + it "passes variables to template" do + allow(controller).to receive(:render).and_call_original + + put "update", params: { id: 42 } + + expect(controller).to have_received(:render).with(include(locals: { query:, state: :edit })) + end + end + end + end + + describe "#unpublish" do + let(:service_class) { Queries::Projects::ProjectQueries::PublishService } + + it "requires login" do + put "unpublish", params: { id: 42 } + + expect(response).not_to be_successful + end + + context "when logged in" do + let(:query) { build_stubbed(:project_query, user:) } + let(:query_id) { "42" } + let(:query_params) { { public: false } } + let(:service_instance) { instance_double(service_class) } + let(:service_result) { instance_double(ServiceResult, success?: success?, result: query) } + let(:success?) { true } + + before do + allow(controller).to receive(:permitted_query_params).and_return(query_params) + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(scope).to receive(:find).with(query_id).and_return(query) + allow(service_class).to receive(:new).with(model: query, user:).and_return(service_instance) + allow(service_instance).to receive(:call).with(query_params).and_return(service_result) + + login_as user + end + + it "calls publish service on query" do + put "unpublish", params: { id: 42 } + + expect(service_instance).to have_received(:call).with(query_params) + end + + context "when service call succeeds" do + it "redirects to projects" do + allow(I18n).to receive(:t).with("lists.unpublish.success").and_return("foo") + + put "unpublish", params: { id: 42 } + + expect(flash[:notice]).to eq("foo") + expect(response).to redirect_to(projects_path(query_id: query.id)) + end + end + + context "when service call fails" do + let(:success?) { false } + let(:errors) { instance_double(ActiveModel::Errors, full_messages: ["something", "went", "wrong"]) } + + before do + allow(service_result).to receive(:errors).and_return(errors) + end + + it "renders projects/index" do + allow(I18n).to receive(:t).with("lists.unpublish.failure", errors: "something\nwent\nwrong").and_return("bar") + + put "unpublish", params: { id: 42 } + + expect(flash[:error]).to eq("bar") + expect(response).to render_template("projects/index") + end + + it "passes variables to template" do + allow(controller).to receive(:render).and_call_original + + put "unpublish", params: { id: 42 } + + expect(controller).to have_received(:render).with(include(locals: { query:, state: :edit })) + end + end + end + end + describe "#destroy" do let(:service_class) { Queries::Projects::ProjectQueries::DeleteService } From 03b9411547f7875a79a91807ec41225b219a6726 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 22 May 2024 13:19:07 +0200 Subject: [PATCH 27/51] Fix Lint/UselessAssignment errors --- spec/models/queries/projects/project_query_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index f8869774f3df..c2fc44a5d631 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -393,7 +393,7 @@ it "returns only public lists" do public_query = create(:project_query, public: true) public_query_other_user = create(:project_query, public: true) - private_query = create(:project_query, public: false) + create(:project_query, public: false) expect(described_class.public_lists).to contain_exactly(public_query, public_query_other_user) end @@ -401,9 +401,9 @@ describe ".private_lists" do it "returns only private lists owned by the user" do - public_query = create(:project_query, public: true) + create(:project_query, public: true) private_query = create(:project_query, public: false) - private_query_other_user = create(:project_query, public: false) + create(:project_query, public: false) expect(described_class.private_lists(user: private_query.user)).to contain_exactly(private_query) end @@ -414,7 +414,7 @@ public_query = create(:project_query, public: true) public_query_other_user = create(:project_query, public: true) private_query = create(:project_query, public: false) - private_query_other_user = create(:project_query, public: false) + create(:project_query, public: false) expect(described_class.visible(user: private_query.user)).to contain_exactly(public_query, public_query_other_user, private_query) From 1358215a0a0daab31137c51be56e0b645eee8e86 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 08:51:58 +0200 Subject: [PATCH 28/51] Move feature decision check into the can_publish? method --- app/components/projects/index_page_header_component.html.erb | 2 +- app/components/projects/index_page_header_component.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index ef38c2243013..dff8c5e76b1c 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -81,7 +81,7 @@ end if query.persisted? - if can_publish? && OpenProject::FeatureDecisions.project_list_sharing_active? + if can_publish? if query.public? menu.with_item( label: t(:button_unpublish), diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index 34f7ed4b1537..d5fab8b4e365 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -85,7 +85,9 @@ def can_save? def can_rename? = may_save_as? && query.persisted? && query.user == current_user && !query.changed? def can_publish? - current_user.allowed_globally?(:manage_public_project_queries) && query.persisted? + OpenProject::FeatureDecisions.project_list_sharing_active? && + current_user.allowed_globally?(:manage_public_project_queries) && + query.persisted? end def show_state? From ca42c30f950c0ed315520cfee98fb8f003afe298 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 08:53:56 +0200 Subject: [PATCH 29/51] Use unless in favor of if ! --- app/contracts/queries/projects/project_queries/base_contract.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/contracts/queries/projects/project_queries/base_contract.rb b/app/contracts/queries/projects/project_queries/base_contract.rb index c3996e6cb034..da00abf3366a 100644 --- a/app/contracts/queries/projects/project_queries/base_contract.rb +++ b/app/contracts/queries/projects/project_queries/base_contract.rb @@ -50,7 +50,7 @@ def self.model protected def user_is_logged_in - if !user.logged? + unless user.logged? errors.add :base, :error_unauthorized end end From 28d39f405bd56aa5bf45057e0d1e2810f5af3a1c Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 09:04:35 +0200 Subject: [PATCH 30/51] Use positional arguments for user in viible methods --- app/controllers/projects/queries_controller.rb | 2 +- app/models/queries/projects/factory.rb | 2 +- app/models/queries/projects/project_query.rb | 4 ++-- spec/controllers/projects/queries_controller_spec.rb | 2 +- spec/models/queries/projects/factory_spec.rb | 2 +- spec/models/queries/projects/project_query_spec.rb | 10 +++++----- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/queries_controller.rb b/app/controllers/projects/queries_controller.rb index 37377be694e0..bf9bbc36fd1c 100644 --- a/app/controllers/projects/queries_controller.rb +++ b/app/controllers/projects/queries_controller.rb @@ -112,6 +112,6 @@ def render_result(service_call, success_i18n_key:, error_i18n_key:) # rubocop:di end def find_query - @query = Queries::Projects::ProjectQuery.visible(user: current_user).find(params[:id]) + @query = Queries::Projects::ProjectQuery.visible(current_user).find(params[:id]) end end diff --git a/app/models/queries/projects/factory.rb b/app/models/queries/projects/factory.rb index 81c4b7b02f1a..bbc7dc19acaa 100644 --- a/app/models/queries/projects/factory.rb +++ b/app/models/queries/projects/factory.rb @@ -133,7 +133,7 @@ def find_static_query_and_set_attributes(id, params, user, duplicate:) end def find_persisted_query_and_set_attributes(id, params, user, duplicate:) - query = Queries::Projects::ProjectQuery.visible(user:).find_by(id:) + query = Queries::Projects::ProjectQuery.visible(user).find_by(id:) return unless query diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index 157ba36c2737..7bf65abb9ee2 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -39,11 +39,11 @@ class Queries::Projects::ProjectQuery < ApplicationRecord scope :public_lists, -> { where(public: true) } scope :private_lists, ->(user: User.current) { where(public: false, user:) } - scope :visible, ->(user: User.current) { + scope :visible, ->(user = User.current) { public_lists.or(private_lists(user:)) } - def visible?(user: User.current) + def visible?(user = User.current) public? || user == self.user end diff --git a/spec/controllers/projects/queries_controller_spec.rb b/spec/controllers/projects/queries_controller_spec.rb index 60dc3a371b86..529318d1751e 100644 --- a/spec/controllers/projects/queries_controller_spec.rb +++ b/spec/controllers/projects/queries_controller_spec.rb @@ -36,7 +36,7 @@ before do scope = instance_double(ActiveRecord::Relation) - allow(Queries::Projects::ProjectQuery).to receive(:visible).with(user:).and_return(scope) + allow(Queries::Projects::ProjectQuery).to receive(:visible).with(user).and_return(scope) allow(scope).to receive(:find).with(query.id.to_s).and_return(query) login_as user diff --git a/spec/models/queries/projects/factory_spec.rb b/spec/models/queries/projects/factory_spec.rb index 4d9fc2d5cab1..c4fc2fc8fa0e 100644 --- a/spec/models/queries/projects/factory_spec.rb +++ b/spec/models/queries/projects/factory_spec.rb @@ -36,7 +36,7 @@ allow(Queries::Projects::ProjectQuery) .to receive(:visible) - .with(user: current_user) + .with(current_user) .and_return(scope) allow(scope) diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index c2fc44a5d631..f86a17593d6a 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -416,8 +416,8 @@ private_query = create(:project_query, public: false) create(:project_query, public: false) - expect(described_class.visible(user: private_query.user)).to contain_exactly(public_query, public_query_other_user, - private_query) + expect(described_class.visible(private_query.user)).to contain_exactly(public_query, public_query_other_user, + private_query) end end end @@ -430,7 +430,7 @@ context "when the user is the owner" do let(:owner) { user } - it { is_expected.to be_visible(user:) } + it { is_expected.to be_visible(user) } end context "when the user is not the owner" do @@ -439,13 +439,13 @@ context "and the query is public" do let(:public) { true } - it { is_expected.to be_visible(user:) } + it { is_expected.to be_visible(user) } end context "and the query is private" do let(:public) { false } - it { is_expected.not_to be_visible(user:) } + it { is_expected.not_to be_visible(user) } end end end From 92a83274cd32187650fa0f432523295a3b234780 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 17:01:33 +0200 Subject: [PATCH 31/51] Remove `can_rename?` method and alias it to can_save? --- app/components/projects/index_page_header_component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index d5fab8b4e365..4f6a8cc28b92 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -82,7 +82,7 @@ def can_save? end end - def can_rename? = may_save_as? && query.persisted? && query.user == current_user && !query.changed? + alias can_rename? can_save? def can_publish? OpenProject::FeatureDecisions.project_list_sharing_active? && From 3264517c50d40cb559165761bafb261f6d6320c8 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 17:14:05 +0200 Subject: [PATCH 32/51] Fix specs --- spec/controllers/projects/queries_controller_spec.rb | 4 +++- spec/helpers/menus/projects_spec.rb | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spec/controllers/projects/queries_controller_spec.rb b/spec/controllers/projects/queries_controller_spec.rb index 529318d1751e..a1cce957c2b8 100644 --- a/spec/controllers/projects/queries_controller_spec.rb +++ b/spec/controllers/projects/queries_controller_spec.rb @@ -96,7 +96,9 @@ let(:query_id) { "42" } before do - allow(Queries::Projects::ProjectQuery).to receive(:find).with(query_id).and_return(query) + scope = instance_double(ActiveRecord::Relation) + allow(Queries::Projects::ProjectQuery).to receive(:visible).and_return(scope) + allow(scope).to receive(:find).with(query_id).and_return(query) login_as user end diff --git a/spec/helpers/menus/projects_spec.rb b/spec/helpers/menus/projects_spec.rb index 71a2cdfe40c2..594aee6aaed3 100644 --- a/spec/helpers/menus/projects_spec.rb +++ b/spec/helpers/menus/projects_spec.rb @@ -45,11 +45,15 @@ Queries::Projects::ProjectQuery.create!(name: "Other user query", user: build(:user)) end + shared_let(:public_query) do + Queries::Projects::ProjectQuery.create!(name: "Public query", user: build(:user), public: true) + end + subject(:first_level_menu_items) { instance.first_level_menu_items } - it "returns 3 menu groups" do + it "returns 4 menu groups" do expect(first_level_menu_items).to all(be_a(OpenProject::Menu::MenuGroup)) - expect(first_level_menu_items.length).to eq(3) + expect(first_level_menu_items.length).to eq(4) end describe "children items" do @@ -66,6 +70,10 @@ it "doesn't contain item for other user query" do expect(children_menu_items).not_to include(have_attributes(title: "Other user query")) end + + it "contains item for public query" do + expect(children_menu_items).to include(have_attributes(title: "Public query")) + end end describe "selected children items" do From 1ba1481d5679ceefe6ab87b2315216bbf0dfe006 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 23 May 2024 17:32:14 +0200 Subject: [PATCH 33/51] Reintroduce can_rename? --- .../projects/index_page_header_component.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index 4f6a8cc28b92..5cf1928edbaa 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -82,7 +82,17 @@ def can_save? end end - alias can_rename? can_save? + def can_rename? + return false unless current_user.logged? + return false unless query.persisted? + return false if query.changed? + + if query.public? + current_user.allowed_globally?(:manage_public_project_queries) + else + query.user == current_user + end + end def can_publish? OpenProject::FeatureDecisions.project_list_sharing_active? && From 481b7022bc6420085a088a4478870420ca6a09d8 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 24 May 2024 11:42:50 +0200 Subject: [PATCH 34/51] remove duplicate "and" --- docs/user-guide/meetings/dynamic-meetings/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/meetings/dynamic-meetings/README.md b/docs/user-guide/meetings/dynamic-meetings/README.md index d11b4862e5cd..5a02967352c8 100644 --- a/docs/user-guide/meetings/dynamic-meetings/README.md +++ b/docs/user-guide/meetings/dynamic-meetings/README.md @@ -78,7 +78,7 @@ In order to edit the title of the meeting select the dropdown menu behind the th After creating a meeting, you can set up a **meeting agenda**. -You do this by adding sections, agenda items or existing work packages by selecting the desired option under the green **Add** button. You can then add notes and to each agenda item. +You do this by adding sections, agenda items or existing work packages by selecting the desired option under the green **Add** button. You can then add notes to each agenda item. ![The add button with three choices: section, agenda item or work package](openproject_dynamic_meetings_add_agenda_item.png) From 83e12847929f70e1ebbd0abc2122a34a662d6efa Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 24 May 2024 13:19:21 +0200 Subject: [PATCH 35/51] rack service_timeout only in production According to the rack timout documentation https://github.com/zombocom/rack-timeout/blob/main/doc/settings.md#service-timeout "Service timeout can be disabled entirely by setting the property to 0 or false" Limiting the service timeout to be active only in production enables debugging in dev and test env without fearing for the timeout to take oneself out of context. --- config/constants/settings/definition.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 2af6bc3b15f6..ee3dcab35f71 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -1087,7 +1087,7 @@ class Definition description: "Web worker count and threads configuration", default: { "workers" => 2, - "timeout" => 120, + "timeout" => Rails.env.production? ? 120 : 0, "wait_timeout" => 10, "min_threads" => 4, "max_threads" => 16 From 636746953b1ab6a4016f782096a15233d9fce4f0 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Fri, 24 May 2024 17:42:09 +0200 Subject: [PATCH 36/51] Use double-quotes and make Rubocop happier --- app/models/permitted_params.rb | 10 +- spec/models/permitted_params_spec.rb | 733 ++++++++++++++------------- 2 files changed, 372 insertions(+), 371 deletions(-) diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 6ed1a45ae846..c768af525dd3 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'permitted_params/allowed_settings' +require "permitted_params/allowed_settings" class PermittedParams # This class intends to provide a method for all params hashes coming from the @@ -162,7 +162,7 @@ def query p = params.require(:query).permit(*self.class.permitted_attributes[:query]) p[:sort_criteria] = params .require(:query) - .permit(sort_criteria: { '0' => [], '1' => [], '2' => [] }) + .permit(sort_criteria: { "0" => [], "1" => [], "2" => [] }) p[:sort_criteria].delete :sort_criteria p end @@ -340,7 +340,7 @@ def message(project = nil) end def attachments - params.permit(attachments: %i[file description id])['attachments'] + params.permit(attachments: %i[file description id])["attachments"] end def enumerations @@ -407,7 +407,7 @@ def custom_field_values(key = nil, required: true) # Reject blank values from include_hidden select fields values.each { |_, v| v.compact_blank! if v.is_a?(Array) } - values.empty? ? {} : { 'custom_field_values' => values.permit! } + values.empty? ? {} : { "custom_field_values" => values.permit! } end def permitted_attributes(key, additions = {}) @@ -529,7 +529,7 @@ def self.permitted_attributes :subject, Proc.new do |args| # avoid costly allowed_in_project? if the param is not there at all - if args[:params]['work_package']&.has_key?('watcher_user_ids') && + if args[:params]["work_package"]&.has_key?("watcher_user_ids") && args[:current_user].allowed_in_project?(:add_work_package_watchers, args[:project]) { watcher_user_ids: [] } diff --git a/spec/models/permitted_params_spec.rb b/spec/models/permitted_params_spec.rb index e19a6cae5c17..e410c07013b1 100644 --- a/spec/models/permitted_params_spec.rb +++ b/spec/models/permitted_params_spec.rb @@ -26,13 +26,13 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'spec_helper' +require "spec_helper" RSpec.describe PermittedParams do let(:user) { build_stubbed(:user) } let(:admin) { build_stubbed(:admin) } - shared_context 'prepare params comparison' do + shared_context "with prepare params comparison" do let(:params_key) { defined?(hash_key) ? hash_key : attribute } let(:params) do nested_params = @@ -52,182 +52,181 @@ ActionController::Parameters.new(ac_params) end - subject { PermittedParams.new(params, user).send(attribute).to_h } + subject { described_class.new(params, user).send(attribute).to_h } end - shared_examples_for 'allows params' do - include_context 'prepare params comparison' + shared_examples_for "allows params" do + include_context "with prepare params comparison" it do - expected = defined?(allowed_params) ? allowed_params : hash + expected = defined?(expected_allowed_params) ? expected_allowed_params : hash expect(subject).to eq(expected) end end - shared_examples_for 'allows nested params' do - include_context 'prepare params comparison' + shared_examples_for "allows nested params" do + include_context "with prepare params comparison" it { expect(subject).to eq(hash) } end - shared_examples_for 'forbids params' do - include_context 'prepare params comparison' + shared_examples_for "forbids params" do + include_context "with prepare params comparison" it { expect(subject).not_to eq(hash) } end - describe '#permit' do - it 'adds an attribute to be permitted later' do + describe "#permit" do + it "adds an attribute to be permitted later" do # just taking project_type here as an example, could be anything # taking the originally whitelisted params to be restored later - original_whitelisted = PermittedParams.instance_variable_get(:@whitelisted_params) + original_whitelisted = described_class.instance_variable_get(:@whitelisted_params) - params = ActionController::Parameters.new(project_type: { 'blubs1' => 'blubs' }) + ActionController::Parameters.new(project_type: { "blubs1" => "blubs" }) - PermittedParams.instance_variable_set(:@whitelisted_params, original_whitelisted) + described_class.instance_variable_set(:@whitelisted_params, original_whitelisted) end - it 'raises an argument error if key does not exist' do - expect { PermittedParams.permit(:bogus_key) }.to raise_error ArgumentError + it "raises an argument error if key does not exist" do + expect { described_class.permit(:bogus_key) }.to raise_error ArgumentError end end - describe '#pref' do + describe "#pref" do let(:attribute) { :pref } let(:hash) do acceptable_params = %w(hide_mail time_zone comments_sorting warn_on_leaving_unsaved) - acceptable_params.index_with { |_x| 'value' } + acceptable_params.index_with { |_x| "value" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#news' do + describe "#news" do let(:attribute) { :news } let(:hash) do - %w(title summary description).index_with { |_x| 'value' }.to_h + %w(title summary description).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#comment' do + describe "#comment" do let(:attribute) { :comment } let(:hash) do - %w(commented author comments).index_with { |_x| 'value' }.to_h + %w(commented author comments).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#watcher' do + describe "#watcher" do let(:attribute) { :watcher } let(:hash) do - %w(watchable user user_id).index_with { |_x| 'value' }.to_h + %w(watchable user user_id).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#reply' do + describe "#reply" do let(:attribute) { :reply } let(:hash) do - %w(content subject).index_with { |_x| 'value' }.to_h + %w(content subject).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#wiki' do + describe "#wiki" do let(:attribute) { :wiki } let(:hash) do - %w(start_page).index_with { |_x| 'value' }.to_h + %w(start_page).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#membership' do + describe "#membership" do let(:attribute) { :membership } let(:hash) do - { 'project_id' => '1', 'role_ids' => ['1', '2', '4'] } + { "project_id" => "1", "role_ids" => ["1", "2", "4"] } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#category' do + describe "#category" do let(:attribute) { :category } let(:hash) do - %w(name assigned_to_id).index_with { |_x| 'value' }.to_h + %w(name assigned_to_id).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#version' do + describe "#version" do let(:attribute) { :version } - context 'whitelisted params' do + context "with whitelisted params" do let(:hash) do %w(name description effective_date due_date - start_date wiki_page_title status sharing).index_with { |_x| 'value' }.to_h + start_date wiki_page_title status sharing).index_with { |_x| "value" }.to_h end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'empty' do + context "when empty" do let(:hash) { {} } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'custom field values' do - let(:hash) { { 'custom_field_values' => { '1' => '5' } } } + context "for custom field values" do + let(:hash) { { "custom_field_values" => { "1" => "5" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end end - describe '#message' do + describe "#message" do let(:attribute) { :message } - context 'no instance passed' do - let(:allowed_params) do - %w(subject content forum_id).index_with { |_x| 'value' }.to_h + context "with no instance passed" do + let(:expected_allowed_params) do + %w(subject content forum_id).index_with { |_x| "value" }.to_h end let(:hash) do - allowed_params.merge(evil: 'true', sticky: 'true', locked: 'true') + expected_allowed_params.merge(evil: "true", sticky: "true", locked: "true") end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'empty' do + context "when empty" do let(:hash) { {} } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'with instance passed' do - let(:instance) { double('message', project: double('project')) } - let(:project) { double('project') } - let(:allowed_params) do - { 'subject' => 'value', - 'content' => 'value', - 'forum_id' => 'value', - 'sticky' => 'true', - 'locked' => 'true' } + context "with project instance passed" do + let(:project) { instance_double(Project) } + let(:expected_allowed_params) do + { "subject" => "value", + "content" => "value", + "forum_id" => "value", + "sticky" => "true", + "locked" => "true" } end let(:hash) do - ActionController::Parameters.new('message' => allowed_params.merge(evil: 'true')) + ActionController::Parameters.new("message" => expected_allowed_params.merge(evil: "true")) end before do @@ -236,215 +235,209 @@ end end - subject { PermittedParams.new(hash, user).message(project).to_h } + subject { described_class.new(hash, user).message(project).to_h } it do - expect(subject).to eq(allowed_params) + expect(subject).to eq(expected_allowed_params) end end end - describe '#attachments' do + describe "#attachments" do let(:attribute) { :attachments } let(:hash) do - { 'file' => 'myfile', - 'description' => 'mydescription' } + { "file" => "myfile", + "description" => "mydescription" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#projects_type_ids' do + describe "#projects_type_ids" do let(:attribute) { :projects_type_ids } - let(:hash_key) { 'project' } + let(:hash_key) { "project" } let(:hash) do - { 'type_ids' => ['1', '', '2'] } + { "type_ids" => ["1", "", "2"] } end - let(:allowed_params) do + let(:expected_allowed_params) do [1, 2] end - include_context 'prepare params comparison' + include_context "with prepare params comparison" it do - actual = PermittedParams.new(params, user).send(attribute) + actual = described_class.new(params, user).send(attribute) - expect(actual).to eq(allowed_params) + expect(actual).to eq(expected_allowed_params) end end - describe '#color' do + describe "#color" do let(:attribute) { :color } let(:hash) do - { 'name' => 'blubs', - 'hexcode' => '#fff' } + { "name" => "blubs", + "hexcode" => "#fff" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#color_move' do + describe "#color_move" do let(:attribute) { :color_move } - let(:hash_key) { 'color' } + let(:hash_key) { "color" } let(:hash) do - { 'move_to' => '1' } + { "move_to" => "1" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#custom_field' do + describe "#custom_field" do let(:attribute) { :custom_field } let(:hash) do - { 'editable' => '0', 'visible' => '0' } + { "editable" => "0", "visible" => "0" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe '#custom_action' do + describe "#custom_action" do let(:attribute) { :custom_action } let(:hash) do { - 'name' => 'blubs', - 'description' => 'blubs blubs', - 'actions' => { 'assigned_to' => '1' }, - 'conditions' => { 'status' => '42' }, - 'move_to' => 'lower' + "name" => "blubs", + "description" => "blubs blubs", + "actions" => { "assigned_to" => "1" }, + "conditions" => { "status" => "42" }, + "move_to" => "lower" } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end describe "#update_work_package" do let(:attribute) { :update_work_package } - let(:hash_key) { 'work_package' } + let(:hash_key) { "work_package" } - context 'subject' do - let(:hash) { { 'subject' => 'blubs' } } + describe "subject" do + let(:hash) { { "subject" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'description' do - let(:hash) { { 'description' => 'blubs' } } + describe "description" do + let(:hash) { { "description" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'start_date' do - let(:hash) { { 'start_date' => '2013-07-08' } } + describe "start_date" do + let(:hash) { { "start_date" => "2013-07-08" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'due_date' do - let(:hash) { { 'due_date' => '2013-07-08' } } + describe "due_date" do + let(:hash) { { "due_date" => "2013-07-08" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'assigned_to_id' do - let(:hash) { { 'assigned_to_id' => '1' } } + describe "assigned_to_id" do + let(:hash) { { "assigned_to_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'responsible_id' do - let(:hash) { { 'responsible_id' => '1' } } + describe "responsible_id" do + let(:hash) { { "responsible_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'type_id' do - let(:hash) { { 'type_id' => '1' } } + describe "type_id" do + let(:hash) { { "type_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'priority_id' do - let(:hash) { { 'priority_id' => '1' } } + describe "priority_id" do + let(:hash) { { "priority_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'parent_id' do - let(:hash) { { 'parent_id' => '1' } } + describe "parent_id" do + let(:hash) { { "parent_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'parent_id' do - let(:hash) { { 'parent_id' => '1' } } + describe "version_id" do + let(:hash) { { "version_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'version_id' do - let(:hash) { { 'version_id' => '1' } } + describe "estimated_hours" do + let(:hash) { { "estimated_hours" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'estimated_hours' do - let(:hash) { { 'estimated_hours' => '1' } } + describe "done_ratio" do + let(:hash) { { "done_ratio" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'done_ratio' do - let(:hash) { { 'done_ratio' => '1' } } + describe "lock_version" do + let(:hash) { { "lock_version" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'lock_version' do - let(:hash) { { 'lock_version' => '1' } } + describe "status_id" do + let(:hash) { { "status_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'status_id' do - let(:hash) { { 'status_id' => '1' } } + describe "category_id" do + let(:hash) { { "category_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'category_id' do - let(:hash) { { 'category_id' => '1' } } + describe "budget_id" do + let(:hash) { { "budget_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'budget_id' do - let(:hash) { { 'budget_id' => '1' } } + describe "notes" do + let(:hash) { { "journal_notes" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'notes' do - let(:hash) { { 'journal_notes' => 'blubs' } } + describe "attachments" do + let(:hash) { { "attachments" => [{ "file" => "djskfj", "description" => "desc" }] } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'attachments' do - let(:hash) { { 'attachments' => [{ 'file' => 'djskfj', 'description' => 'desc' }] } } - - it_behaves_like 'allows params' - end - - context 'watcher_user_ids' do - include_context 'prepare params comparison' - let(:hash) { { 'watcher_user_ids' => ['1', '2'] } } - let(:project) { double('project') } + describe "watcher_user_ids" do + include_context "with prepare params comparison" + let(:hash) { { "watcher_user_ids" => ["1", "2"] } } + let(:project) { instance_double(Project) } before do mock_permissions_for(user) do |mock| @@ -452,9 +445,9 @@ end end - subject { PermittedParams.new(params, user).update_work_package(project:).to_h } + subject { described_class.new(params, user).update_work_package(project:).to_h } - context 'user is allowed to add watchers' do + context "when user is allowed to add watchers" do before do mock_permissions_for(user) do |mock| mock.allow_in_project :add_work_package_watchers, project: @@ -466,7 +459,7 @@ end end - context 'user is not allowed to add watchers' do + context "when user is not allowed to add watchers" do before do mock_permissions_for(user, &:forbid_everything) end @@ -477,20 +470,20 @@ end end - context 'custom field values' do - let(:hash) { { 'custom_field_values' => { '1' => '5' } } } + context "for custom field values" do + let(:hash) { { "custom_field_values" => { "1" => "5" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context "removes custom field values that do not follow the schema 'id as string' => 'value as string'" do - let(:hash) { { 'custom_field_values' => { 'blubs' => '5', '5' => { '1' => '2' } } } } + describe "removes custom field values that do not follow the schema 'id as string' => 'value as string'" do + let(:hash) { { "custom_field_values" => { "blubs" => "5", "5" => { "1" => "2" } } } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - describe '#time_entry_activities_project' do + describe "#time_entry_activities_project" do let(:attribute) { :time_entry_activities_project } let(:hash) do [ @@ -498,98 +491,101 @@ { "activity_id" => "6", "active" => "1" } ] end - let(:allowed_params) do - [{ "activity_id" => "5", "active" => "0" }, { "activity_id" => "6", "active" => "1" }] + let(:expected_allowed_params) do + [ + ActionController::Parameters.new("activity_id" => "5", "active" => "0").permit!, + ActionController::Parameters.new("activity_id" => "6", "active" => "1").permit! + ] end - it_behaves_like 'allows params' do - subject { PermittedParams.new(params, user).send(attribute) } + it_behaves_like "allows params" do + subject { described_class.new(params, user).send(attribute) } end end - describe '#user' do - include_context 'prepare params comparison' + describe "#user" do + include_context "with prepare params comparison" - let(:hash_key) { 'user' } + let(:hash_key) { "user" } let(:external_authentication) { false } let(:change_password_allowed) { true } - subject { PermittedParams.new(params, user).send(attribute, external_authentication, change_password_allowed).to_h } + subject { described_class.new(params, user).send(attribute, external_authentication, change_password_allowed).to_h } - all_permissions = ['admin', - 'login', - 'firstname', - 'lastname', - 'mail', - 'language', - 'custom_fields', - 'ldap_auth_source_id', - 'force_password_change'] + all_permissions = ["admin", + "login", + "firstname", + "lastname", + "mail", + "language", + "custom_fields", + "ldap_auth_source_id", + "force_password_change"] - describe :user_create_as_admin do + describe "#user_create_as_admin" do let(:attribute) { :user_create_as_admin } let(:default_permissions) { %w[custom_fields firstname lastname language mail ldap_auth_source_id] } - context 'for a non-admin' do + context "for a non-admin" do let(:hash) { all_permissions.zip(all_permissions).to_h } - it 'permits default permissions' do + it "permits default permissions" do expect(subject.keys).to match_array(default_permissions) end end - context 'for a non-admin with global :create_user permission' do + context "for a non-admin with global :create_user permission" do let(:user) { create(:user, global_permissions: [:create_user]) } let(:hash) { all_permissions.zip(all_permissions).to_h } it 'permits default permissions and "login"' do - expect(subject.keys).to match_array(default_permissions + ['login']) + expect(subject.keys).to match_array(default_permissions + ["login"]) end end - context 'for an admin' do + context "for an admin" do let(:user) { admin } all_permissions.each do |field| context field do - let(:hash) { { field => 'test' } } + let(:hash) { { field => "test" } } it "permits #{field}" do - expect(subject).to eq(field => 'test') + expect(subject).to eq(field => "test") end end end - context 'with no password change allowed' do - let(:hash) { { 'force_password_change' => 'true' } } + context "with no password change allowed" do + let(:hash) { { "force_password_change" => "true" } } let(:change_password_allowed) { false } - it 'does not permit force_password_change' do + it "does not permit force_password_change" do expect(subject).to eq({}) end end - context 'with external authentication' do - let(:hash) { { 'ldap_auth_source_id' => 'true' } } + context "with external authentication" do + let(:hash) { { "ldap_auth_source_id" => "true" } } let(:external_authentication) { true } - it 'does not permit ldap_auth_source_id' do + it "does not permit ldap_auth_source_id" do expect(subject).to eq({}) end end - context 'custom field values' do - let(:hash) { { 'custom_field_values' => { '1' => '5' } } } + context "for custom field values" do + let(:hash) { { "custom_field_values" => { "1" => "5" } } } - it 'permits custom_field_values' do + it "permits custom_field_values" do expect(subject).to eq(hash) end end - context "custom field values that do not follow the schema 'id as string' => 'value as string'" do - let(:hash) { { 'custom_field_values' => { 'blubs' => '5', '5' => { '1' => '2' } } } } + context "for custom field values that do not follow the schema 'id as string' => 'value as string'" do + let(:hash) { { "custom_field_values" => { "blubs" => "5", "5" => { "1" => "2" } } } } - it 'are removed' do + it "are removed" do expect(subject).to eq({}) end end @@ -597,134 +593,134 @@ end user_permissions = [ - 'firstname', - 'lastname', - 'mail', - 'language', - 'custom_fields' + "firstname", + "lastname", + "mail", + "language", + "custom_fields" ] - describe '#user' do + describe "#user" do let(:attribute) { :user } let(:user) { admin } user_permissions.each do |field| context field do - let(:hash) { { field => 'test' } } + let(:hash) { { field => "test" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end end (all_permissions - user_permissions).each do |field| - context "#{field} (admin-only)" do - let(:hash) { { field => 'test' } } + context "for #{field} (admin-only)" do + let(:hash) { { field => "test" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - context 'custom field values' do - let(:hash) { { 'custom_field_values' => { '1' => '5' } } } + context "for custom field values" do + let(:hash) { { "custom_field_values" => { "1" => "5" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context "custom field values that do not follow the schema 'id as string' => 'value as string'" do - let(:hash) { { 'custom_field_values' => { 'blubs' => '5', '5' => { '1' => '2' } } } } + context "for custom field values that do not follow the schema 'id as string' => 'value as string'" do + let(:hash) { { "custom_field_values" => { "blubs" => "5", "5" => { "1" => "2" } } } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end - context 'identity_url' do - let(:hash) { { 'identity_url' => 'test_identity_url' } } + context "for identity_url" do + let(:hash) { { "identity_url" => "test_identity_url" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end end - describe '#user_register_via_omniauth' do + describe "#user_register_via_omniauth" do let(:attribute) { :user_register_via_omniauth } - let(:hash_key) { 'user' } + let(:hash_key) { "user" } user_permissions = %w(login firstname lastname mail language) user_permissions.each do |field| - let(:hash) { { field => 'test' } } + let(:hash) { { field => "test" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'identity_url' do - let(:hash) { { 'identity_url' => 'test_identity_url' } } + context "for identity_url" do + let(:hash) { { "identity_url" => "test_identity_url" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - shared_examples_for 'allows enumeration move params' do - let(:hash) { { '2' => { 'move_to' => 'lower' } } } + shared_examples_for "allows enumeration move params" do + let(:hash) { { "2" => { "move_to" => "lower" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - shared_examples_for 'allows move params' do - let(:hash) { { 'move_to' => 'lower' } } + shared_examples_for "allows move params" do + let(:hash) { { "move_to" => "lower" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - shared_examples_for 'allows custom fields' do - describe 'valid custom fields' do - let(:hash) { { '1' => { 'custom_field_values' => { '1' => '5' } } } } + shared_examples_for "allows custom fields" do + describe "valid custom fields" do + let(:hash) { { "1" => { "custom_field_values" => { "1" => "5" } } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'invalid custom fields' do - let(:hash) { { 'custom_field_values' => { 'blubs' => '5', '5' => { '1' => '2' } } } } + describe "invalid custom fields" do + let(:hash) { { "custom_field_values" => { "blubs" => "5", "5" => { "1" => "2" } } } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - describe '#status' do + describe "#status" do let (:attribute) { :status } - describe 'name' do - let(:hash) { { 'name' => 'blubs' } } + describe "name" do + let(:hash) { { "name" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'default_done_ratio' do - let(:hash) { { 'default_done_ratio' => '10' } } + describe "default_done_ratio" do + let(:hash) { { "default_done_ratio" => "10" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'is_closed' do - let(:hash) { { 'is_closed' => 'true' } } + describe "is_closed" do + let(:hash) { { "is_closed" => "true" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'is_default' do - let(:hash) { { 'is_default' => 'true' } } + describe "is_default" do + let(:hash) { { "is_default" => "true" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'move_to' do - it_behaves_like 'allows move params' + describe "move_to" do + it_behaves_like "allows move params" end end - describe '#settings' do + describe "#settings" do let (:attribute) { :settings } - describe 'with password login enabled' do + describe "with password login enabled" do before do allow(OpenProject::Configuration) .to receive(:disable_password_login?) @@ -733,19 +729,19 @@ let(:hash) do { - 'sendmail_arguments' => 'value', - 'brute_force_block_after_failed_logins' => 'value', - 'password_active_rules' => ['value'], - 'default_projects_modules' => ['value', 'value'], - 'emails_footer' => { 'en' => 'value' } + "sendmail_arguments" => "value", + "brute_force_block_after_failed_logins" => "value", + "password_active_rules" => ["value"], + "default_projects_modules" => ["value", "value"], + "emails_footer" => { "en" => "value" } } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'with password login disabled' do - include_context 'prepare params comparison' + describe "with password login disabled" do + include_context "with prepare params comparison" before do allow(OpenProject::Configuration) @@ -755,27 +751,27 @@ let(:hash) do { - 'sendmail_arguments' => 'value', - 'brute_force_block_after_failed_logins' => 'value', - 'password_active_rules' => ['value'], - 'default_projects_modules' => ['value', 'value'], - 'emails_footer' => { 'en' => 'value' } + "sendmail_arguments" => "value", + "brute_force_block_after_failed_logins" => "value", + "password_active_rules" => ["value"], + "default_projects_modules" => ["value", "value"], + "emails_footer" => { "en" => "value" } } end let(:permitted_hash) do { - 'sendmail_arguments' => 'value', - 'brute_force_block_after_failed_logins' => 'value', - 'default_projects_modules' => ['value', 'value'], - 'emails_footer' => { 'en' => 'value' } + "sendmail_arguments" => "value", + "brute_force_block_after_failed_logins" => "value", + "default_projects_modules" => ["value", "value"], + "emails_footer" => { "en" => "value" } } end it { expect(subject).to eq(permitted_hash) } end - describe 'with writable registration footer' do + describe "with writable registration footer" do before do allow(Setting) .to receive(:registration_footer_writable?) @@ -784,17 +780,17 @@ let(:hash) do { - 'registration_footer' => { - 'en' => 'some footer' + "registration_footer" => { + "en" => "some footer" } } end - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'with a non-writable registration footer (set via env var or config file)' do - include_context 'prepare params comparison' + describe "with a non-writable registration footer (set via env var or config file)" do + include_context "with prepare params comparison" before do allow(Setting) @@ -804,8 +800,8 @@ let(:hash) do { - 'registration_footer' => { - 'en' => 'some footer' + "registration_footer" => { + "en" => "some footer" } } end @@ -818,173 +814,178 @@ end end - describe '#enumerations' do + describe "#enumerations" do let (:attribute) { :enumerations } - describe 'name' do - let(:hash) { { '1' => { 'name' => 'blubs' } } } + describe "name" do + let(:hash) { { "1" => { "name" => "blubs" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'active' do - let(:hash) { { '1' => { 'active' => 'true' } } } + describe "active" do + let(:hash) { { "1" => { "active" => "true" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'is_default' do - let(:hash) { { '1' => { 'is_default' => 'true' } } } + describe "is_default" do + let(:hash) { { "1" => { "is_default" => "true" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'reassign_to_id' do - let(:hash) { { '1' => { 'reassign_to_id' => '1' } } } + describe "reassign_to_id" do + let(:hash) { { "1" => { "reassign_to_id" => "1" } } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'move_to' do - it_behaves_like 'allows enumeration move params' + describe "move_to" do + it_behaves_like "allows enumeration move params" end - describe 'custom fields' do - it_behaves_like 'allows custom fields' + describe "custom fields" do + it_behaves_like "allows custom fields" end end - describe '#wiki_page_rename' do + describe "#wiki_page_rename" do let(:hash_key) { :page } let (:attribute) { :wiki_page_rename } - describe 'title' do - let(:hash) { { 'title' => 'blubs' } } + describe "title" do + let(:hash) { { "title" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'redirect_existing_links' do - let(:hash) { { 'redirect_existing_links' => '1' } } + describe "redirect_existing_links" do + let(:hash) { { "redirect_existing_links" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end end - describe '#wiki_page' do + describe "#wiki_page" do let(:hash_key) { :page } let (:attribute) { :wiki_page } - describe 'title' do - let(:hash) { { 'title' => 'blubs' } } + describe "title" do + let(:hash) { { "title" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'parent_id' do - let(:hash) { { 'parent_id' => '1' } } + describe "parent_id" do + let(:hash) { { "parent_id" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'redirect_existing_links' do - let(:hash) { { 'redirect_existing_links' => '1' } } + describe "redirect_existing_links" do + let(:hash) { { "redirect_existing_links" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'journal_notes' do - let(:hash) { { 'journal_notes' => 'blubs' } } + describe "journal_notes" do + let(:hash) { { "journal_notes" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'text' do - let(:hash) { { 'text' => 'blubs' } } + describe "text" do + let(:hash) { { "text" => "blubs" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'lock_version' do - let(:hash) { { 'lock_version' => '1' } } + describe "lock_version" do + let(:hash) { { "lock_version" => "1" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end end - describe 'member' do + describe "member" do let (:attribute) { :member } - describe 'role_ids' do - let(:hash) { { 'role_ids' => [] } } + describe "role_ids" do + let(:hash) { { "role_ids" => [] } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - describe 'user_id' do - let(:hash) { { 'user_id' => 'blubs' } } + describe "user_id" do + let(:hash) { { "user_id" => "blubs" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end - describe 'project_id' do - let(:hash) { { 'project_id' => 'blubs' } } + describe "project_id" do + let(:hash) { { "project_id" => "blubs" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end - describe 'created_at' do - let(:hash) { { 'created_at' => 'blubs' } } + describe "created_at" do + let(:hash) { { "created_at" => "blubs" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - describe '.add_permitted_attributes' do + describe ".add_permitted_attributes" do before do - @original_permitted_attributes = PermittedParams.permitted_attributes.clone + @original_permitted_attributes = described_class.permitted_attributes.clone end after do # Class variable is not accessible within class_eval original_permitted_attributes = @original_permitted_attributes - PermittedParams.class_eval do + described_class.class_eval do @whitelisted_params = original_permitted_attributes end end - describe 'with a known key' do + describe "with a known key" do let(:attribute) { :user } before do - PermittedParams.send(:add_permitted_attributes, user: [:a_test_field]) + described_class.send(:add_permitted_attributes, user: [:a_test_field]) end - context 'with an allowed parameter' do - let(:hash) { { 'a_test_field' => 'a test value' } } + context "with an allowed parameter" do + let(:hash) { { "a_test_field" => "a test value" } } - it_behaves_like 'allows params' + it_behaves_like "allows params" end - context 'with a disallowed parameter' do - let(:hash) { { 'a_not_allowed_field' => 'a test value' } } + context "with a disallowed parameter" do + let(:hash) { { "a_not_allowed_field" => "a test value" } } - it_behaves_like 'forbids params' + it_behaves_like "forbids params" end end - describe 'with an unknown key' do + describe "with an unknown key" do let(:attribute) { :unknown_key } - let(:hash) { { 'a_test_field' => 'a test value' } } + let(:hash) { { "a_test_field" => "a test value" } } before do - expect(Rails.logger).not_to receive(:warn) - PermittedParams.send(:add_permitted_attributes, unknown_key: [:a_test_field]) + allow(Rails.logger).to receive(:warn) + described_class.send(:add_permitted_attributes, unknown_key: [:a_test_field]) + end + + it "permitted attributes should include the key" do + expect(described_class.permitted_attributes.keys).to include(:unknown_key) end - it 'permitted attributes should include the key' do - expect(PermittedParams.permitted_attributes.keys).to include(:unknown_key) + it "does not log any warnings" do + described_class.permitted_attributes.keys + expect(Rails.logger).not_to have_received(:warn) end end end From c6f332620f9ce5ac82a5379b76bea373ffa53088 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Fri, 24 May 2024 17:46:32 +0200 Subject: [PATCH 37/51] Avoid misleading "Unpermitted parameters: xxx" log When `PermittedParams#settings` was called, it was not permitting any params, which lead to "Unpermitted parameters: ..." messages being logged, and then merged the resulting empty params with the controller params after having filtered them manually. It was ok-ish but the logged error message was misleading. The `#settings` method was refactored to permit params that are allowed in a single step and avoid the misleading log messages. --- app/models/permitted_params.rb | 6 ++-- .../permitted_params/allowed_settings.rb | 25 +++++++++++++ spec/models/permitted_params_spec.rb | 35 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index c768af525dd3..90295e9f3fc0 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -180,10 +180,8 @@ def status end def settings - permitted_params = params.require(:settings).permit - all_valid_keys = AllowedSettings.all - - permitted_params.merge(params[:settings].to_unsafe_hash.slice(*all_valid_keys)) + scalar_filters, complex_filters = AllowedSettings.scalar_and_complex_filters + params.require(:settings).permit(*scalar_filters, **complex_filters) end def user(additional_params = []) diff --git a/app/models/permitted_params/allowed_settings.rb b/app/models/permitted_params/allowed_settings.rb index ef30aab777de..b96f44b7ec8b 100644 --- a/app/models/permitted_params/allowed_settings.rb +++ b/app/models/permitted_params/allowed_settings.rb @@ -29,6 +29,31 @@ def all keys end + def scalar_and_complex_filters + restricted_keys = Set.new(self.restricted_keys) + scalar_filters = [] + complex_filters = {} + Settings::Definition.all.each do |key, definition| # rubocop:disable Rails/FindEach + next if restricted_keys.include?(key) + + case definition.format + when :hash + complex_filters[key] = {} + when :array + complex_filters[key] = [] + else + scalar_filters << key + end + end + + [scalar_filters, complex_filters] + end + + def restricted_keys + restrictions.select(&:applicable?) + .flat_map(&:restricted_keys) + end + def add_restriction!(keys:, condition:) restrictions << Restriction.new(keys, condition) end diff --git a/spec/models/permitted_params_spec.rb b/spec/models/permitted_params_spec.rb index e410c07013b1..23d42e88fda7 100644 --- a/spec/models/permitted_params_spec.rb +++ b/spec/models/permitted_params_spec.rb @@ -812,6 +812,41 @@ it { expect(subject).to eq(expected_permitted_hash) } end + + context "when fetching settings" do + include_context "with prepare params comparison" + + let(:hash) do + { + "registration_footer" => { + "en" => "some footer" + }, + "working_days" => ["", "1", "2", "3", "4", "5"] + } + end + + def recording_notifications_for(notification) + events = [] + subscription = ActiveSupport::Notifications.subscribe notification do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + begin + yield + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + events + end + + it "does not log any 'unpermitted' message" do + events = recording_notifications_for(/unpermitted_parameters/) do + subject + end + expect(events).to be_empty + end + end end describe "#enumerations" do From b98f9d9c5f053d160eeafef09d8325aa8956ef92 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Sat, 25 May 2024 03:08:19 +0000 Subject: [PATCH 38/51] update locales from crowdin [ci skip] --- config/locales/crowdin/ru.seeders.yml | 22 ++++---- config/locales/crowdin/ru.yml | 50 +++++++++---------- .../backlogs/config/locales/crowdin/no.yml | 4 +- .../bim/config/locales/crowdin/ru.seeders.yml | 20 +------- modules/budgets/config/locales/crowdin/no.yml | 10 ++-- modules/costs/config/locales/crowdin/no.yml | 4 +- .../ldap_groups/config/locales/crowdin/no.yml | 2 +- modules/meeting/config/locales/crowdin/no.yml | 10 ++-- .../reporting/config/locales/crowdin/no.yml | 4 +- .../storages/config/locales/crowdin/ru.yml | 28 +++++------ .../config/locales/crowdin/no.yml | 4 +- 11 files changed, 70 insertions(+), 88 deletions(-) diff --git a/config/locales/crowdin/ru.seeders.yml b/config/locales/crowdin/ru.seeders.yml index 697dc4b5d52a..c24362bc7b32 100644 --- a/config/locales/crowdin/ru.seeders.yml +++ b/config/locales/crowdin/ru.seeders.yml @@ -252,20 +252,20 @@ ru: item_4: subject: Окончание проекта wiki: | - _In this wiki you can collaboratively create and edit pages and sub-pages to create a project wiki._ + _В этой вики Вы можете совместно создавать и редактировать страницы и подстраницы для создания вики-проекта._ - **You can:** + **Вы можете:** - * Insert text and images, also with copy and paste from other documents - * Create a page hierarchy with parent pages - * Include wiki pages to the project menu - * Use macros to include, e.g. table of contents, work package lists, or Gantt charts - * Include wiki pages in other text fields, e.g. project overview page - * Include links to other documents - * View the change history - * View as Markdown + * Вставлять текст и изображения, в том числе с помощью копирования и вставки из других документов + * Создавать иерархию страниц с родительскими страницами + * Включать вики-страницы в меню проекта + * Использовать макросы для включения, например, оглавление, списки рабочих пакетов или диаграммы Ганта + * Включать вики-страницы в другие текстовые поля, например, на страницу обзора проекта + * Включать ссылки на другие документы + * Просмотр истории изменений + * Просмотр в формате Markdown - More information: [https://www.openproject.org/docs/user-guide/wiki/](https://www.openproject.org/docs/user-guide/wiki/) + Дополнительная информация: [https://www.openproject.org/docs/user-guide/wiki/](https://www.openproject.org/docs/user-guide/wiki/) scrum-project: name: Scrum-проект status_explanation: Все задачи выполнены по расписанию. Участвующие в них люди знают свои задачи. Система полностью настроена. diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index c71b212a50ce..a2737cc1fb52 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -338,12 +338,12 @@ ru: one: "Однако, %{shared_work_packages_link} также был предоставлен этому пользователю.\n" few: "Однако, %{shared_work_packages_link} также был предоставлен этому пользователю." many: "However, %{shared_work_packages_link} have also been shared with this user." - other: "However, %{shared_work_packages_link} have also been shared with this user." + other: "Однако, %{shared_work_packages_link} также был предоставлен этому пользователю." however_work_packages_shared_with_group_html: one: "Однако, %{shared_work_packages_link} также был предоставлен этой группе." few: "However, %{shared_work_packages_link} have also been shared with this group." many: "However, %{shared_work_packages_link} have also been shared with this group." - other: "However, %{shared_work_packages_link} have also been shared with this group." + other: "Однако, %{shared_work_packages_link} также был предоставлен этой группе." remove_work_packages_shared_with_user_too: "Пользователь, который был удален как участник по-прежнему может получить доступ к общим пакетам работ. Вы хотели бы также удалить эти ресурсы?" remove_work_packages_shared_with_group_too: "Группа, которая была удалена как участник, все еще может получить доступ к общим пакетам работ. Вы хотели бы также удалить эти ресурсы?" will_not_affect_inherited_shares: "(Это не повлияет на пакеты работ, разделенные с их группой)." @@ -352,7 +352,7 @@ ru: one: "Также, %{shared_work_packages_link} был предоставлен этому пользователю." few: "Also, %{shared_work_packages_link} have been shared with this user." many: "Also, %{shared_work_packages_link} have been shared with this user." - other: "Also, %{shared_work_packages_link} have been shared with this user." + other: "Также, %{shared_work_packages_link} был предоставлен этому пользователю." remove_project_membership_or_work_package_shares_too: "Вы хотите удалить только пользователя как непосредственного члена (и сохранить ресурсы) или также удалить пакет работ?" will_remove_all_user_access_priveleges: "Удаление этого участника удалит все права доступа пользователя к проекту. Пользователь по-прежнему будет существовать как часть экземпляра." will_remove_all_group_access_priveleges: "Удаление этого участника удалит все права доступа группы к проекту. Группа по-прежнему будет существовать как часть экземпляра." @@ -365,25 +365,25 @@ ru: one: "%{all_shared_work_packages_link} был предоставлен этому пользователю." few: "%{all_shared_work_packages_link} have been shared with this user." many: "%{all_shared_work_packages_link} have been shared with this user." - other: "%{all_shared_work_packages_link} have been shared with this user." + other: "%{all_shared_work_packages_link} был предоставлен этому пользователю." shared_with_this_group_html: one: "%{all_shared_work_packages_link} был предоставлен этой группе." few: "%{all_shared_work_packages_link} have been shared with this group." many: "%{all_shared_work_packages_link} have been shared with this group." - other: "%{all_shared_work_packages_link} have been shared with this group." + other: "%{all_shared_work_packages_link} был предоставлен этой группе." shared_with_permission_html: one: "Только %{shared_work_packages_link} был предоставлен общий доступ с правами %{shared_role_name}." few: "Only %{shared_work_packages_link} have been shared with %{shared_role_name} permissions." many: "Only %{shared_work_packages_link} have been shared with %{shared_role_name} permissions." - other: "Only %{shared_work_packages_link} have been shared with %{shared_role_name} permissions." + other: "Только %{shared_work_packages_link} был предоставлен общий доступ с правами %{shared_role_name}." revoke_all_or_with_role: "Хотите ли Вы отменить доступ ко всем общим пакетам работ или только к тем, которые имеют разрешения %{shared_role_name}?" will_not_affect_inherited_shares: "(Это не повлияет на пакеты работ, разделенные с их группой)." cannot_remove_inherited: "Общие пакеты работ, к которым предоставлен общий доступ через группы, не могут быть удалены." cannot_remove_inherited_with_role: "Общие пакеты работ с ролью %{shared_role_name} доступны через группы и не могут быть удалены.\n\n\n\n" cannot_remove_inherited_note_admin_html: "Вы можете либо отозвать общий доступ к группе, либо удалить этого конкретного участника из группы в %{administration_settings_link}." cannot_remove_inherited_note_non_admin: "Вы можете либо отозвать общий доступ к группе, либо обратиться к администратору, чтобы удалить этого конкретного участника из группы." - will_revoke_directly_granted_access: "This action will revoke their access to all of them, but the work packages shared with a group." - will_revoke_access_to_all: "This action will revoke their access to all of them." + will_revoke_directly_granted_access: "Это действие лишит их доступа ко всем пакетам работ, кроме тех, которыми поделились с группой." + will_revoke_access_to_all: "Это действие лишит их доступа ко всем пакетам работ." my: access_token: failed_to_reset_token: "Не удалось сбросить маркер доступа: %{error}" @@ -508,7 +508,7 @@ ru: sharing: missing_workflow_warning: title: "Отсутствует рабочий процесс для совместного использования пакета работ" - message: "No workflow is configured for the 'Work package editor' role. Without a workflow, the shared with user cannot alter the status of the work package. Workflows can be copied. Select a source type (e.g. 'Task') and source role (e.g. 'Member'). Then select the target types. To start with, you could select all the types as targets. Finally, select the 'Work package editor' role as the target and press 'Copy'. After having thus created the defaults, fine tune the workflows as you do for every other role." + message: "Для роли «Редактор рабочего пакета» не настроен рабочий процесс. Без рабочего процесса пользователь, к которому предоставлен общий доступ, не может изменить состояние рабочего пакета. Рабочие процессы можно копировать. Выберите тип источника (например, «Задача») и роль источника (например, «Участник»). Затем выберите целевые типы. Для начала вы можете выбрать все типы в качестве целей. Наконец, выберите роль «Редактор рабочего пакета» в качестве цели и нажмите «Копировать». После создания настроек по умолчанию настройте рабочие процессы так же, как и для любой другой роли." link_message: "Настройте рабочие процессы в Администрировании." summary: reports: @@ -1494,10 +1494,10 @@ ru: many: "%{count} часов" other: "%{count} часов" x_hours_abbreviated: - one: "1 ч" + one: "1 час" few: "%{count} hrs" many: "%{count} hrs" - other: "%{count} ч" + other: "%{count} часов" x_weeks: one: "1 неделя" few: "%{count} weeks" @@ -1519,10 +1519,10 @@ ru: many: "%{count} секунд" other: "%{count} секунды" x_seconds_abbreviated: - one: "1 с" + one: "1 секунда" few: "%{count} s" many: "%{count} s" - other: "%{count} с" + other: "%{count} секунд" units: hour: one: "час" @@ -1724,18 +1724,18 @@ ru: dates: working: "%{date} сейчас рабочий" non_working: "%{date} теперь нерабочий" - progress_mode_changed_to_status_based: Режим расчета прогресса установлен на основе статуса + progress_mode_changed_to_status_based: Расчет прогресса установлен в режим "На основе статуса" status_p_complete_changed: >- "% завершения" для статуса '%{status_name}' изменено с %{old_value}% на %{new_value}% system_update: file_links_journal: > Теперь на вкладке Активность появится активность, связанная со ссылками файлов (файлы, хранящиеся во внешних хранилищах). Ниже описывается деятельность по уже существующим ссылкам: progress_calculation_adjusted_from_disabled_mode: >- - Расчет прогресса автоматически переводится в рабочий режим и корректируется при обновлении версии. + Расчет прогресса автоматически устанавливается в режим "На основе трудозатрат" и корректируется при обновлении версии. progress_calculation_adjusted: >- Расчет прогресса автоматически корректируется при обновлении версии. totals_removed_from_childless_work_packages: >- - Work and progress totals automatically removed for non-parent work packages with version update. This is a maintenance task and can be safely ignored. + При обновлении версии автоматически удаляются итоги работы и прогресса для неродительских пакетов работ. Это задача по обслуживанию, и её можно смело игнорировать. links: configuration_guide: "Руководство по конфигурации" get_in_touch: "У вас есть вопросы? Свяжитесь с нами." @@ -2934,7 +2934,7 @@ ru: Если CORS включен, то это источники, которым разрешен доступ к OpenProject API.
Пожалуйста, проверьте документацию по происхождению о том, как указывать ожидаемые значения. setting_apiv3_write_readonly_attributes: "Доступ на запись к атрибутам, доступным только для чтения" setting_apiv3_write_readonly_attributes_instructions_html: > - If enabled, the API will allow administrators to write static read-only attributes during creation, such as createdAt and author.
Warning: This setting has a use-case for e.g., importing data, but allows administrators to impersonate the creation of items as other users. All creation requests are being logged however with the true author.
For more information on attributes and supported resources, please see the %{api_documentation_link}. + Если этот параметр включен, API позволит администраторам записывать во время создания статические атрибуты, доступные только для чтения, такие как «Создано в» и «Автор».
Внимание! Этот параметр можно использовать, например, для импорта данных, но он позволяет администраторам выдавать себя при создании элементов за других пользователей. Однако все запросы на создание регистрируются с указанием истинного автора.
Для получения дополнительной информации об атрибутах и ​​поддерживаемых ресурсах см. %{api_documentation_link}. setting_apiv3_max_page_size: "Максимальный размер страницы API" setting_apiv3_max_page_instructions_html: > Установите максимальный размер страницы, который будет отвечать API. Невозможно выполнить API-запросы, возвращающие много значений на одной странице.
Предупреждение: Пожалуйста, измените это значение, только если вы уверены, зачем вам это нужно. Установка большого значения приведет к значительному снижению производительности, в то время как значение ниже, чем для параметров страницы, вызовет ошибки в представлениях с разбивкой на страницы. @@ -2998,11 +2998,11 @@ ru: setting_file_max_size_displayed: "Максимальная длина строки текстовых файлов" setting_host_name: "Имя компьютера в сети" setting_invitation_expiration_days: "Действие письма активации истекает после" - setting_work_package_done_ratio: "Расчет прогресса" - setting_work_package_done_ratio_field: "Рабочий" - setting_work_package_done_ratio_status: "Статус" + setting_work_package_done_ratio: "Режим расчета прогресса" + setting_work_package_done_ratio_field: "На основе трудозатрат" + setting_work_package_done_ratio_status: "На основе статуса" setting_work_package_done_ratio_explanation_html: > - В режиме На основе работы, % Выполнения рассчитывается на основе того, сколько работы выполнено по отношению к общему объему работы. В режиме На основе статуса, каждый статус имеет связанное с ним значение % Выполнения. Изменение статуса изменит % Выполнения. + В режиме На основе трудозатрат, процент завершения рассчитывается на основе того, сколько работы выполнено по отношению к общему объему работ. В режиме На основе статуса, каждый статус имеет связанное с ним значение процента завершения. Изменение статуса изменит процент завершения. setting_work_package_properties: "Свойства пакета работ" setting_work_package_startdate_is_adddate: "Использовать текущую дату как дату начала для новых пакетов работ" setting_work_packages_projects_export_limit: "Ограничение экспорта пакетов работ / проектов" @@ -3070,7 +3070,7 @@ ru: remaining_scan_complete_html: > Оставшиеся файлы были просканированы. В карантине находится %{file_count} . Вы перенаправляетесь на страницу карантина. Используйте эту страницу, чтобы удалить или отменить файлы, помещенные в карантин. remaining_rescanned_files: > - Virus scanning has been enabled successfully. There are %{file_count} that were uploaded previously and still need to be scanned. This process has been scheduled in the background. The files will remain accessible during the scan. + Проверка на вирусы успешно включена. Есть %{file_count}, которые были загружены ранее и все еще нуждаются в сканировании. Этот процесс был запланирован в фоновом режиме. Файлы останутся доступными во время сканирования. upsale: description: "Убедитесь, что загруженные файлы в OpenProject проверяются на наличие вирусов, прежде чем они станут доступны другим пользователям." actions: @@ -3388,7 +3388,7 @@ ru: modal: work_based_help_text: "% Выполнения автоматически выводится из Работ и Оставшихся работ." status_based_help_text: "% Выполнения определяется статусом пакета работ." - migration_warning_text: "In work-based progress calculation mode, % Complete cannot be manually set and is tied to Work. The existing value has been kept but cannot be edited. Please input Work first." + migration_warning_text: "В режиме расчета прогресса \"На основе трудозатрат\" процент завершения невозможно установить вручную, он привязан к трудозатратам. Существующее значение сохранено, но его нельзя изменить. Пожалуйста, сначала введите трудозатраты." sharing: count: zero: "0 пользователей" @@ -3612,7 +3612,7 @@ ru: label_request_token: "Токен запроса" label_refresh_token: "Токен обновления" errors: - oauth_authorization_code_grant_had_errors: "OAuth2 Authorization grant unsuccessful" + oauth_authorization_code_grant_had_errors: "Ошибка авторизации OAuth2" oauth_reported: "Источник OAuth2 сообщил" oauth_returned_error: "OAuth2 вернул ошибку" oauth_returned_json_error: "OAuth2 вернул ошибку JSON" @@ -3652,7 +3652,7 @@ ru: link: ссылка plugin_openproject_auth_plugins: name: "Плагины аутентификации OpenProject" - description: "Integration of OmniAuth strategy providers for authentication in OpenProject." + description: "Интеграция поставщиков стратегии OmniAuth для аутентификации в OpenProject." plugin_openproject_auth_saml: name: "OmniAuth SAML / Однозначный вход" description: "Добавляет поставщика OmniAuth SAML в OpenProject" diff --git a/modules/backlogs/config/locales/crowdin/no.yml b/modules/backlogs/config/locales/crowdin/no.yml index 1701425b130c..878f0e499e9e 100644 --- a/modules/backlogs/config/locales/crowdin/no.yml +++ b/modules/backlogs/config/locales/crowdin/no.yml @@ -113,7 +113,7 @@ label_column_in_backlog: "Kolonne i forsinkelse" label_hours: " timer" label_work_package_hierarchy: "Hierarki for arbeidspakker" - label_master_backlog: "Master Backlog" + label_master_backlog: "Master Forsinkelse" label_not_prioritized: "ikke prioritert" label_points: "punkter" label_points_burn_down: "Ned" @@ -132,7 +132,7 @@ label_version: 'Versjon' label_webcal: "Webcal Feed" label_wiki: "Wiki" - permission_view_master_backlog: "Vis master backlog" + permission_view_master_backlog: "Vis master forsinkelse" permission_view_taskboards: "Vis oppgavetavler" permission_select_done_statuses: "Velg ferdige statuser" permission_update_sprints: "Oppdater etapper" diff --git a/modules/bim/config/locales/crowdin/ru.seeders.yml b/modules/bim/config/locales/crowdin/ru.seeders.yml index 926938dc2002..78364b883b7c 100644 --- a/modules/bim/config/locales/crowdin/ru.seeders.yml +++ b/modules/bim/config/locales/crowdin/ru.seeders.yml @@ -120,25 +120,7 @@ ru: options: name: Приступая к работе text: | - We are glad you joined! We suggest to try a few things to get started in OpenProject. - - But before you jump right into it, you should know that this exemplary project is split up into two different projects: - - 1. [Construction project]({{opSetting:base_url}}/projects/demo-planning-constructing-project): Here you will find the classical roles, some workflows and work packages for your construction project. - 2. [Creating BIM Model]({{opSetting:base_url}}/projects/demo-bim-project): This project also offers roles, workflows and work packages but especially in the BIM context. - - _Try the following steps:_ - - 1. _Invite new members to your project_: → Go to [Members]({{opSetting:base_url}}/projects/demo-construction-project/members) in the project navigation. - 2. _View the work in your projects_: → Go to [Work packages]({{opSetting:base_url}}/projects/demo-construction-project/work_packages?query_props=%7B%22c%22%3A%5B%22type%22%2C%22id%22%2C%22subject%22%2C%22status%22%2C%22assignee%22%2C%22priority%22%5D%2C%22hl%22%3A%22priority%22%2C%22hi%22%3Atrue%2C%22g%22%3A%22%22%2C%22t%22%3A%22startDate%3Aasc%22%2C%22f%22%3A%5B%7B%22n%22%3A%22bcfIssueAssociated%22%2C%22o%22%3A%22%3D%22%2C%22v%22%3A%5B%22f%22%5D%7D%5D%2C%22pa%22%3A1%2C%22pp%22%3A100%2C%22dr%22%3A%22list%22%7D) in the project navigation. - 3. _Create a new work package_: → Go to [Work packages → Create]({{opSetting:base_url}}/projects/demo-construction-project/work_packages/new?query_props=%7B%22c%22%3A%5B%22type%22%2C%22id%22%2C%22subject%22%2C%22status%22%2C%22assignee%22%2C%22priority%22%5D%2C%22hl%22%3A%22priority%22%2C%22hi%22%3Atrue%2C%22g%22%3A%22%22%2C%22t%22%3A%22startDate%3Aasc%22%2C%22f%22%3A%5B%7B%22n%22%3A%22bcfIssueAssociated%22%2C%22o%22%3A%22%3D%22%2C%22v%22%3A%5B%22f%22%5D%7D%5D%2C%22pa%22%3A1%2C%22pp%22%3A100%2C%22dr%22%3A%22list%22%7D&type=11). - 4. _Create and update a Gantt chart_: → Go to [Gantt chart]({{opSetting:base_url}}/projects/demo-construction-project/work_packages?query_props=%7B%22c%22%3A%5B%22type%22%2C%22id%22%2C%22subject%22%2C%22assignee%22%2C%22responsible%22%5D%2C%22tv%22%3Atrue%2C%22tzl%22%3A%22weeks%22%2C%22hl%22%3A%22priority%22%2C%22hi%22%3Atrue%2C%22g%22%3A%22%22%2C%22t%22%3A%22startDate%3Aasc%22%2C%22f%22%3A%5B%5D%2C%22pa%22%3A1%2C%22pp%22%3A100%2C%22dr%22%3A%22list%22%7D) in the project navigation. - 5. _Activate further modules_: → Go to [Project settings → Modules]({{opSetting:base_url}}/projects/demo-construction-project/settings/modules). - 6. _Check out the tile view to get an overview of your BCF issues:_ → Go to [Work packages]({{opSetting:base_url}}/projects/demo-construction-project/work_packages?query_props=%7B%22c%22%3A%5B%22type%22%2C%22id%22%2C%22subject%22%2C%22status%22%2C%22assignee%22%2C%22priority%22%5D%2C%22hl%22%3A%22priority%22%2C%22hi%22%3Atrue%2C%22g%22%3A%22%22%2C%22t%22%3A%22id%3Aasc%22%2C%22f%22%3A%5B%5D%2C%22pa%22%3A1%2C%22pp%22%3A100%2C%22dr%22%3A%22card%22%7D) - 7. _Agile working? Check out our brand new boards:_ → Go to [Boards]({{opSetting:base_url}}/projects/demo-construction-project/boards) - - Here you will find our [User Guides](https://www.openproject.org/docs/user-guide/). - Please let us know if you have any questions or need support. Contact us: [support\[at\]openproject.com](mailto:support@openproject.com). + shared with me item_4: options: name: Участники diff --git a/modules/budgets/config/locales/crowdin/no.yml b/modules/budgets/config/locales/crowdin/no.yml index 1fa52a07735a..14bf7f262d3f 100644 --- a/modules/budgets/config/locales/crowdin/no.yml +++ b/modules/budgets/config/locales/crowdin/no.yml @@ -33,7 +33,7 @@ spent: "Brukt" status: "Status" subject: "Emne" - type: "Type kostnader" + type: "Kostnadstype" labor_budget: "Planlagte arbeidskostnader" material_budget: "Planlagte enhetskostnader" work_package: @@ -53,12 +53,12 @@ button_cancel_edit_costs: "Avbryt redigering av kostnader" caption_labor: "Arbeid" caption_labor_costs: "Reelle arbeidkostnader" - caption_material_costs: "Reell utgift" + caption_material_costs: "Reell enhetskostnad" budgets_title: "Budsjetter" events: budget: "Budsjett endret" help_click_to_edit: "Klikk her for å redigere." - help_currency_format: "Formatet på vist valuta %n erstattes med valutaverdien, %u" + help_currency_format: "Formatet på vist valuta. %n erstattes med valutaverdien, %u ertsattes av valutaenheten." help_override_rate: "Skriv inn en verdi her for å overstyre standard sats." label_budget: "Budsjett" label_budget_new: "Nytt budsjett" @@ -74,5 +74,5 @@ permission_view_budgets: "Vis budsjetter" project_module_budgets: "Budsjetter" text_budget_reassign_to: "Flytt dem til dette budsjettet:" - text_budget_delete: "Slett budsjettet fra alle oppgaver" - text_budget_destroy_assigned_wp: "Det er %{count} oppgaver i dette budsjettet. Hva vil du gjøre?" + text_budget_delete: "Slett budsjettet fra alle arbeidspakker" + text_budget_destroy_assigned_wp: "Det er %{count} arbeidspakker i dette budsjettet. Hva vil du gjøre?" diff --git a/modules/costs/config/locales/crowdin/no.yml b/modules/costs/config/locales/crowdin/no.yml index 73072ec66c8a..9d326f3b617a 100644 --- a/modules/costs/config/locales/crowdin/no.yml +++ b/modules/costs/config/locales/crowdin/no.yml @@ -46,7 +46,7 @@ default_rates: "Standard satser" models: cost_type: - one: "Type kostnader" + one: "Kostnadstype" other: "Kostnadstyper" rate: "Sats" errors: @@ -56,7 +56,7 @@ nullify_is_not_valid_for_cost_entries: "Kostnadsoppføringer kan ikke tilordnes et prosjekt." attributes: comment: "Kommentar" - cost_type: "Type kostnader" + cost_type: "Kostnadstype" costs: "Kostnader" current_rate: "Nåværende sats" hours: "Timer" diff --git a/modules/ldap_groups/config/locales/crowdin/no.yml b/modules/ldap_groups/config/locales/crowdin/no.yml index 62dfe0b17d9a..18e5f1dca1d9 100644 --- a/modules/ldap_groups/config/locales/crowdin/no.yml +++ b/modules/ldap_groups/config/locales/crowdin/no.yml @@ -11,7 +11,7 @@ ldap_groups/synchronized_filter: filter_string: 'LDAP-filter' ldap_auth_source: 'LDAP-tilkobling' - group_name_attribute: "Gruppenavn-attributt" + group_name_attribute: "Gruppenavn-egenskap" sync_users: 'Synkroniser brukere' base_dn: "Søk i DN-basen" models: diff --git a/modules/meeting/config/locales/crowdin/no.yml b/modules/meeting/config/locales/crowdin/no.yml index 4a8439c0b65b..6bf29beda199 100644 --- a/modules/meeting/config/locales/crowdin/no.yml +++ b/modules/meeting/config/locales/crowdin/no.yml @@ -35,7 +35,7 @@ participants: "Deltagere" participant: one: "1 Deltaker" - other: "%{count} deltakere" + other: "%{count} Deltakere" participants_attended: "Tilsluttete" participants_invited: "Inviterte" project: "Prosjekt" @@ -45,7 +45,7 @@ meeting_agenda_item: title: "Tittel" author: "Forfatter" - duration_in_minutes: "min" + duration_in_minutes: "minutter" description: "Notater" presenter: "Foredragsholder" meeting_section: @@ -107,7 +107,7 @@ agenda: "Kopier saksliste" agenda_text: "Kopier sakslisten fra det forrige møtet" email: - open_meeting_link: "Åpne møte" + open_meeting_link: "Åpent møte" invited: summary: "%{actor} har sendt deg en invitasjon til møtet %{title}" rescheduled: @@ -150,7 +150,7 @@ text_in_hours: "i timer" text_meeting_agenda_for_meeting: 'saksliste for møtet "%{meeting}"' text_meeting_closing_are_you_sure: "Er du sikker på at du vil lukke agendaen?" - text_meeting_agenda_open_are_you_sure: "Dette vil overskrive alle endringer i minuttene! Vil du fortsette?" + text_meeting_agenda_open_are_you_sure: "Dette vil overskrive alle endringer i referatet! Vil du fortsette?" text_meeting_minutes_for_meeting: 'referat for møtet "%{meeting}"' text_notificiation_invited: "Denne e-posten inneholder ny informasjon for møtet nedenfor:" text_meeting_empty_heading: "Ditt møte er tomt" @@ -181,7 +181,7 @@ label_meeting_details: "Møtedetaljer" label_meeting_details_edit: "Rediger møtedetaljer" label_meeting_state: "Møtestatus" - label_meeting_state_open: "Åpne" + label_meeting_state_open: "Åpen" label_meeting_state_open_html: "Åpen" label_meeting_state_closed: "Stengt" label_meeting_state_closed_html: "Stengt" diff --git a/modules/reporting/config/locales/crowdin/no.yml b/modules/reporting/config/locales/crowdin/no.yml index 1d6a4ef67c79..d62803eec530 100644 --- a/modules/reporting/config/locales/crowdin/no.yml +++ b/modules/reporting/config/locales/crowdin/no.yml @@ -27,7 +27,7 @@ comments: "Kommentar" cost_reports_title: "Tid og kostnader" label_cost_report: "Kostnadsrapport" - label_cost_report_plural: "Kostnadsrapport" + label_cost_report_plural: "Kostnadsrapporter" description_drill_down: "Vis detaljer" description_filter_selection: "Utvalg" description_multi_select: "Vis flervalg" @@ -36,7 +36,7 @@ label_click_to_edit: "Klikk for å redigere." label_closed: "lukket" label_columns: "Kolonner" - label_cost_entry_attributes: "Kostnadsattributter" + label_cost_entry_attributes: "Kostnadsegenskaper" label_days_ago: "for de siste dagene" label_entry: "Kostnad" label_filter_text: "Filtrer tekst" diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index 1f13f6749070..80b9abc13c0d 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -40,18 +40,18 @@ ru: errors: too_many_elements_created_at_once: Слишком много элементов, созданных сразу. Ожидалось %{max} максимум - получено %{actual}. external_file_storages: Внешние хранилища файлов - permission_create_files: 'Automatically managed project folders: Create files' - permission_create_files_explanation: This permission is only available for Nextcloud storages - permission_delete_files: 'Automatically managed project folders: Delete files' - permission_delete_files_explanation: This permission is only available for Nextcloud storages + permission_create_files: 'Автоматически управляемые папки проекта: Создание файлов' + permission_create_files_explanation: Это разрешение доступно только для хранилищ Nextcloud + permission_delete_files: 'Автоматически управляемые папки проекта: Удаление файлов' + permission_delete_files_explanation: Это разрешение доступно только для хранилищ Nextcloud permission_header_for_project_module_storages: Автоматически управляемые папки проектов permission_manage_file_links: Управление ссылками файлов permission_manage_storages_in_project: Управление файловыми хранилищами в проекте - permission_read_files: 'Automatically managed project folders: Read files' - permission_share_files: 'Automatically managed project folders: Share files' - permission_share_files_explanation: This permission is only available for Nextcloud storages + permission_read_files: 'Автоматически управляемые папки проекта: Чтение файлов' + permission_share_files: 'Автоматически управляемые папки проекта: Делиться файлами' + permission_share_files_explanation: Это разрешение доступно только для хранилищ Nextcloud permission_view_file_links: Просмотр ссылок на файл - permission_write_files: 'Automatically managed project folders: Write files' + permission_write_files: 'Автоматически управляемые папки проекта: Запись файлов' project_module_storages: Файлы storages: buttons: @@ -68,8 +68,8 @@ ru: one_drive: Разрешить OpenProject доступ к данным Azure, используя OAuth для подключения OneDrive/Sharepoint. redirect_uri_incomplete: one_drive: Завершите установку с правильным перенаправлением URI. - confirm_replace_oauth_application: This action will reset the current OAuth credentials. After confirming you will have to reenter the credentials at the storage provider and all remote users will have to authorize against OpenProject again. Are you sure you want to proceed? - confirm_replace_oauth_client: This action will reset the current OAuth credentials. After confirming you will have to enter new credentials from the storage provider and all users will have to authorize against %{provider_type} again. Are you sure you want to proceed? + confirm_replace_oauth_application: Это действие приведет к сбросу текущих учетных данных OAuth. После подтверждения вам придется повторно ввести учетные данные у поставщика хранилища, и всем удаленным пользователям придется снова авторизоваться в OpenProject. Вы уверены, что хотите продолжить? + confirm_replace_oauth_client: Это действие приведет к сбросу текущих учетных данных OAuth. После подтверждения вам нужно будет ввести новые учетные данные от поставщика хранилища, и всем пользователям придется снова авторизоваться в %{provider_type}. Вы уверены, что хотите продолжить? delete_warning: input_delete_confirmation: Введите имя хранилища файлов %{file_storage} для подтверждения удаления. irreversible_notice: Удаление хранилища файлов является необратимым действием. @@ -85,9 +85,9 @@ ru: access_management: automatic_management: Автоматически управляемый доступ и папки automatic_management_description: Разрешить OpenProject автоматически создавать папки для каждого проекта и управлять доступом пользователей к ним. Это рекомендуется, так как гарантирует, что каждый член команды всегда будет иметь правильные права на доступ. - description: Select the type of management of user access and folder creation. We recommend to use the Automatically managed access to have a more organised structure and guarantee access to all relevant users. + description: Выберите тип управления доступом пользователя и созданием папок. Мы рекомендуем использовать "автоматически управляемый доступ" для более организованной структуры и гарантировать доступ всем необходимым пользователям. manual_management: Вручную управляемый доступ и папки - manual_management_description: Create and manage folders per project manually on your own. You will need to manually ensure relevant users have access. + manual_management_description: Создавайте и управляйте папками для каждого проекта вручную. Вам нужно будет вручную обеспечить доступ соответствующим пользователям. setup_incomplete: Выберите тип управления доступом пользователя и созданием папок. subtitle: Управление доступом title: Доступ и папки проекта @@ -232,7 +232,7 @@ ru: project_storage_members: subtitle: Проверьте статус подключения для хранилища %{storage_name_link} всех участников проекта. title: Статус подключения участников - permission_header_explanation: 'File permissions on external storages are applied only to folders and files within automatically managed project folders. Note that not all file permissions are supported by all storage providers. Please check the documentation on file storage permissions for more information.' + permission_header_explanation: 'Разрешения для файлов во внешних хранилищах применяются только к папкам и файлам в автоматически управляемых папках проекта. Обратите внимание, что не все разрешения для файлов поддерживаются всеми поставщиками хранилищ. Для получения дополнительной информации ознакомьтесь с документацией по разрешениям на хранение файлов.' provider_types: label: Тип поставщика nextcloud: @@ -246,7 +246,7 @@ ru: name: OneDrive/SharePoint name_placeholder: напр. OneDrive show_attachments_toggle: - description: 'Deactivating this option will hide the attachments list on the work packages files tab. The files attached in the description of a work package will still be uploaded in the internal attachments storage. ' + description: 'Деактивация этой опции скроет список вложений на вкладке «Файлы» пакетов работ. Файлы, прикрепленные к описанию пакета работ, по-прежнему будут загружены во внутреннее хранилище вложений.' label: Показывать вложения во вкладке "Файлы" пакетов работ storage_list_blank_slate: description: Добавьте хранилище, чтобы увидеть его здесь. diff --git a/modules/team_planner/config/locales/crowdin/no.yml b/modules/team_planner/config/locales/crowdin/no.yml index b963d67f0a0e..ef969fe7592b 100644 --- a/modules/team_planner/config/locales/crowdin/no.yml +++ b/modules/team_planner/config/locales/crowdin/no.yml @@ -5,12 +5,12 @@ description: "Tilbyr teamplanlegger visninger." permission_view_team_planner: "Vis teamplanlegger" permission_manage_team_planner: "Administrer teamplanlegger" - project_module_team_planner_view: "Teamplanlegger" + project_module_team_planner_view: "Teamplanleggere" team_planner: label_team_planner: "Teamplanlegger" label_new_team_planner: "Ny teamplanlegger" label_create_new_team_planner: "Opprett ny teamplanlegger" - label_team_planner_plural: "Teamplanlegger" + label_team_planner_plural: "Teamplanleggere" label_assignees: "Tildelt" upsale: title: "Teamplanlegger" From 310e11182e34f346d8b6cd3dfdb19fd707b4b06c Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Mon, 27 May 2024 03:05:05 +0000 Subject: [PATCH 39/51] update locales from crowdin [ci skip] --- modules/meeting/config/locales/crowdin/zh-CN.yml | 4 ++-- modules/storages/config/locales/crowdin/lt.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/meeting/config/locales/crowdin/zh-CN.yml b/modules/meeting/config/locales/crowdin/zh-CN.yml index c59e30b1c4fc..fc0170413aba 100644 --- a/modules/meeting/config/locales/crowdin/zh-CN.yml +++ b/modules/meeting/config/locales/crowdin/zh-CN.yml @@ -106,8 +106,8 @@ zh-CN: agenda: "复制议程" agenda_text: "复制之前会议的议程" email: - send_emails: "Send emails" - send_invitation_emails: "Send out invitation emails upon creation" + send_emails: "发送电子邮件" + send_invitation_emails: "创建后发送邀请电子邮件" open_meeting_link: "打开会议" invited: summary: "%{actor} 已经向您发送了一个 %{title} 会议邀请" diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index c11fb68a88fc..ef69139d270e 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -49,7 +49,7 @@ lt: permission_manage_storages_in_project: Tvarkyti failų saugyklas projekte permission_read_files: 'Automatiškai valdomi projekto aplankai: Skaityti failus' permission_share_files: 'Automatiškai valdomi projekto aplankai: Bendrinti failus' - permission_share_files_explanation: Šis teisė galima tik Nextcloud saugykloms + permission_share_files_explanation: Ši teisė galima tik Nextcloud saugykloms permission_view_file_links: Žiūrėti failo nuorodas permission_write_files: 'Automatiškai valdomi projekto aplankai: Rašyti failus' project_module_storages: Failai From 3fa2d6c37cd2cf0878be5030140e418f3bb41764 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Mon, 27 May 2024 03:07:09 +0000 Subject: [PATCH 40/51] update locales from crowdin [ci skip] --- modules/storages/config/locales/crowdin/lt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index c11fb68a88fc..ef69139d270e 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -49,7 +49,7 @@ lt: permission_manage_storages_in_project: Tvarkyti failų saugyklas projekte permission_read_files: 'Automatiškai valdomi projekto aplankai: Skaityti failus' permission_share_files: 'Automatiškai valdomi projekto aplankai: Bendrinti failus' - permission_share_files_explanation: Šis teisė galima tik Nextcloud saugykloms + permission_share_files_explanation: Ši teisė galima tik Nextcloud saugykloms permission_view_file_links: Žiūrėti failo nuorodas permission_write_files: 'Automatiškai valdomi projekto aplankai: Rašyti failus' project_module_storages: Failai From 14bda51f96b807df8cc0cbc2e15f73c75608866f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 05:25:17 +0000 Subject: [PATCH 41/51] Bump @ngx-formly/core from 6.3.0 to 6.3.2 in /frontend Bumps [@ngx-formly/core](https://github.com/ngx-formly/ngx-formly) from 6.3.0 to 6.3.2. - [Release notes](https://github.com/ngx-formly/ngx-formly/releases) - [Changelog](https://github.com/ngx-formly/ngx-formly/blob/main/CHANGELOG.md) - [Commits](https://github.com/ngx-formly/ngx-formly/compare/v6.3.0...v6.3.2) --- updated-dependencies: - dependency-name: "@ngx-formly/core" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d7fb10c6a8e3..945f7dbb8297 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4079,9 +4079,9 @@ } }, "node_modules/@ngx-formly/core": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.0.tgz", - "integrity": "sha512-9qCoPdLLVShoruzXeJUjMdIhfIlHCI+TggA3Wc01ISHTK2vXx1gNIFLuS+hez3JEzu8nIDRuA/nWqj4j8fJCNg==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.2.tgz", + "integrity": "sha512-rPnPDkZp+ns6FxpFBGOJLw3p2JYbIZlM+azNlsmDly4E/WlzX4YOPJNh87nNBINH4OqU99yLURkJFIcGGa9Qcg==", "dependencies": { "tslib": "^2.0.0" }, @@ -25180,9 +25180,9 @@ "integrity": "sha512-CjSVVa/9fzMpEDQP01SC4colKCbZwj7vUq0H2bivp8jVsmd21x9Fu0gDBH0Y9NdfAIm4eGZvmiZKMII3vIOaYQ==" }, "@ngx-formly/core": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.0.tgz", - "integrity": "sha512-9qCoPdLLVShoruzXeJUjMdIhfIlHCI+TggA3Wc01ISHTK2vXx1gNIFLuS+hez3JEzu8nIDRuA/nWqj4j8fJCNg==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.2.tgz", + "integrity": "sha512-rPnPDkZp+ns6FxpFBGOJLw3p2JYbIZlM+azNlsmDly4E/WlzX4YOPJNh87nNBINH4OqU99yLURkJFIcGGa9Qcg==", "requires": { "tslib": "^2.0.0" } From 0e1b28f3424c5f11e256adc279ccebea2fd602f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 05:58:46 +0000 Subject: [PATCH 42/51] Bump google-apis-gmail_v1 from 0.40.0 to 0.41.0 Bumps [google-apis-gmail_v1](https://github.com/googleapis/google-api-ruby-client) from 0.40.0 to 0.41.0. - [Release notes](https://github.com/googleapis/google-api-ruby-client/releases) - [Changelog](https://github.com/googleapis/google-api-ruby-client/blob/main/generated/google-apis-gmail_v1/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-ruby-client/compare/0.40.0...0.41.0) --- updated-dependencies: - dependency-name: google-apis-gmail_v1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1c9ae13e8dbc..ae70812f34c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -579,8 +579,8 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - google-apis-gmail_v1 (0.40.0) - google-apis-core (>= 0.14.0, < 2.a) + google-apis-gmail_v1 (0.41.0) + google-apis-core (>= 0.15.0, < 2.a) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) googleauth (1.11.0) From 8ff0b5cbad88821768fd0f080f16764e523e306e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 06:00:21 +0000 Subject: [PATCH 43/51] Bump webmock from 3.23.0 to 3.23.1 Bumps [webmock](https://github.com/bblimke/webmock) from 3.23.0 to 3.23.1. - [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md) - [Commits](https://github.com/bblimke/webmock/compare/v3.23.0...v3.23.1) --- updated-dependencies: - dependency-name: webmock dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1c9ae13e8dbc..39d377799610 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1134,7 +1134,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.23.0) + webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) From 3416068da9ff2edd38cf2557e72692f7e31f983a Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Mon, 27 May 2024 09:10:56 +0200 Subject: [PATCH 44/51] refactor: simplify filters for permitted params `ActionController::Parameters#permit` takes a list of filters. There is no kwargs argument. Co-authored-by: Klaus Zanders --- app/models/permitted_params.rb | 3 +-- app/models/permitted_params/allowed_settings.rb | 14 +++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 90295e9f3fc0..107ead67dfaa 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -180,8 +180,7 @@ def status end def settings - scalar_filters, complex_filters = AllowedSettings.scalar_and_complex_filters - params.require(:settings).permit(*scalar_filters, **complex_filters) + params.require(:settings).permit(*AllowedSettings.filters) end def user(additional_params = []) diff --git a/app/models/permitted_params/allowed_settings.rb b/app/models/permitted_params/allowed_settings.rb index b96f44b7ec8b..ce1e2b1de59d 100644 --- a/app/models/permitted_params/allowed_settings.rb +++ b/app/models/permitted_params/allowed_settings.rb @@ -29,24 +29,20 @@ def all keys end - def scalar_and_complex_filters + def filters restricted_keys = Set.new(self.restricted_keys) - scalar_filters = [] - complex_filters = {} - Settings::Definition.all.each do |key, definition| # rubocop:disable Rails/FindEach + Settings::Definition.all.flat_map do |key, definition| next if restricted_keys.include?(key) case definition.format when :hash - complex_filters[key] = {} + { key => {} } when :array - complex_filters[key] = [] + { key => [] } else - scalar_filters << key + key end end - - [scalar_filters, complex_filters] end def restricted_keys From b2f0a339a5b0549b00cb3f56f0668d2bbcef5dc4 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Mon, 27 May 2024 09:42:14 +0200 Subject: [PATCH 45/51] Allow to permit additional parameters in `PermittedParams#settings` "attachment_whitelist" is normally permitted as an array parameter, but the form passes it as a string parameter to be split. This commit adds a way to also permit parameters such as "attachment_whitelist" as a string parameter. --- .../admin/settings/api_settings_controller.rb | 6 ++++++ .../settings/attachments_settings_controller.rb | 6 ++++++ app/controllers/admin/settings_controller.rb | 12 +++++++++++- app/models/permitted_params.rb | 4 ++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/settings/api_settings_controller.rb b/app/controllers/admin/settings/api_settings_controller.rb index 249c390e3ce1..f7aae0d4d183 100644 --- a/app/controllers/admin/settings/api_settings_controller.rb +++ b/app/controllers/admin/settings/api_settings_controller.rb @@ -39,5 +39,11 @@ def settings_params settings["apiv3_cors_origins"] = settings["apiv3_cors_origins"].split(/\r?\n/) end end + + def extra_permitted_filters + # attachment_whitelist is normally permitted as an array parameter. + # Explicitly permit it as a string here. + [:apiv3_cors_origins] + end end end diff --git a/app/controllers/admin/settings/attachments_settings_controller.rb b/app/controllers/admin/settings/attachments_settings_controller.rb index 8ab3bb5e6f4b..7929473435ae 100644 --- a/app/controllers/admin/settings/attachments_settings_controller.rb +++ b/app/controllers/admin/settings/attachments_settings_controller.rb @@ -41,5 +41,11 @@ def settings_params settings["attachment_whitelist"] = settings["attachment_whitelist"].split(/\r?\n/) end end + + def extra_permitted_filters + # attachment_whitelist is normally permitted as an array parameter. + # Explicitly permit it as a string here. + [:attachment_whitelist] + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 8d9823ced5a1..c7daac6c37d2 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -90,7 +90,17 @@ def find_plugin end def settings_params - permitted_params.settings.to_h + permitted_params.settings(*extra_permitted_filters).to_h + end + + # Override to allow additional permitted parameters. + # + # Useful when the format of the setting in the parameters is different from + # the expected format in the setting definition, for instance a setting is + # an array in the definition but is passed as a string to be split in the + # parameters. + def extra_permitted_filters + nil end def update_service diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 107ead67dfaa..241c79b26db5 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -179,8 +179,8 @@ def status params.require(:status).permit(*self.class.permitted_attributes[:status]) end - def settings - params.require(:settings).permit(*AllowedSettings.filters) + def settings(extra_permitted_filters = nil) + params.require(:settings).permit(*AllowedSettings.filters, *extra_permitted_filters) end def user(additional_params = []) From 42dce573e217fb27c17f2fbfd9352a865c6218a6 Mon Sep 17 00:00:00 2001 From: Marcello Rocha Date: Fri, 17 May 2024 14:43:26 +0200 Subject: [PATCH 46/51] Makes sure jobs are eventually discarded when concurrency happens --- app/workers/work_packages/apply_working_days_change_job.rb | 4 ++++ app/workers/work_packages/progress/job.rb | 4 ++++ .../app/workers/storages/health_status_mailer_job.rb | 4 ++++ .../app/workers/storages/manage_storage_integrations_job.rb | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/app/workers/work_packages/apply_working_days_change_job.rb b/app/workers/work_packages/apply_working_days_change_job.rb index 8b4f17992374..715b299cfb18 100644 --- a/app/workers/work_packages/apply_working_days_change_job.rb +++ b/app/workers/work_packages/apply_working_days_change_job.rb @@ -34,6 +34,10 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob total_limit: 1 ) + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + wait: 5.minutes, + attempts: 3 + def perform(user_id:, previous_working_days:, previous_non_working_days:) user = User.find(user_id) diff --git a/app/workers/work_packages/progress/job.rb b/app/workers/work_packages/progress/job.rb index 6356bfe6c575..efdce8675918 100644 --- a/app/workers/work_packages/progress/job.rb +++ b/app/workers/work_packages/progress/job.rb @@ -39,4 +39,8 @@ class WorkPackages::Progress::Job < ApplicationJob perform_limit: 1, key: -> { "WorkPackagesProgressJob" } ) + + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + wait: 5.minutes, + attempts: 3 end diff --git a/modules/storages/app/workers/storages/health_status_mailer_job.rb b/modules/storages/app/workers/storages/health_status_mailer_job.rb index 260042ff6cc8..d765e8746104 100644 --- a/modules/storages/app/workers/storages/health_status_mailer_job.rb +++ b/modules/storages/app/workers/storages/health_status_mailer_job.rb @@ -39,6 +39,10 @@ class HealthStatusMailerJob < ApplicationJob key: -> { "#{self.class.name}-#{arguments.last[:storage].id}" } ) + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + wait: 5.minutes, + attempts: 3 + discard_on ActiveJob::DeserializationError def perform(storage:) diff --git a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb index d753e7911524..52082a57182f 100644 --- a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb +++ b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb @@ -46,6 +46,11 @@ class ManageStorageIntegrationsJob < ApplicationJob enqueue_limit: 1, perform_limit: 1 ) + S + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + wait: 5.minutes, + attempts: 3 + SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze KEY = :manage_nextcloud_integration_job_debounce_happened_at CRON_JOB_KEY = :"Storages::ManageStorageIntegrationsJob" From 43002cc328352681c8311868ebe634eb282d1b09 Mon Sep 17 00:00:00 2001 From: Marcello Rocha Date: Fri, 17 May 2024 14:54:17 +0200 Subject: [PATCH 47/51] Limits the CreateDateAlertsNotificationsJob --- .../create_date_alerts_notifications_job.rb | 11 +++++++++++ .../storages/manage_storage_integrations_job.rb | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/workers/notifications/create_date_alerts_notifications_job.rb b/app/workers/notifications/create_date_alerts_notifications_job.rb index c29d714b20e6..e5fb7938ea8c 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job.rb @@ -28,6 +28,17 @@ module Notifications class CreateDateAlertsNotificationsJob < ApplicationJob + include GoodJob::ActiveJobExtensions::Concurrency + + good_job_control_concurrency_with( + enqueue_limit: 1, + perform_limit: 1 + ) + + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + wait: 5.minutes, + attempts: 3 + def perform(user) return unless EnterpriseToken.allows_to?(:date_alerts) diff --git a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb index 52082a57182f..1588ab8b08ce 100644 --- a/modules/storages/app/workers/storages/manage_storage_integrations_job.rb +++ b/modules/storages/app/workers/storages/manage_storage_integrations_job.rb @@ -46,7 +46,7 @@ class ManageStorageIntegrationsJob < ApplicationJob enqueue_limit: 1, perform_limit: 1 ) - S + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, wait: 5.minutes, attempts: 3 From 36108c86c0283b393331ef74126e8979fec11578 Mon Sep 17 00:00:00 2001 From: Marcello Rocha Date: Tue, 21 May 2024 10:25:55 +0200 Subject: [PATCH 48/51] Apply @ulferts feedback --- app/workers/mails/reminder_job.rb | 3 +++ .../create_date_alerts_notifications_job.rb | 9 +-------- .../work_packages/apply_working_days_change_job.rb | 4 ---- app/workers/work_packages/progress/job.rb | 2 +- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/workers/mails/reminder_job.rb b/app/workers/mails/reminder_job.rb index d110423c5a38..7e0884342287 100644 --- a/app/workers/mails/reminder_job.rb +++ b/app/workers/mails/reminder_job.rb @@ -28,6 +28,9 @@ class Mails::ReminderJob < Mails::DeliverJob include ::Notifications::WithMarkedNotifications + include GoodJob::ActiveJobExtensions::Concurrency + + good_job_control_concurrency_with total_limit: 1 private diff --git a/app/workers/notifications/create_date_alerts_notifications_job.rb b/app/workers/notifications/create_date_alerts_notifications_job.rb index e5fb7938ea8c..98e8beb69559 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job.rb @@ -30,14 +30,7 @@ module Notifications class CreateDateAlertsNotificationsJob < ApplicationJob include GoodJob::ActiveJobExtensions::Concurrency - good_job_control_concurrency_with( - enqueue_limit: 1, - perform_limit: 1 - ) - - retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, - wait: 5.minutes, - attempts: 3 + good_job_control_concurrency_with total_limit: 1 def perform(user) return unless EnterpriseToken.allows_to?(:date_alerts) diff --git a/app/workers/work_packages/apply_working_days_change_job.rb b/app/workers/work_packages/apply_working_days_change_job.rb index 715b299cfb18..8b4f17992374 100644 --- a/app/workers/work_packages/apply_working_days_change_job.rb +++ b/app/workers/work_packages/apply_working_days_change_job.rb @@ -34,10 +34,6 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob total_limit: 1 ) - retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, - wait: 5.minutes, - attempts: 3 - def perform(user_id:, previous_working_days:, previous_non_working_days:) user = User.find(user_id) diff --git a/app/workers/work_packages/progress/job.rb b/app/workers/work_packages/progress/job.rb index efdce8675918..c4c1bb47222e 100644 --- a/app/workers/work_packages/progress/job.rb +++ b/app/workers/work_packages/progress/job.rb @@ -42,5 +42,5 @@ class WorkPackages::Progress::Job < ApplicationJob retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, wait: 5.minutes, - attempts: 3 + attempts: :unlimited end From 8c33cb59ef41f3f6f98586b99543e56c8b57bb12 Mon Sep 17 00:00:00 2001 From: Marcello Rocha Date: Tue, 21 May 2024 11:54:32 +0200 Subject: [PATCH 49/51] Adds a concurrency key to the jobs depending on the user_id --- app/workers/mails/reminder_job.rb | 5 ++++- .../notifications/create_date_alerts_notifications_job.rb | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/workers/mails/reminder_job.rb b/app/workers/mails/reminder_job.rb index 7e0884342287..afcac61bac04 100644 --- a/app/workers/mails/reminder_job.rb +++ b/app/workers/mails/reminder_job.rb @@ -30,7 +30,10 @@ class Mails::ReminderJob < Mails::DeliverJob include ::Notifications::WithMarkedNotifications include GoodJob::ActiveJobExtensions::Concurrency - good_job_control_concurrency_with total_limit: 1 + good_job_control_concurrency_with( + total_limit: 1, + key: -> { "#{self.class.name}-#{arguments.last}" } + ) private diff --git a/app/workers/notifications/create_date_alerts_notifications_job.rb b/app/workers/notifications/create_date_alerts_notifications_job.rb index 98e8beb69559..181c50286684 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job.rb @@ -30,7 +30,10 @@ module Notifications class CreateDateAlertsNotificationsJob < ApplicationJob include GoodJob::ActiveJobExtensions::Concurrency - good_job_control_concurrency_with total_limit: 1 + good_job_control_concurrency_with( + total_limit: 1, + key: -> { "#{self.class.name}-#{arguments.last}" } + ) def perform(user) return unless EnterpriseToken.allows_to?(:date_alerts) From f81c34b4e73ef0f21eb6216fda58822faa5718dc Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 27 May 2024 11:43:56 +0200 Subject: [PATCH 50/51] Add a TODO about removing the temporary menu items --- app/components/projects/index_page_header_component.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index dff8c5e76b1c..6f99b4163acd 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -81,6 +81,7 @@ end if query.persisted? + # TODO: Remove section when the sharing modal is implemented (https://community.openproject.org/projects/openproject/work_packages/55163) if can_publish? if query.public? menu.with_item( From 9f37add7965bf82093628f036b21179ca8e5d2ff Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 27 May 2024 14:26:47 +0200 Subject: [PATCH 51/51] Use shared_let and explicitly set user for `public_query` --- .../queries/projects/project_query_spec.rb | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index f86a17593d6a..d095a0349dff 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -389,35 +389,30 @@ end describe "scopes" do + shared_let(:public_query) { create(:project_query, user:, public: true) } + shared_let(:public_query_other_user) { create(:project_query, public: true) } + shared_let(:private_query) { create(:project_query, user:) } + shared_let(:private_query_other_user) { create(:project_query) } + describe ".public_lists" do it "returns only public lists" do - public_query = create(:project_query, public: true) - public_query_other_user = create(:project_query, public: true) - create(:project_query, public: false) - expect(described_class.public_lists).to contain_exactly(public_query, public_query_other_user) end end describe ".private_lists" do it "returns only private lists owned by the user" do - create(:project_query, public: true) - private_query = create(:project_query, public: false) - create(:project_query, public: false) - - expect(described_class.private_lists(user: private_query.user)).to contain_exactly(private_query) + expect(described_class.private_lists(user:)).to contain_exactly(private_query) end end describe ".visible" do it "returns public and private queries owned by the user" do - public_query = create(:project_query, public: true) - public_query_other_user = create(:project_query, public: true) - private_query = create(:project_query, public: false) - create(:project_query, public: false) - - expect(described_class.visible(private_query.user)).to contain_exactly(public_query, public_query_other_user, - private_query) + expect(described_class.visible(user)).to contain_exactly( + public_query, + public_query_other_user, + private_query + ) end end end