diff --git a/.pkgr.yml b/.pkgr.yml index c08cf79c015f..fd9dacf94cb8 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -25,7 +25,6 @@ targets: <<: *debian centos-8: ¢os8 env: - - BUNDLE_BUILD__PG="--with-pg-config=/usr/pgsql-13/bin/pg_config" - NODE_ENV=production - NPM_CONFIG_PRODUCTION=false dependencies: diff --git a/app/services/journals/create_service.rb b/app/services/journals/create_service.rb index 3309da5dbbd7..6c5758cca8c4 100644 --- a/app/services/journals/create_service.rb +++ b/app/services/journals/create_service.rb @@ -229,6 +229,14 @@ def journal_modification_sql(predecessor, notes, cause) def relation_modifications_sql(predecessor) relations = [] + for_supported_associations do |association| + relations << association_modifications_sql(association, predecessor) + end + + relations + end + + def for_supported_associations associations = { attachable?: :attachable, customizable?: :customizable, @@ -238,11 +246,9 @@ def relation_modifications_sql(predecessor) associations.each do |is_associated, association| if journable.respond_to?(is_associated) - relations << association_modifications_sql(association, predecessor) + yield association end end - - relations end def association_modifications_sql(association, predecessor) @@ -612,28 +618,23 @@ def select_max_journal_sql(predecessor) end def select_changed_sql - <<~SQL + sql = <<~SQL SELECT * FROM (#{data_changes_sql}) data_changes - FULL JOIN - (#{customizable_changes_sql}) customizable_changes - ON - customizable_changes.journable_id = data_changes.journable_id - FULL JOIN - (#{attachable_changes_sql}) attachable_changes - ON - attachable_changes.journable_id = data_changes.journable_id - FULL JOIN - (#{storable_changes_sql}) storable_changes - ON - storable_changes.journable_id = data_changes.journable_id - FULL JOIN - (#{agenda_itemable_changes_sql}) agenda_itemable_changes - ON - agenda_itemable_changes.journable_id = data_changes.journable_id SQL + + for_supported_associations do |association| + sql += <<~SQL + FULL JOIN + (#{send(:"#{association}_changes_sql")}) #{association}_changes + ON + #{association}_changes.journable_id = data_changes.journable_id + SQL + end + + sql end def attachable_changes_sql diff --git a/config/application.rb b/config/application.rb index 136fbe7bd0d3..0e48b09bc813 100644 --- a/config/application.rb +++ b/config/application.rb @@ -66,6 +66,10 @@ class Application < Rails::Application # https://community.openproject.org/wp/45463 for details. config.load_defaults 5.0 + # Silence the "multiple database warning" + # Note that this warning can be removed in the 7.1 upgrade + ActiveRecord.suppress_multiple_database_warning = true + # Do not require `belongs_to` associations to be present by default. # Rails 5.0+ default is true. Because of history, lots of tests fail when # set to true. diff --git a/docs/development/code-review-guidelines/README.md b/docs/development/code-review-guidelines/README.md index 9934e1e41f03..d792d6def642 100644 --- a/docs/development/code-review-guidelines/README.md +++ b/docs/development/code-review-guidelines/README.md @@ -135,6 +135,11 @@ Verify that the appropriate tests have been added as documented above. When testing a feature or change, check out the code test at least the happy paths according to the specification of the ticket. +### Documenting changes right away + +If possible, add smaller documentation changes right away. + +If there are breaking changes (e.g., to permissions, code relevant for developers), add them to the release notes draft for the release or create a new draft if none exists yet. ## Other diff --git a/docs/release-notes/13-1-1/README.md b/docs/release-notes/13-1-1/README.md new file mode 100644 index 000000000000..c5d3b9d1fb6d --- /dev/null +++ b/docs/release-notes/13-1-1/README.md @@ -0,0 +1,34 @@ +--- +title: OpenProject 13.1.1 +sidebar_navigation: + title: 13.1.1 +release_version: 13.1.1 +release_date: 2023-12-20 +--- + +# OpenProject 13.1.1 + +Release date: 2023-12-20 + +We released [OpenProject 13.1.1](https://community.openproject.com/versions/1980). +The release contains several bug fixes and we recommend updating to the newest version. + + +#### Bug fixes and changes + +- Fixed: Inconsistent hrefs in wp shared mail \[[#51480](https://community.openproject.com/wp/51480)\] +- Fixed: Slow notification polling \[[#51622](https://community.openproject.com/wp/51622)\] +- Fixed: Error from rails at openproject configure : Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. \[[#51625](https://community.openproject.com/wp/51625)\] +- Fixed: Share modal has two close options \[[#51652](https://community.openproject.com/wp/51652)\] +- Fixed: Missing translation " wrote:" when quoting in work package activity \[[#51656](https://community.openproject.com/wp/51656)\] +- Fixed: Very slow Project.visible scope for administrators \[[#51659](https://community.openproject.com/wp/51659)\] +- Fixed: Meeting agenda create Form Buttons overlap on mobile \[[#51687](https://community.openproject.com/wp/51687)\] +- Fixed: Time and costs \[[#51700](https://community.openproject.com/wp/51700)\] +- Fixed: Pasting into autocompleter does not work initially \[[#51730](https://community.openproject.com/wp/51730)\] + +#### Contributions +A big thanks to community members for reporting bugs and helping us identifying and providing fixes. + +Special thanks for reporting and finding bugs go to + +Tom Gugel, Marek Krempa diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index cc9d20b3b722..5b51f441d537 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -14,6 +14,13 @@ Stay up to date and get an overview of the new features included in the releases +## 13.1.1 + +Release date: 2023-12-20 + +[Release Notes](13-1-1/) + + ## 13.1.0 Release date: 2023-12-13 diff --git a/docs/user-guide/work-packages/share-work-packages/README.md b/docs/user-guide/work-packages/share-work-packages/README.md index 938e258761a9..f3438a4354d3 100644 --- a/docs/user-guide/work-packages/share-work-packages/README.md +++ b/docs/user-guide/work-packages/share-work-packages/README.md @@ -20,7 +20,7 @@ To share a work package with a project non-member select the detailed view of a A dialogue window will open, showing the list of all users, who this work package has already been shared with. If the work package has not yet been shared, the list will be empty. -> **Note**: In order to be able to share a work package with non members you need to have specific rights. If you do not see the option to share a work package, please contact your administrator. +> **Note**: In order to be able to share a work package with non members you need to have been assigned a [global role *create users*](./././system-admin-guide/users-permissions/users/#create-users). If you do not see the option to share a work package, please contact your administrator. ![List of users with access to a work package in OpenProject](openproject_user_guide_shared_with_list.png) @@ -46,7 +46,7 @@ Following user roles are available as filters: ![Filter list of users by user role](openproject_user_guide_sharing_member_role_filter.png) -**Note:** Please keep in mind that users listed after you have applied a filter may have additional permissions. For example if you select the **View** filter, it is possible that a user is listed, which has inherited additional role as part of user group with permissions exceeding the viewing ones. +> **Note:** Please keep in mind that users listed after you have applied a filter may have additional permissions. For example if you select the **View** filter, it is possible that a user is listed, which has inherited additional role as part of user group with permissions exceeding the viewing ones. You can search for a user or a group via a user name, group name or an email address. You can either select an existing user from the dropdown menu or enter an email address for an entirely new user, who will receive an invitation to create an account on your instance. diff --git a/frontend/package.json b/frontend/package.json index dc06897720a2..d3ec9577a089 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -165,7 +165,7 @@ "build:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --watch --named-chunks", "tokens:generate": "theo src/app/spot/styles/tokens/tokens.yml --transform web --format sass,json --dest src/app/spot/styles/tokens/dist", "icon-font:generate": "node ./src/app/spot/icon-font/generate.js ./src/app/spot/icon-font", - "serve": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --public-host http://localhost:4200", + "serve": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --public-host http://${PROXY_HOSTNAME:-localhost}:4200", "serve:test": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --disable-host-check --public-host http://frontend-test:4200", "test": "ng test --watch=false", "test:watch": "ng test --watch=true", diff --git a/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts b/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts index 1fefe053eb2b..3caee976ff52 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-container/filter-container.directive.ts @@ -34,15 +34,19 @@ import { OnDestroy, OnInit, Output, - } from '@angular/core'; -import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service'; +import { + WorkPackageViewFiltersService, +} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service'; import { DebouncedEventEmitter } from 'core-app/shared/helpers/rxjs/debounced-event-emitter'; import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; -import { Observable } from 'rxjs'; +import { from, merge, Observable } from 'rxjs'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; -import { WorkPackageFiltersService } from 'core-app/features/work-packages/components/filters/wp-filters/wp-filters.service'; +import { + WorkPackageFiltersService, +} from 'core-app/features/work-packages/components/filters/wp-filters/wp-filters.service'; +import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service'; @Component({ templateUrl: './filter-container.directive.html', @@ -66,14 +70,17 @@ export class WorkPackageFilterContainerComponent extends UntilDestroyedMixin imp readonly wpTableFilters:WorkPackageViewFiltersService, readonly cdRef:ChangeDetectorRef, readonly wpFiltersService:WorkPackageFiltersService, + readonly wpListService:WorkPackagesListService, ) { super(); this.visible$ = this.wpFiltersService.observeUntil(componentDestroyed(this)); } ngOnInit():void { - this.wpTableFilters - .pristine$() + merge( + this.wpTableFilters.pristine$(), + from(this.wpListService.conditionallyLoadForm()), + ) .pipe( this.untilDestroyed(), ) diff --git a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts index 29815bea7764..07ab2179a787 100644 --- a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts +++ b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts @@ -32,10 +32,15 @@ import { States } from 'core-app/core/states/states.service'; import { IFieldSchema } from 'core-app/shared/components/fields/field.base'; import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing-portal/edit-field-handler'; -import { WorkPackageViewColumnsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-columns.service'; +import { + WorkPackageViewColumnsService, +} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-columns.service'; import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper'; import { EditingPortalService } from 'core-app/shared/components/fields/edit/editing-portal/editing-portal-service'; -import { CellBuilder, editCellContainer, tdClassName } from 'core-app/features/work-packages/components/wp-fast-table/builders/cell-builder'; +import { + CellBuilder, + tdClassName, +} from 'core-app/features/work-packages/components/wp-fast-table/builders/cell-builder'; import { WorkPackageTable } from 'core-app/features/work-packages/components/wp-fast-table/wp-fast-table'; import { EditForm } from 'core-app/shared/components/fields/edit/edit-form/edit-form'; import { editModeClassName } from 'core-app/shared/components/fields/edit/edit-field.component'; @@ -43,6 +48,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { editFieldContainerClass } from 'core-app/shared/components/fields/display/display-field-renderer'; +import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service'; export const activeFieldContainerClassName = 'inline-edit--active-field'; export const activeFieldClassName = 'inline-edit--field'; @@ -58,6 +64,8 @@ export class TableEditForm extends EditForm { @InjectField() public editingPortalService:EditingPortalService; + @InjectField() wpListService:WorkPackagesListService; + // Use cell builder to reset edit fields private cellBuilder = new CellBuilder(this.injector); @@ -134,8 +142,14 @@ export class TableEditForm extends EditForm { } public requireVisible(fieldName:string):Promise { - this.wpTableColumns.addColumn(fieldName); - return this.waitForContainer(fieldName); + // Ensure the query form is loaded before trying to set fields + // as we require new columns to be present + return this.wpListService + .conditionallyLoadForm() + .then(() => { + this.wpTableColumns.addColumn(fieldName); + return this.waitForContainer(fieldName); + }); } protected focusOnFirstError():void { diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts index 75e300241a31..cc0fdf11fd9a 100644 --- a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts @@ -36,21 +36,9 @@ import isPersistedResource from 'core-app/features/hal/helpers/is-persisted-reso import { UrlParamsHelperService } from 'core-app/features/work-packages/components/wp-query/url-params-helper'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { - firstValueFrom, - from, - Observable, - of, -} from 'rxjs'; +import { firstValueFrom, from, Observable, of } from 'rxjs'; import { input } from '@openproject/reactivestates'; -import { - catchError, - mapTo, - mergeMap, - share, - switchMap, - take, -} from 'rxjs/operators'; +import { catchError, mapTo, mergeMap, share, switchMap, take } from 'rxjs/operators'; import { WorkPackageViewPaginationService, } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-pagination.service'; @@ -359,12 +347,19 @@ export class WorkPackagesListService { return this.wpTablePagination.paginationObject; } - private conditionallyLoadForm(query:QueryResource):void { + public conditionallyLoadForm(query = this.currentQuery):Promise { const currentForm = this.querySpace.queryForm.value; + if (!query) { + return firstValueFrom(this.queryLoading) + .then((loaded) => this.conditionallyLoadForm(loaded)); + } + if (!currentForm || query.$links.update.href !== currentForm.href) { - setTimeout(() => this.loadForm(query), 0); + return this.loadForm(query); } + + return Promise.resolve(currentForm); } public get currentQuery() { diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts index 17c200ec8305..37b9593e0c40 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts @@ -109,7 +109,6 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo } protected initializeStates(query:QueryResource) { - void this.loadForm(query); super.initializeStates(query); this.querySpace @@ -127,29 +126,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo }); } - private loadForm(query:QueryResource):Promise { - if (!this.formPromise) { - this.formPromise = firstValueFrom( - this - .apiv3Service - .withOptionalProject(this.projectIdentifier) - .queries - .form - .load(query), - ) - .then(([form, _]) => { - this.wpStatesInitialization.updateStatesFromForm(query, form); - return form; - }) - .catch(() => undefined); - } - - return this.formPromise; - } - public loadQuery(visible = true, firstPage = false):Promise { - // Ensure we are loading the form. - this.formPromise = undefined; if (this.loadedQuery) { const query = this.loadedQuery; diff --git a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts index e1a228592629..3eb66c20365f 100644 --- a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts @@ -360,8 +360,6 @@ export class OpAutocompleterComponent= ?', Time.zone.now) + .where("meetings.start_time + (interval '1 hour' * meetings.duration) >= ?", Time.zone.now) .find_each do |meeting| select.option( label: "#{meeting.title} #{format_date(meeting.start_time)} #{format_time(meeting.start_time, false)}", diff --git a/modules/meeting/config/locales/en.yml b/modules/meeting/config/locales/en.yml index 6e6659ad5554..3f1fde6c15c8 100644 --- a/modules/meeting/config/locales/en.yml +++ b/modules/meeting/config/locales/en.yml @@ -201,7 +201,7 @@ en: label_add_work_package_to_meeting_dialog_title: "Add work package to meeting" label_add_work_package_to_meeting_dialog_button: "Add to meeting" - label_meeting_selection_caption: "It's only possible to add this work package to open, upcoming meetings." + label_meeting_selection_caption: "It's only possible to add this work package to upcoming or ongoing open meetings." text_add_work_package_to_meeting_description: "A work package can be added to one or multiple meetings for discussion. Any notes concerning it are also visible here." text_agenda_item_no_notes: "No notes provided" diff --git a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb index b3edbd651522..41eb7ccd2d54 100644 --- a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb @@ -296,10 +296,13 @@ end context 'when open, upcoming meetings are visible for the user' do - let!(:past_meeting) { create(:structured_meeting, project:, start_time: Date.yesterday - 10.hours) } - let!(:first_upcoming_meeting) { create(:structured_meeting, project:) } - let!(:second_upcoming_meeting) { create(:structured_meeting, project:) } - let!(:closed_upcoming_meeting) { create(:structured_meeting, project:, state: :closed) } + shared_let(:past_meeting) { create(:structured_meeting, project:, start_time: Date.yesterday - 10.hours) } + shared_let(:first_upcoming_meeting) { create(:structured_meeting, project:) } + shared_let(:second_upcoming_meeting) { create(:structured_meeting, project:) } + shared_let(:closed_upcoming_meeting) { create(:structured_meeting, project:, state: :closed) } + shared_let(:ongoing_meeting) do + create(:structured_meeting, title: 'Ongoing', project:, start_time: 1.hour.ago, duration: 4.0) + end it 'enables the user to add the work package to multiple open, upcoming meetings' do work_package_page.visit! @@ -334,6 +337,16 @@ end end + it 'allows the user to select ongoing meetings' do + work_package_page.visit! + switch_to_meetings_tab + + meetings_tab.open_add_to_meeting_dialog + + fill_in('meeting_agenda_item_meeting_id', with: ongoing_meeting.title) + expect(page).to have_css('.ng-option-marked', text: ongoing_meeting.title) + end + it 'does not enable the user to select a past meeting' do work_package_page.visit! switch_to_meetings_tab diff --git a/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb b/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb index 99247f93a3f5..6e7ae17a59d3 100644 --- a/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb +++ b/modules/storages/app/workers/storages/manage_nextcloud_integration_events_job.rb @@ -30,18 +30,28 @@ module Storages class ManageNextcloudIntegrationEventsJob < ApplicationJob include ManageNextcloudIntegrationJobMixin - DEBOUNCE_TIME = 5.seconds.freeze + SINGLE_THREAD_DEBOUNCE_TIME = 4.seconds.freeze + MULTI_THREAD_DEBOUNCE_TIME = 5.seconds.freeze + KEY = :manage_nextcloud_integration_events_job_debounce_happend_at queue_with_priority :above_normal - def self.debounce - count = Delayed::Job - .where("handler LIKE ?", "%job_class: #{self}%") - .where(locked_at: nil) - .where('run_at <= ?', DEBOUNCE_TIME.from_now) - .delete_all - Rails.logger.info("deleted: #{count} jobs") - set(wait: DEBOUNCE_TIME).perform_later + class << self + def debounce + unless debounce_happend_in_current_thread_recently? + Rails.cache.fetch(KEY, expires_in: MULTI_THREAD_DEBOUNCE_TIME) do + set(wait: MULTI_THREAD_DEBOUNCE_TIME).perform_later + RequestStore.store[KEY] = Time.current + end + end + end + + private + + def debounce_happend_in_current_thread_recently? + timestamp = RequestStore.store[KEY] + timestamp.present? && (timestamp + SINGLE_THREAD_DEBOUNCE_TIME) > Time.current + end end def perform diff --git a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb b/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb index 8f87a9295a28..4994a693a750 100644 --- a/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb +++ b/modules/storages/spec/workers/storages/manage_nextcloud_integration_events_job_spec.rb @@ -39,36 +39,30 @@ end describe '.debounce' do - it 'debounces job with 1 minute timeframe' do - ActiveJob::Base.disable_test_adapter + context 'when has been debounced by other thread' do + before do + Rails.cache.write(described_class::KEY, Time.current) + end - other_handler = Storages::ManageNextcloudIntegrationCronJob.perform_later.provider_job_id - same_handler_within_timeframe1 = described_class.set(wait: 1.second).perform_later.provider_job_id - same_handler_within_timeframe2 = described_class.set(wait: 2.seconds).perform_later.provider_job_id - same_handler_within_timeframe3 = described_class.set(wait: 3.seconds).perform_later.provider_job_id - same_handler_out_of_timeframe = described_class.set(wait: 1.minute).perform_later.provider_job_id - same_handler_within_timeframe_in_progress = described_class.set(wait: 18.seconds).perform_later.tap do |job| - # simulate in progress state - Delayed::Job.where(id: job.provider_job_id).update_all(locked_at: Time.current, locked_by: "test_process #{Process.pid}") - end.provider_job_id + it 'does nothing' do + expect { described_class.debounce }.not_to change(enqueued_jobs, :count) + end + end + + context 'when has not been debounced by other thread' do + it 'schedules a job' do + expect { described_class.debounce }.to change(enqueued_jobs, :count).from(0).to(1) + end - expect(Delayed::Job.count).to eq(6) + it 'hits cache once when called 1000 times in a short period of time' do + allow(Rails.cache).to receive(:fetch).and_call_original - described_class.debounce + expect do + 1000.times { described_class.debounce } + end.to change(enqueued_jobs, :count).from(0).to(1) - expect(Delayed::Job.count).to eq(4) - expect(Delayed::Job.pluck(:id)).to include(other_handler, - same_handler_out_of_timeframe, - same_handler_within_timeframe_in_progress) - expect(Delayed::Job.pluck(:id)).not_to include(same_handler_within_timeframe1, - same_handler_within_timeframe2, - same_handler_within_timeframe3) - expect( - Delayed::Job - .where("handler LIKE ?", "%job_class: #{described_class}%") - .last - .run_at - ).to be_within(3.seconds).of(described_class::DEBOUNCE_TIME.from_now) + expect(Rails.cache).to have_received(:fetch).once + end end end diff --git a/spec/lib/open_project/events_spec.rb b/spec/lib/open_project/events_spec.rb index 806304e51d08..f07150f05208 100644 --- a/spec/lib/open_project/events_spec.rb +++ b/spec/lib/open_project/events_spec.rb @@ -36,6 +36,10 @@ def fire_event(event_constant_name) ) end + before do + allow(Storages::ManageNextcloudIntegrationEventsJob).to receive(:debounce) + end + %w[ PROJECT_STORAGE_CREATED PROJECT_STORAGE_UPDATED @@ -48,20 +52,17 @@ def fire_event(event_constant_name) let(:payload) { {} } it do - expect { subject }.not_to change(enqueued_jobs, :count) + subject + expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) end end - context 'when payload contains automatic project_folder_mpde' do + context 'when payload contains automatic project_folder_mode' do let(:payload) { { project_folder_mode: :automatic } } - it do - expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) - end - it do subject - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) end it do @@ -90,13 +91,9 @@ def fire_event(event_constant_name) let(:payload) { {} } - it do - expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) - end - it do subject - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) end end end @@ -108,20 +105,17 @@ def fire_event(event_constant_name) let(:payload) { {} } it do - expect { subject }.not_to change(enqueued_jobs, :count) + subject + expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) end end context 'when payload contains storage integration type' do let(:payload) { { integration_type: 'Storages::Storage' } } - it do - expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) - end - it do subject - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) end end end @@ -133,20 +127,17 @@ def fire_event(event_constant_name) let(:payload) { {} } it do - expect { subject }.not_to change(enqueued_jobs, :count) + subject + expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) end end context 'when payload contains some nextcloud related permissions as a diff' do let(:payload) { { permissions_diff: [:read_files] } } - it do - expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) - end - it do subject - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) end end end @@ -158,20 +149,17 @@ def fire_event(event_constant_name) let(:payload) { {} } it do - expect { subject }.not_to change(enqueued_jobs, :count) + subject + expect(Storages::ManageNextcloudIntegrationEventsJob).not_to have_received(:debounce) end end context 'when payload contains some nextcloud related permissions' do let(:payload) { { permissions: [:read_files] } } - it do - expect { subject }.to change(enqueued_jobs, :count).from(0).to(1) - end - it do subject - expect(enqueued_jobs[0][:job]).to eq(Storages::ManageNextcloudIntegrationEventsJob) + expect(Storages::ManageNextcloudIntegrationEventsJob).to have_received(:debounce) end end end