Merge branch 'release/12.3' into dev

pull/11400/head
ulferts 2 years ago
commit f78f4d6b29
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 28
      app/services/work_packages/schedule_dependency.rb
  2. 15
      app/workers/backup_job.rb
  3. 1
      frontend/src/app/features/boards/new-board-modal/new-board-modal.html
  4. 4
      frontend/src/app/features/boards/tile-view/tile-view.component.html
  5. 4
      frontend/src/app/features/boards/tile-view/tile-view.component.sass
  6. 6
      frontend/src/app/features/calendar/wp-calendar/wp-calendar.component.ts
  7. 8
      frontend/src/app/features/reporting/reporting-page/functionality/reporting_engine/group_bys.js
  8. 8
      frontend/src/app/features/reporting/reporting-page/styles/_reporting_group_by.sass
  9. 4
      frontend/src/app/features/team-planner/team-planner/calendar-drag-drop.service.ts
  10. 17
      frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts
  11. 28
      frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.spec.ts
  12. 43
      frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts
  13. 2
      frontend/src/app/shared/components/tabs/tab.interface.ts
  14. 12
      frontend/src/app/spot/styles/sass/common/container-overrides.sass
  15. 1
      frontend/src/app/spot/styles/sass/components/modal.sass
  16. 26
      lib/api/v3/custom_options/custom_options_api.rb
  17. 2
      modules/costs/spec/lib/api/v3/time_entries/schemas/time_entry_schema_representer_spec.rb
  18. 2
      modules/costs/spec/lib/api/v3/time_entries/time_entry_representer_rendering_spec.rb
  19. 2
      modules/costs/spec/requests/api/time_entries/create_form_resource_spec.rb
  20. 2
      modules/costs/spec/requests/api/time_entries/update_form_resource_spec.rb
  21. 2
      modules/costs/spec/requests/api/time_entry_resource_spec.rb
  22. 2
      modules/reporting/app/models/cost_query/group_by/author_id.rb
  23. 10
      modules/reporting/lib/widget/group_bys.rb
  24. 133
      modules/reporting/spec/models/cost_query/chaining_spec.rb
  25. 120
      modules/reporting/spec/models/cost_query/filter_spec.rb
  26. 125
      modules/reporting/spec/models/cost_query/group_by_spec.rb
  27. 32
      modules/reporting/spec/models/cost_query/integration_spec.rb
  28. 134
      modules/reporting/spec/models/cost_query/operator_spec.rb
  29. 26
      modules/reporting/spec/models/cost_query/result_spec.rb
  30. 22
      modules/reporting/spec/models/cost_query/walker_spec.rb
  31. 5
      modules/reporting/spec/support/query_helper.rb
  32. 1
      modules/team_planner/config/locales/js-en.yml
  33. 81
      spec/factories/custom_field_factory.rb
  34. 188
      spec/requests/api/v3/custom_options/custom_options_resource_spec.rb
  35. 49
      spec/services/work_packages/set_schedule_service_spec.rb
  36. 29
      spec/workers/backup_job_spec.rb

@ -93,6 +93,8 @@ class WorkPackages::ScheduleDependency
end
def descendants(work_package)
# Avoid using WorkPackage.with_ancestors to save database requests.
# All needed data is already loaded.
@descendants ||= {}
@descendants[work_package] ||= begin
children = children_by_parent_id(work_package.id)
@ -101,15 +103,14 @@ class WorkPackages::ScheduleDependency
end
end
# Get relations of type follows for which the given work package is a direct
# follower, or an indirect follower (through parent and/or children).
#
# Used by +Dependency#dependent_ids+ to get work packages that must be
# scheduled prior to the given work package.
def follows_relations(work_package)
@follows_relations ||= {}
@follows_relations[work_package] ||= begin
line = [work_package] + ancestors(work_package) + descendants(work_package)
@follows_relations_by_from_id ||= known_follows_relations.group_by(&:from_id)
@follows_relations_by_from_id
.fetch_values(*line.map(&:id)) { [] }
.flatten
end
@follows_relations[work_package] ||= all_direct_and_indirect_follows_relations_for(work_package)
end
private
@ -117,6 +118,18 @@ class WorkPackages::ScheduleDependency
attr_accessor :known_follows_relations,
:moved_work_packages
def all_direct_and_indirect_follows_relations_for(work_package)
family = ancestors(work_package) + [work_package] + descendants(work_package)
follows_relations_by_follower_id
.fetch_values(*family.pluck(:id)) { [] }
.flatten
.uniq
end
def follows_relations_by_follower_id
@follows_relations_by_follower_id ||= known_follows_relations.group_by(&:from_id)
end
def create_dependencies
moving_work_packages.index_with { |work_package| Dependency.new(work_package, self) }
end
@ -168,6 +181,7 @@ class WorkPackages::ScheduleDependency
WorkPackage
.with_ancestor(known_work_packages)
.where.not(id: known_work_packages.map(&:id))
.distinct
end
# Load all the predecessors of follows relations that are not already loaded.

@ -249,23 +249,30 @@ class BackupJob < ::ApplicationJob
end
def pg_env
config = ActiveRecord::Base.connection_db_config.configuration_hash
entries = pg_env_to_connection_config.map do |key, config_key|
value = config[config_key].to_s
possible_keys = Array(config_key)
value = possible_keys
.lazy
.filter_map { |key| database_config[key] }
.first
[key.to_s, value] if value.present?
[key.to_s, value.to_s] if value.present?
end
entries.compact.to_h
end
def database_config
@database_config ||= ActiveRecord::Base.connection_db_config.configuration_hash
end
##
# Maps the PG env variable name to the key in the AR connection config.
def pg_env_to_connection_config
{
PGHOST: :host,
PGPORT: :port,
PGUSER: :username,
PGUSER: %i[username user],
PGPASSWORD: :password,
PGDATABASE: :database
}

@ -19,6 +19,7 @@
</p>
<p
*ngIf="!eeShowBanners"
class="spot-body-small"
[textContent]="text.select_board_type"></p>
<section class="new-board--section">
<tile-view

@ -1,7 +1,7 @@
<div class="op-tile-block">
<button
*ngFor="let tile of tiles"
class="op-tile-block--tile button"
class="op-tile-block--tile button button_no-margin"
data-qa-selector="op-tile-block"
type="button"
[disabled]="tile.disabled || disable"
@ -16,4 +16,4 @@
<p class="op-tile-block--description" [textContent]="tile.description"></p>
</div>
</button>
</div>
</div>

@ -3,8 +3,7 @@
display: grid
grid-template-rows: repeat(minmax(200px, auto))
grid-template-columns: auto auto
grid-column-gap: 10px
grid-row-gap: 10px
grid-gap: 1rem
&--tile
border-radius: 10px
@ -16,7 +15,6 @@
justify-items: left
background: #f7fafc
min-height: 150px
margin: auto
&:disabled
background: #fafafa

@ -314,10 +314,8 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
}
private handleDateClicked(info:DateSelectArg) {
const startDay = new Date(info.start).getDate();
const endDay = new Date(info.end).getDate();
const duration = endDay - startDay;
const nonWorkingDays = duration !== 1 ? false : this.weekdayService.isNonWorkingDay(info.start);
const due = moment(info.endStr).subtract(1, 'day').toDate();
const nonWorkingDays = this.weekdayService.isNonWorkingDay(info.start) || this.weekdayService.isNonWorkingDay(due);
const defaults = {
startDate: info.startStr,

@ -27,7 +27,7 @@
//++
/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
/*global window, $, $$, Reporting, Effect, Ajax, selectAllOptions, moveOptions, moveOptionUp, moveOptionDown */
/*global _, dragula, I18n, jQuery, Reporting*/
Reporting.GroupBys = (function($){
var group_by_container_ids = function() {
@ -59,6 +59,7 @@ Reporting.GroupBys = (function($){
.attr('class', 'in_row group-by--label')
.attr('for', group_by.attr('id'))
.attr('id', group_by.attr('id') + '_label')
.attr('title', text)
.html(text);
};
@ -106,7 +107,7 @@ Reporting.GroupBys = (function($){
};
var create_group_by = function(field, caption) {
var group_by, label, right_arrow, left_arrow, remove_button;
var group_by, label, remove_button;
group_by = $('<span></span>');
group_by.attr('class', 'group-by--selected-element');
group_by.attr('data-group-by', field);
@ -146,8 +147,7 @@ Reporting.GroupBys = (function($){
};
var add_group_by = function(field, caption, container) {
var group_by, add_groups_select_box, added_container;
add_groups_select_box = container.find('select').first();
var group_by, added_container;
group_by = Reporting.GroupBys.create_group_by(field, caption);
added_container = container.find('.group-by--selected-elements');
added_container.append(group_by);

@ -21,6 +21,8 @@
padding-left: 14px
margin-left: 18px
min-width: 145px
display: flex
align-items: center
fieldset.collapsible.header_collapsible legend.in_row
width: inherit
@ -31,8 +33,8 @@ fieldset.collapsible.header_collapsible legend.in_row
.group-by--label
margin: 0px
padding: 0px 18px 0 0
min-width: 60px
max-width: 110px
text-align: center
white-space: nowrap
font-weight: bold
@ -40,6 +42,8 @@ fieldset.collapsible.header_collapsible legend.in_row
height: 36px
line-height: 36px
cursor: move
overflow: hidden
text-overflow: ellipsis
.group-by--selected-element:after, .group-by--selected-element:before
border: solid transparent
@ -77,6 +81,7 @@ fieldset.collapsible.header_collapsible legend.in_row
line-height: normal
cursor: pointer
color: #FFFFFF
padding: 0 0 0 5px
.group-by--remove:hover
background-color: #3493B3 !important
@ -86,6 +91,7 @@ fieldset.collapsible.header_collapsible legend.in_row
.group-by--control
margin: 0
padding: 0
max-width: 200px
.group-by--selected-elements
background-color: #EEE

@ -10,7 +10,6 @@ import { BehaviorSubject } from 'rxjs';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
import { OpWorkPackagesCalendarService } from 'core-app/features/calendar/op-work-packages-calendar.service';
@Injectable()
@ -119,7 +118,8 @@ export class CalendarDragDropService {
const startDate = moment(workPackage.startDate);
const dueDate = moment(workPackage.dueDate);
const diff = dueDate.diff(startDate, 'days') + 1;
const duration = Number(moment.duration(workPackage.duration).asDays().toFixed(0));
const diff = duration > 0 ? duration : dueDate.diff(startDate, 'days') + 1;
return {
id: `${workPackage.href as string}-external`,

@ -293,6 +293,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
cannot_drag_here: this.I18n.t('js.team_planner.cannot_drag_here'),
updating: this.I18n.t('js.ajax.updating'),
successful_update: this.I18n.t('js.notice_successful_update'),
cannot_drag_to_non_working_day: this.I18n.t('js.team_planner.cannot_drag_to_non_working_day'),
};
principals$ = this.principalIds$
@ -752,17 +753,15 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
}
private handleDateClicked(info:DateSelectArg) {
const startDay = new Date(info.start).getDate();
const endDay = new Date(info.end).getDate();
const duration = endDay - startDay;
const ignoreNonWorkingDays = duration !== 1 ? false : this.weekdayService.isNonWorkingDay(info.start);
const due = moment(info.endStr).subtract(1, 'day').toDate();
const nonWorkingDays = this.weekdayService.isNonWorkingDay(info.start) || this.weekdayService.isNonWorkingDay(due);
this.openNewSplitCreate(
info.startStr,
// end date is exclusive
this.workPackagesCalendar.getEndDateFromTimestamp(info.endStr),
info.resource?.id || '',
ignoreNonWorkingDays,
nonWorkingDays,
);
}
@ -814,7 +813,13 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
private async updateEvent(info:EventResizeDoneArg|EventDropArg|EventReceiveArg):Promise<void> {
const changeset = this.workPackagesCalendar.updateDates(info);
const due = moment(info.event.endStr).subtract(1, 'day').toDate();
const start = moment(info.event.startStr).toDate();
if (!changeset.projectedResource.ignoreNonWorkingDays && (this.weekdayService.isNonWorkingDay(start) || this.weekdayService.isNonWorkingDay(due))) {
this.toastService.addError(this.text.cannot_drag_to_non_working_day);
info?.revert();
return;
}
const resource = info.event.getResources()[0];
if (resource) {
changeset.setValue('assignee', { href: resource.id });

@ -1,18 +1,17 @@
import { HttpClientModule } from '@angular/common/http';
import { Injector, Input } from '@angular/core';
import { Input } from '@angular/core';
import { StateService } from '@uirouter/angular';
import { TestBed } from '@angular/core/testing';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { StateService } from '@uirouter/angular';
import { WorkPackageTabsService } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service';
import {
WorkPackageTabsService,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service';
import { TabComponent } from '../../components/wp-tab-wrapper/tab';
describe('WpTabsService', () => {
let service:WorkPackageTabsService;
const workPackage:any = { id: 1234 };
let injector:Injector;
let halResourceService:HalResourceService;
class TestComponent implements TabComponent {
@Input() public workPackage:WorkPackageResource;
@ -33,6 +32,7 @@ describe('WpTabsService', () => {
};
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [
HttpClientModule,
@ -44,8 +44,6 @@ describe('WpTabsService', () => {
service = TestBed.inject(WorkPackageTabsService);
(service as any).registeredTabs = [];
service.register(displayableTab, notDisplayableTab);
injector = TestBed.inject(Injector);
});
describe('displayableTabs()', () => {
@ -61,4 +59,16 @@ describe('WpTabsService', () => {
expect(service.getTab('non-displayable-test-tab', workPackage)).toEqual(undefined);
});
});
describe('patchTabDefinition()', () => {
it('must change the display condition and return accordingly', () => {
service.patchTabCondition('displayable-test-tab', () => false);
service.patchTabCondition('not-displayable-test-tab', () => true);
const displayableTabs = service.getDisplayableTabs(workPackage);
expect(displayableTabs).toHaveSize(1);
expect(displayableTabs[0].id).toEqual(notDisplayableTab.id);
});
});
});

@ -31,16 +31,34 @@ import { from } from 'rxjs';
import { StateService } from '@uirouter/core';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { WpTabDefinition } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab';
import { WorkPackageRelationsTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/relations-tab/relations-tab.component';
import { WorkPackageOverviewTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/overview-tab/overview-tab.component';
import { WorkPackageActivityTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-tab.component';
import { WorkPackageWatchersTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component';
import { WorkPackageFilesTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component';
import {
WorkPackageRelationsTabComponent,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/relations-tab/relations-tab.component';
import {
WorkPackageOverviewTabComponent,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/overview-tab/overview-tab.component';
import {
WorkPackageActivityTabComponent,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-tab.component';
import {
WorkPackageWatchersTabComponent,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component';
import {
WorkPackageFilesTabComponent,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.component';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { workPackageWatchersCount } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-watchers-count.function';
import { workPackageRelationsCount } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-relations-count.function';
import { workPackageNotificationsCount } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-notifications-count.function';
import { workPackageFilesCount } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function';
import {
workPackageWatchersCount,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-watchers-count.function';
import {
workPackageRelationsCount,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-relations-count.function';
import {
workPackageNotificationsCount,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-notifications-count.function';
import {
workPackageFilesCount,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function';
@Injectable({
providedIn: 'root',
@ -67,6 +85,13 @@ export class WorkPackageTabsService {
];
}
patchTabCondition(id:string, displayable:(workPackage:WorkPackageResource, $state:StateService) => boolean):void {
const tabDefinition = this.registeredTabs.find((tab) => tab.id === id);
if (tabDefinition) {
tabDefinition.displayable = displayable;
}
}
getDisplayableTabs(workPackage:WorkPackageResource):WpTabDefinition[] {
return this
.tabs

@ -4,7 +4,7 @@ import { Injector } from '@angular/core';
export interface TabDefinition {
/** Internal identifier of the tab */
id:string;
/** Human readable name */
/** Human-readable name */
name:string;
/** Manual URL to link to if set */
path?:string;

@ -1,6 +1,10 @@
.spot-container
&.spot-modal--body > *
margin: $spot-spacing-1 $spot-spacing-1 0
// This style must apply for anything inside a spot-modal, that is not yet a spot element.
// The override can get deleted once every modal contains nothing but spot elements, which are styled
// in `./container.sass`
&.spot-modal--body
> *:not(.spot-list, .spot-breadcrumbs, .spot-header-big, .spot-header-small, .spot-subheader-big, .spot-subheader-small, .spot-subheader-extra-small, .spot-body-big, .spot-body-small, .spot-caption, .spot-divider, .spot-text-field)
margin: $spot-spacing-1 $spot-spacing-1 0
&:last-child
margin-bottom: $spot-spacing-1
&:last-child
margin-bottom: $spot-spacing-1

@ -45,6 +45,7 @@
width: $spot-spacing-1-5
height: $spot-spacing-1-5
margin-right: $spot-spacing-0-5
font-size: 1rem
&_highlight
background-color: var(--header-bg-color)

@ -37,12 +37,26 @@ module API
end
helpers do
def authorize_view_in_activated_project(custom_option)
def authorize_custom_option_visibility(custom_option)
case custom_option.custom_field
when WorkPackageCustomField
authorized_work_package_option(custom_option)
when ProjectCustomField
authorize_any(%i[view_project], global: true) { raise API::Errors::NotFound }
when TimeEntryCustomField
authorize_any(%i[log_time log_own_time], global: true) { raise API::Errors::NotFound }
when UserCustomField, GroupCustomField
true
else
raise API::Errors::NotFound
end
end
def authorized_work_package_option(custom_option)
allowed = Project
.allowed_to(current_user, :view_work_packages)
.joins(:work_package_custom_fields)
.where(custom_fields: { id: custom_option.custom_field_id })
.exists?
.allowed_to(current_user, :view_work_packages)
.joins(:work_package_custom_fields)
.exists?(custom_fields: { id: custom_option.custom_field_id })
unless allowed
raise API::Errors::NotFound
@ -53,7 +67,7 @@ module API
get do
co = CustomOption.find(params[:id])
authorize_view_in_activated_project(co)
authorize_custom_option_visibility(co)
CustomOptionRepresenter.new(co, current_user:)
end

@ -268,7 +268,7 @@ describe ::API::V3::TimeEntries::Schemas::TimeEntrySchemaRepresenter do
end
context 'custom value' do
let(:custom_field) { build_stubbed(:time_entry_custom_field) }
let(:custom_field) { build_stubbed(:text_time_entry_custom_field) }
let(:path) { "customField#{custom_field.id}" }
before do

@ -255,7 +255,7 @@ describe ::API::V3::TimeEntries::TimeEntryRepresenter, 'rendering' do
end
context 'custom value' do
let(:custom_field) { build_stubbed(:time_entry_custom_field) }
let(:custom_field) { build_stubbed(:text_time_entry_custom_field) }
let(:custom_value) do
CustomValue.new(custom_field:,
value: '1234',

@ -39,7 +39,7 @@ describe ::API::V3::TimeEntries::CreateFormAPI, content_type: :json do
TimeEntryActivitiesProject.insert(project_id: project.id, activity_id: tea.id, active: false)
end
end
let(:custom_field) { create(:time_entry_custom_field) }
let(:custom_field) { create(:text_time_entry_custom_field) }
let(:user) do
create(:user,
member_in_project: project,

@ -40,7 +40,7 @@ describe ::API::V3::TimeEntries::UpdateFormAPI, content_type: :json do
TimeEntryActivitiesProject.insert(project_id: project.id, activity_id: tea.id, active: false)
end
end
let(:custom_field) { create(:time_entry_custom_field) }
let(:custom_field) { create(:text_time_entry_custom_field) }
let(:user) do
create(:user,
member_in_project: project,

@ -54,7 +54,7 @@ describe 'API v3 time_entry resource', type: :request do
let(:other_project) { other_work_package.project }
let(:role) { create(:role, permissions:) }
let(:permissions) { %i(view_time_entries view_work_packages) }
let(:custom_field) { create(:time_entry_custom_field) }
let(:custom_field) { create(:text_time_entry_custom_field) }
let(:custom_value) do
CustomValue.create(custom_field:,
value: '1234',

@ -27,6 +27,8 @@
#++
class CostQuery::GroupBy::AuthorId < Report::GroupBy::Base
join_table WorkPackage
def self.label
WorkPackage.human_attribute_name(:author)
end

@ -30,9 +30,11 @@ class Widget::GroupBys < Widget::Base
def render_options(group_by_ary)
group_by_ary.sort_by(&:label).map do |group_by|
next unless group_by.selectable?
content_tag :option, value: group_by.underscore_name, 'data-label': CGI::escapeHTML(h(group_by.label)).to_s do
h(group_by.label)
label_text = CGI::escapeHTML(h(group_by.label)).to_s
option_tags = { value: group_by.underscore_name, 'data-label': label_text }
option_tags[:title] = label_text if group_by.label.length > 40
content_tag :option, option_tags do
h(truncate_single_line(group_by.label, length: 40))
end
end.join.html_safe
end
@ -65,7 +67,7 @@ class Widget::GroupBys < Widget::Base
class: 'hidden-for-sighted'
label += content_tag :select, id: "group-by--add-#{type}", class: 'advanced-filters--select' do
content = content_tag :option, I18n.t(:label_group_by_add), value: ''
content = content_tag :option, I18n.t(:label_group_by_add), value: '', disabled: true, selected: true
content += engine::GroupBy.all_grouped.sort_by do |label, _group_by_ary|
I18n.t(label)

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
describe CostQuery, type: :model, reporting_query_helper: true do
let(:project) { create(:project) }
@ -37,72 +37,72 @@ describe CostQuery, type: :model, reporting_query_helper: true do
before do
# FIXME: is there a better way to load all filter and groups?
CostQuery::Filter.all && CostQuery::GroupBy.all
CostQuery.chain_initializer.clear
described_class.chain_initializer.clear
end
after(:all) do
CostQuery.chain_initializer.clear
described_class.chain_initializer.clear
end
it "contains NoFilter" do
expect(@query.chain).to be_a(CostQuery::Filter::NoFilter)
expect(query.chain).to be_a(CostQuery::Filter::NoFilter)
end
it "keeps NoFilter at bottom" do
@query.filter :project_id
expect(@query.chain.bottom).to be_a(CostQuery::Filter::NoFilter)
expect(@query.chain.top).not_to be_a(CostQuery::Filter::NoFilter)
query.filter :project_id
expect(query.chain.bottom).to be_a(CostQuery::Filter::NoFilter)
expect(query.chain.top).not_to be_a(CostQuery::Filter::NoFilter)
end
it "remembers it's correct parent" do
@query.group_by :project_id
@query.filter :project_id
expect(@query.chain.top.child.child.parent).to eq(@query.chain.top.child)
query.group_by :project_id
query.filter :project_id
expect(query.chain.top.child.child.parent).to eq(query.chain.top.child)
end
it "places filter after a group_by" do
@query.group_by :project_id
expect(@query.chain.bottom.parent).to be_a(CostQuery::GroupBy::ProjectId)
expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
query.group_by :project_id
expect(query.chain.bottom.parent).to be_a(CostQuery::GroupBy::ProjectId)
expect(query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
@query.filter :project_id
expect(@query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
query.filter :project_id
expect(query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
expect(query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
end
it "places rows in front of columns when adding a column first" do
@query.column :project_id
expect(@query.chain.bottom.parent.type).to eq(:column)
expect(@query.chain.top.type).to eq(:column)
query.column :project_id
expect(query.chain.bottom.parent.type).to eq(:column)
expect(query.chain.top.type).to eq(:column)
@query.row :project_id
expect(@query.chain.bottom.parent.type).to eq(:column)
expect(@query.chain.top.type).to eq(:row)
query.row :project_id
expect(query.chain.bottom.parent.type).to eq(:column)
expect(query.chain.top.type).to eq(:row)
end
it "places rows in front of filters" do
@query.row :project_id
expect(@query.chain.bottom.parent.type).to eq(:row)
expect(@query.chain.top.type).to eq(:row)
@query.filter :project_id
expect(@query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
expect(@query.chain.top.type).to eq(:row)
query.row :project_id
expect(query.chain.bottom.parent.type).to eq(:row)
expect(query.chain.top.type).to eq(:row)
query.filter :project_id
expect(query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
expect(query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
expect(query.chain.top.type).to eq(:row)
end
it "returns all filters, including the NoFilter" do
@query.filter :project_id
@query.group_by :project_id
expect(@query.filters.size).to eq(2)
expect(@query.filters.map { |f| f.class.underscore_name }).to include "project_id"
query.filter :project_id
query.group_by :project_id
expect(query.filters.size).to eq(2)
expect(query.filters.map { |f| f.class.underscore_name }).to include "project_id"
end
it "returns all group_bys" do
@query.filter :project_id
@query.group_by :project_id
expect(@query.group_bys.size).to eq(1)
expect(@query.group_bys.map { |g| g.class.underscore_name }).to include "project_id"
query.filter :project_id
query.group_by :project_id
expect(query.group_bys.size).to eq(1)
expect(query.group_bys.map { |g| g.class.underscore_name }).to include "project_id"
end
it "initializes the chain through a block" do
@ -112,38 +112,39 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
end
TestFilter.send(:initialize_query_with) { |query| query.filter(:project_id, value: project.id) }
@query.build_new_chain
expect(@query.filters.map { |f| f.class.underscore_name }).to include "project_id"
expect(@query.filters.detect { |f| f.class.underscore_name == "project_id" }.values).to eq(Array(project.id))
query.build_new_chain
expect(query.filters.map { |f| f.class.underscore_name }).to include "project_id"
expect(query.filters.detect { |f| f.class.underscore_name == "project_id" }.values).to eq(Array(project.id))
end
context "store and load" do
let(:new_query) { described_class.deserialize(query.serialize) }
before do
@query.filter :project_id, value: project.id
@query.filter :cost_type_id, value: CostQuery::Filter::CostTypeId.available_values.first
@query.filter :category_id, value: CostQuery::Filter::CategoryId.available_values.first
@query.group_by :activity_id
@query.group_by :budget_id
@query.group_by :cost_type_id
@new_query = CostQuery.deserialize(@query.serialize)
query.filter :project_id, value: project.id
query.filter :cost_type_id, value: CostQuery::Filter::CostTypeId.available_values.first
query.filter :category_id, value: CostQuery::Filter::CategoryId.available_values.first
query.group_by :activity_id
query.group_by :budget_id
query.group_by :cost_type_id
end
it "serializes the chain correctly" do
%i[filters group_bys].each do |type|
@query.send(type).each do |chainable|
expect(@query.serialize[type].collect { |c| c[0] }).to include chainable.class.name.demodulize
query.send(type).each do |chainable|
expect(query.serialize[type].collect { |c| c[0] }).to include chainable.class.name.demodulize
end
end
end
it "deserializes a serialized query correctly" do
expect(@new_query.serialize).to eq(@query.serialize)
expect(new_query.serialize).to eq(query.serialize)
end
it "keeps the order of group bys" do
@query.group_bys.each_with_index do |group_by, index|
query.group_bys.each_with_index do |group_by, index|
# check for order
@new_query.group_bys.each_with_index do |g, ix|
new_query.group_bys.each_with_index do |g, ix|
if g.instance_of?(group_by.class)
expect(ix).to eq(index)
end
@ -152,11 +153,11 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "keeps the right filter values" do
@query.filters.each_with_index do |filter, _index|
query.filters.each_with_index do |filter, _index|
# check for presence
expect(@new_query.filters.any? do |f|
expect(new_query.filters).to be_any do |f|
f.instance_of?(filter.class) && (filter.respond_to?(:values) ? f.values == filter.values : true)
end).to be_truthy
end
end
end
end
@ -164,33 +165,33 @@ describe CostQuery, type: :model, reporting_query_helper: true do
describe Report::Chainable do
describe '#top' do
before { @chain = Report::Chainable.new }
let(:chain) { described_class.new }
it "returns for an one element long chain that chain as top" do
expect(@chain.top).to eq(@chain)
expect(@chain).to be_top
expect(chain.top).to eq(chain)
expect(chain).to be_top
end
it "does not keep the old top when prepending elements" do
Report::Chainable.new @chain
expect(@chain.top).not_to eq(@chain)
expect(@chain).not_to be_top
described_class.new chain
expect(chain.top).not_to eq(chain)
expect(chain).not_to be_top
end
it "sets new top when prepending elements" do
current = @chain
current = chain
10.times do
old = current
current = Report::Chainable.new(current)
current = described_class.new(current)
expect(old.top).to eq(current)
expect(@chain.top).to eq(current)
expect(chain.top).to eq(current)
end
end
end
describe '#inherited_attribute' do
before do
@a = Class.new Report::Chainable
@a = Class.new described_class
@a.inherited_attribute :foo, default: 42
@b = Class.new @a
@c = Class.new @a

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter')
describe CostQuery, type: :model, reporting_query_helper: true do
@ -51,17 +51,17 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "shows all entries when no filter is applied" do
expect(@query.result.count).to eq(Entry.count)
expect(query.result.count).to eq(Entry.count)
end
it "always sets cost_type" do
@query.result.each do |result|
query.result.each do |result|
expect(result["cost_type"]).not_to be_nil
end
end
it "sets activity_id to -1 for cost entries" do
@query.result.each do |result|
query.result.each do |result|
expect(result["activity_id"].to_i).to eq(-1) if result["type"] != "TimeEntry"
end
end
@ -102,29 +102,29 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "onlies return entries from the given #{filter}" do
@query.filter field, value: object.id
@query.result.each do |result|
query.filter field, value: object.id
query.result.each do |result|
expect(result[field].to_s).to eq(object.id.to_s)
end
end
it "allows chaining the same filter" do
@query.filter field, value: object.id
@query.filter field, value: object.id
@query.result.each do |result|
query.filter field, value: object.id
query.filter field, value: object.id
query.result.each do |result|
expect(result[field].to_s).to eq(object.id.to_s)
end
end
it "returns no results for excluding filters" do
@query.filter field, value: object.id
@query.filter field, value: object.id + 1
expect(@query.result.count).to eq(0)
query.filter field, value: object.id
query.filter field, value: object.id + 1
expect(query.result.count).to eq(0)
end
it "computes the correct number of results" do
@query.filter field, value: object.id
expect(@query.result.count).to eq(expected_count)
query.filter field, value: object.id
expect(query.result.count).to eq(expected_count)
end
end
end
@ -157,49 +157,49 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "onlies return entries from the given CostQuery::Filter::AuthorId" do
@query.filter 'author_id', value: author.id
@query.result.each do |result|
query.filter 'author_id', value: author.id
query.result.each do |result|
work_package_id = result["work_package_id"]
expect(WorkPackage.find(work_package_id).author.id).to eq(author.id)
end
end
it "allows chaining the same filter" do
@query.filter 'author_id', value: author.id
@query.filter 'author_id', value: author.id
@query.result.each do |result|
query.filter 'author_id', value: author.id
query.filter 'author_id', value: author.id
query.result.each do |result|
work_package_id = result["work_package_id"]
expect(WorkPackage.find(work_package_id).author.id).to eq(author.id)
end
end
it "returns no results for excluding filters" do
@query.filter 'author_id', value: author.id
@query.filter 'author_id', value: author.id + 1
expect(@query.result.count).to eq(0)
query.filter 'author_id', value: author.id
query.filter 'author_id', value: author.id + 1
expect(query.result.count).to eq(0)
end
it "computes the correct number of results" do
@query.filter 'author_id', value: author.id
expect(@query.result.count).to eq(2)
query.filter 'author_id', value: author.id
expect(query.result.count).to eq(2)
end
end
it "filters spent_on" do
@query.filter :spent_on, operator: 'w'
expect(@query.result.count).to eq(Entry.all.select { |e| e.spent_on.cweek == TimeEntry.all.first.spent_on.cweek }.count)
query.filter :spent_on, operator: 'w'
expect(query.result.count).to eq(Entry.all.select { |e| e.spent_on.cweek == TimeEntry.all.first.spent_on.cweek }.count)
end
it "filters created_at" do
@query.filter :created_on, operator: 't'
query.filter :created_on, operator: 't'
# we assume that some of our fixtures set created_at to Time.now
expect(@query.result.count).to eq(Entry.all.select { |e| e.created_at.to_date == Date.today }.count)
expect(query.result.count).to eq(Entry.all.select { |e| e.created_at.to_date == Time.zone.today }.count)
end
it "filters updated_at" do
@query.filter :updated_on, value: Date.today.years_ago(20), operator: '>d'
query.filter :updated_on, value: Time.zone.today.years_ago(20), operator: '>d'
# we assume that our were updated in the last 20 years
expect(@query.result.count).to eq(Entry.all.select { |e| e.updated_at.to_date > Date.today.years_ago(20) }.count)
expect(query.result.count).to eq(Entry.all.select { |e| e.updated_at.to_date > Time.zone.today.years_ago(20) }.count)
end
it "filters user_id" do
@ -209,8 +209,8 @@ describe CostQuery, type: :model, reporting_query_helper: true do
create_work_package_with_time_entry({}, { user: anonymous })
# create matching entry
create_work_package_with_time_entry
@query.filter :user_id, value: user.id, operator: '='
expect(@query.result.count).to eq(1)
query.filter :user_id, value: user.id, operator: '='
expect(query.result.count).to eq(1)
end
describe "work_package-based filters" do
@ -227,79 +227,79 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "filters overridden_costs" do
@query.filter :overridden_costs, operator: 'y'
expect(@query.result.count).to eq(Entry.all.select { |e| not e.overridden_costs.nil? }.count)
query.filter :overridden_costs, operator: 'y'
expect(query.result.count).to eq(Entry.all.reject { |e| e.overridden_costs.nil? }.count)
end
it "filters status" do
matching_status = create(:status, is_closed: true)
create_work_packages_and_time_entries(3, status: matching_status)
@query.filter :status_id, operator: 'c'
expect(@query.result.count).to eq(3)
query.filter :status_id, operator: 'c'
expect(query.result.count).to eq(3)
end
it "filters types" do
matching_type = project.types.first
create_work_packages_and_time_entries(3, type: matching_type)
@query.filter :type_id, operator: '=', value: matching_type.id
expect(@query.result.count).to eq(3)
query.filter :type_id, operator: '=', value: matching_type.id
expect(query.result.count).to eq(3)
end
it "filters work_package authors" do
matching_author = create_matching_object_with_time_entries(:user, :author, 3)
@query.filter :author_id, operator: '=', value: matching_author.id
expect(@query.result.count).to eq(3)
query.filter :author_id, operator: '=', value: matching_author.id
expect(query.result.count).to eq(3)
end
it "filters priority" do
matching_priority = create_matching_object_with_time_entries(:priority, :priority, 3)
@query.filter :priority_id, operator: '=', value: matching_priority.id
expect(@query.result.count).to eq(3)
query.filter :priority_id, operator: '=', value: matching_priority.id
expect(query.result.count).to eq(3)
end
it "filters assigned to" do
matching_user = create_matching_object_with_time_entries(:user, :assigned_to, 3)
@query.filter :assigned_to_id, operator: '=', value: matching_user.id
expect(@query.result.count).to eq(3)
query.filter :assigned_to_id, operator: '=', value: matching_user.id
expect(query.result.count).to eq(3)
end
it "filters category" do
category = create(:category, project:)
create_work_packages_and_time_entries(3, category:)
@query.filter :category_id, operator: '=', value: category.id
expect(@query.result.count).to eq(3)
query.filter :category_id, operator: '=', value: category.id
expect(query.result.count).to eq(3)
end
it "filters target version" do
matching_version = create(:version, project:)
create_work_packages_and_time_entries(3, version: matching_version)
@query.filter :version_id, operator: '=', value: matching_version.id
expect(@query.result.count).to eq(3)
query.filter :version_id, operator: '=', value: matching_version.id
expect(query.result.count).to eq(3)
end
it "filters subject" do
matching_work_package = create_work_package_with_time_entry(subject: 'matching subject')
@query.filter :subject, operator: '=', value: 'matching subject'
expect(@query.result.count).to eq(1)
query.filter :subject, operator: '=', value: 'matching subject'
expect(query.result.count).to eq(1)
end
it "filters start" do
start_date = Date.new(2013, 1, 1)
matching_work_package = create_work_package_with_time_entry(start_date:)
@query.filter :start_date, operator: '=d', value: start_date
expect(@query.result.count).to eq(1)
query.filter :start_date, operator: '=d', value: start_date
expect(query.result.count).to eq(1)
end
it "filters due date" do
due_date = Date.new(2013, 1, 1)
matching_work_package = create_work_package_with_time_entry(due_date:)
@query.filter :due_date, operator: '=d', value: due_date
expect(@query.result.count).to eq(1)
query.filter :due_date, operator: '=d', value: due_date
expect(query.result.count).to eq(1)
end
it "raises an error if operator is not supported" do
expect { @query.filter :spent_on, operator: 'c' }.to raise_error(ArgumentError)
expect { query.filter :spent_on, operator: 'c' }.to raise_error(ArgumentError)
end
end
@ -426,7 +426,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
it "includes custom fields classes in CustomFieldEntries.all" do
custom_field
expect(CostQuery::Filter::CustomFieldEntries.all)
expect(described_class.all)
.to include(filter_class_name_string(custom_field).constantize)
end
@ -457,15 +457,15 @@ describe CostQuery, type: :model, reporting_query_helper: true do
it "is usable as filter" do
create_searchable_fields_and_values
id = WorkPackageCustomField.find_by(name: "Searchable Field").id
@query.filter "custom_field_#{id}".to_sym, operator: '=', value: "125"
expect(@query.result.count).to eq(2)
query.filter "custom_field_#{id}".to_sym, operator: '=', value: "125"
expect(query.result.count).to eq(2)
end
it "is usable as filter #2" do
create_searchable_fields_and_values
id = WorkPackageCustomField.find_by(name: "Searchable Field").id
@query.filter "custom_field_#{id}".to_sym, operator: '=', value: "finnlabs"
expect(@query.result.count).to eq(0)
query.filter "custom_field_#{id}".to_sym, operator: '=', value: "finnlabs"
expect(query.result.count).to eq(0)
end
end
end

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter')
describe CostQuery, type: :model, reporting_query_helper: true do
@ -75,75 +75,80 @@ describe CostQuery, type: :model, reporting_query_helper: true do
describe CostQuery::GroupBy do
it "computes group_by on projects" do
@query.group_by :project_id
expect(@query.result.size).to eq(2)
query.group_by :project_id
expect(query.result.size).to eq(2)
end
it "keeps own and all parents' group fields in all_group_fields" do
@query.group_by :project_id
@query.group_by :work_package_id
@query.group_by :cost_type_id
expect(@query.all_group_fields).to eq(%w[entries.cost_type_id])
expect(@query.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id])
expect(@query.child.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id entries.project_id])
query.group_by :project_id
query.group_by :work_package_id
query.group_by :cost_type_id
expect(query.all_group_fields).to eq(%w[entries.cost_type_id])
expect(query.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id])
expect(query.child.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id entries.project_id])
end
it "computes group_by WorkPackage" do
@query.group_by :work_package_id
expect(@query.result.size).to eq(2)
query.group_by :work_package_id
expect(query.result.size).to eq(2)
end
it "computes group_by CostType" do
@query.group_by :cost_type_id
query.group_by :cost_type_id
# type 'Labor' for time entries, 2 different cost types
expect(@query.result.size).to eq(3)
expect(query.result.size).to eq(3)
end
it "computes group_by Activity" do
@query.group_by :activity_id
query.group_by :activity_id
# "-1" for time entries, 2 different cost activities
expect(@query.result.size).to eq(3)
expect(query.result.size).to eq(3)
end
it "computes group_by Date (day)" do
@query.group_by :spent_on
expect(@query.result.size).to eq(2)
query.group_by :spent_on
expect(query.result.size).to eq(2)
end
it "computes group_by Date (week)" do
@query.group_by :tweek
expect(@query.result.size).to eq(2)
query.group_by :tweek
expect(query.result.size).to eq(2)
end
it "computes group_by Date (month)" do
@query.group_by :tmonth
expect(@query.result.size).to eq(2)
query.group_by :tmonth
expect(query.result.size).to eq(2)
end
it "computes group_by Date (year)" do
@query.group_by :tyear
expect(@query.result.size).to eq(2)
query.group_by :tyear
expect(query.result.size).to eq(2)
end
it "computes group_by User" do
@query.group_by :user_id
expect(@query.result.size).to eq(4)
query.group_by :user_id
expect(query.result.size).to eq(4)
end
it "computes group_by Author" do
query.group_by :author_id
expect(query.result.size).to eq(2)
end
it "computes group_by Type" do
@query.group_by :type_id
expect(@query.result.size).to eq(1)
query.group_by :type_id
expect(query.result.size).to eq(1)
end
it "computes group_by Budget" do
@query.group_by :budget_id
expect(@query.result.size).to eq(1)
query.group_by :budget_id
expect(query.result.size).to eq(1)
end
it "computes multiple group_by" do
@query.group_by :project_id
@query.group_by :user_id
sql_result = @query.result
query.group_by :project_id
query.group_by :user_id
sql_result = query.result
expect(sql_result.size).to eq(4)
# for each user the number of projects should be correct
@ -159,9 +164,9 @@ describe CostQuery, type: :model, reporting_query_helper: true do
# TODO: ?
it "computes multiple group_by with joins" do
@query.group_by :project_id
@query.group_by :type_id
sql_result = @query.result
query.group_by :project_id
query.group_by :type_id
sql_result = query.result
expect(sql_result.size).to eq(1)
# for each type the number of projects should be correct
sql_sizes = []
@ -175,39 +180,39 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "compute count correct with lots of group_by" do
@query.group_by :project_id
@query.group_by :work_package_id
@query.group_by :cost_type_id
@query.group_by :activity_id
@query.group_by :spent_on
@query.group_by :tweek
@query.group_by :type_id
@query.group_by :tmonth
@query.group_by :tyear
expect(@query.result.count).to eq(8)
query.group_by :project_id
query.group_by :work_package_id
query.group_by :cost_type_id
query.group_by :activity_id
query.group_by :spent_on
query.group_by :tweek
query.group_by :type_id
query.group_by :tmonth
query.group_by :tyear
expect(query.result.count).to eq(8)
end
it "accepts row as a specialised group_by" do
@query.row :project_id
expect(@query.chain.type).to eq(:row)
query.row :project_id
expect(query.chain.type).to eq(:row)
end
it "accepts column as a specialised group_by" do
@query.column :project_id
expect(@query.chain.type).to eq(:column)
query.column :project_id
expect(query.chain.type).to eq(:column)
end
it "has type :column as a default" do
@query.group_by :project_id
expect(@query.chain.type).to eq(:column)
query.group_by :project_id
expect(query.chain.type).to eq(:column)
end
it "aggregates a third group_by which owns at least 2 sub results" do
@query.group_by :tweek
@query.group_by :project_id
@query.group_by :user_id
sql_result = @query.result
query.group_by :tweek
query.group_by :project_id
query.group_by :user_id
sql_result = query.result
expect(sql_result.size).to eq(4)
# for each user the number of projects should be correct
@ -243,7 +248,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
before do
check_cache
CostQuery::GroupBy.all.merge CostQuery::GroupBy::CustomFieldEntries.all
CostQuery::GroupBy.all.merge described_class.all
end
def check_cache
@ -287,7 +292,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "includes custom fields classes in CustomFieldEntries.all" do
expect(CostQuery::GroupBy::CustomFieldEntries.all)
expect(described_class.all)
.to include(group_by_class_name_string(custom_field).constantize)
end
@ -301,8 +306,8 @@ describe CostQuery, type: :model, reporting_query_helper: true do
check_cache
@query.group_by "custom_field_#{custom_field2.id}".to_sym
footprint = @query.result.each_direct_result.map { |c| [c.count, c.units.to_i] }.sort
query.group_by "custom_field_#{custom_field2.id}".to_sym
footprint = query.result.each_direct_result.map { |c| [c.count, c.units.to_i] }.sort
expect(footprint).to eq([[8, 8]])
end
end

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
describe CostQuery, type: :model, reporting_query_helper: true do
minimal_query
@ -47,9 +47,9 @@ describe CostQuery, type: :model, reporting_query_helper: true do
describe "the reporting system" do
it "computes group_by and a filter" do
@query.group_by :project_id
@query.filter :status_id, operator: 'o'
sql_result = @query.result
query.group_by :project_id
query.filter :status_id, operator: 'o'
sql_result = query.result
expect(sql_result.size).to eq(2)
# for each project the number of entries should be correct
@ -63,11 +63,11 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "applies two filter and a group_by correctly" do
@query.filter :project_id, operator: '=', value: [project1.id]
@query.group_by :user_id
@query.filter :overridden_costs, operator: 'n'
query.filter :project_id, operator: '=', value: [project1.id]
query.group_by :user_id
query.filter :overridden_costs, operator: 'n'
sql_result = @query.result
sql_result = query.result
expect(sql_result.size).to eq(2)
# for each user the number of entries should be correct
sql_count = []
@ -80,10 +80,10 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "applies two different filters on the same field" do
@query.filter :project_id, operator: '=', value: [project1.id, project2.id]
@query.filter :project_id, operator: '!', value: [project2.id]
query.filter :project_id, operator: '=', value: [project1.id, project2.id]
query.filter :project_id, operator: '!', value: [project2.id]
sql_result = @query.result
sql_result = query.result
expect(sql_result.count).to eq(2)
end
@ -98,14 +98,14 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
# create a random query
@query.group_by :work_package_id
@query.column :tweek
@query.row :project_id
@query.row :user_id
query.group_by :work_package_id
query.column :tweek
query.row :project_id
query.row :user_id
# count how often a sql query was created
number_of_sql_queries = 0
# do some random things on it
walker = @query.transformer
walker = query.transformer
walker.row_first
walker.column_first
# TODO - to do something

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
describe CostQuery, type: :model, reporting_query_helper: true do
minimal_query
@ -35,7 +35,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
let!(:project2) { create(:project, name: "project2", created_at: 6.minutes.ago) }
describe CostQuery::Operator do
def query(table, field, operator, *values)
def cost_query(table, field, operator, *values)
sql = CostQuery::SqlStatement.new table
yield sql if block_given?
operator.to_operator.modify sql, field, *values
@ -53,11 +53,11 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "does =" do
expect(query('projects', 'id', '=', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '=', project1.id).size).to eq(1)
end
it "does = for multiple values" do
expect(query('projects', 'id', '=', project1.id, project2.id).size).to eq(2)
expect(cost_query('projects', 'id', '=', project1.id, project2.id).size).to eq(2)
end
it "does = for no values" do
@ -68,172 +68,172 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "does = for nil" do
expect(query('projects', 'id', '=', nil).size).to eq(0)
expect(cost_query('projects', 'id', '=', nil).size).to eq(0)
end
it "does = for empty string" do
expect(query('projects', 'id', '=', '').size).to eq(0)
expect(cost_query('projects', 'id', '=', '').size).to eq(0)
end
it "does <=" do
expect(query('projects', 'id', '<=', project2.id - 1).size).to eq(1)
expect(cost_query('projects', 'id', '<=', project2.id - 1).size).to eq(1)
end
it "does >=" do
expect(query('projects', 'id', '>=', project1.id + 1).size).to eq(1)
expect(cost_query('projects', 'id', '>=', project1.id + 1).size).to eq(1)
end
it "does !" do
expect(query('projects', 'id', '!', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '!', project1.id).size).to eq(1)
end
it "does ! for empty string" do
expect(query('projects', 'id', '!', '').size).to eq(0)
expect(cost_query('projects', 'id', '!', '').size).to eq(0)
end
it "does ! for multiple values" do
expect(query('projects', 'id', '!', project1.id, project2.id).size).to eq(0)
expect(cost_query('projects', 'id', '!', project1.id, project2.id).size).to eq(0)
end
it "does !*" do
expect(query('cost_entries', 'project_id', '!*', []).size).to eq(0)
expect(cost_query('cost_entries', 'project_id', '!*', []).size).to eq(0)
end
it "does ~ (contains)" do
expect(query('projects', 'name', '~', 'o').size).to eq(Project.all.select { |p| p.name =~ /o/ }.count)
expect(query('projects', 'name', '~', 'test').size).to eq(Project.all.select { |p| p.name =~ /test/ }.count)
expect(query('projects', 'name', '~', 'child').size).to eq(Project.all.select { |p| p.name =~ /child/ }.count)
expect(cost_query('projects', 'name', '~', 'o').size).to eq(Project.all.select { |p| p.name =~ /o/ }.count)
expect(cost_query('projects', 'name', '~', 'test').size).to eq(Project.all.select { |p| p.name =~ /test/ }.count)
expect(cost_query('projects', 'name', '~', 'child').size).to eq(Project.all.select { |p| p.name =~ /child/ }.count)
end
it "does !~ (not contains)" do
expect(query('projects', 'name', '!~', 'o').size).to eq(Project.all.select { |p| p.name !~ /o/ }.count)
expect(query('projects', 'name', '!~', 'test').size).to eq(Project.all.select { |p| p.name !~ /test/ }.count)
expect(query('projects', 'name', '!~', 'child').size).to eq(Project.all.select { |p| p.name !~ /child/ }.count)
expect(cost_query('projects', 'name', '!~', 'o').size).to eq(Project.all.reject { |p| p.name =~ /o/ }.count)
expect(cost_query('projects', 'name', '!~', 'test').size).to eq(Project.all.reject { |p| p.name =~ /test/ }.count)
expect(cost_query('projects', 'name', '!~', 'child').size).to eq(Project.all.reject { |p| p.name =~ /child/ }.count)
end
it "does c (closed work_package)" do
expect(query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
expect(cost_query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
end
it "does o (open work_package)" do
expect(query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
expect(cost_query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
end
it "does give the correct number of results when counting closed and open work_packages" do
a = query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size
b = query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size
a = cost_query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size
b = cost_query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size
expect(WorkPackage.count).to eq(a + b)
end
it "does w (this week)" do
# somehow this test doesn't work on sundays
n = query('projects', 'created_at', 'w').size
day_in_this_week = Time.now.at_beginning_of_week + 1.day
n = cost_query('projects', 'created_at', 'w').size
day_in_this_week = Time.zone.now.at_beginning_of_week + 1.day
create(:project, created_at: day_in_this_week)
expect(query('projects', 'created_at', 'w').size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 'w').size).to eq(n + 1)
create(:project, created_at: day_in_this_week + 7.days)
create(:project, created_at: day_in_this_week - 7.days)
expect(query('projects', 'created_at', 'w').size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 'w').size).to eq(n + 1)
end
it "does t (today)" do
s = query('projects', 'created_at', 't').size
s = cost_query('projects', 'created_at', 't').size
create(:project, created_at: Date.yesterday)
expect(query('projects', 'created_at', 't').size).to eq(s)
create(:project, created_at: Time.now)
expect(query('projects', 'created_at', 't').size).to eq(s + 1)
expect(cost_query('projects', 'created_at', 't').size).to eq(s)
create(:project, created_at: Time.zone.now)
expect(cost_query('projects', 'created_at', 't').size).to eq(s + 1)
end
it "does <t+ (before the day which is n days in the future)" do
n = query('projects', 'created_at', '<t+', 2).size
n = cost_query('projects', 'created_at', '<t+', 2).size
create(:project, created_at: Date.tomorrow + 1)
expect(query('projects', 'created_at', '<t+', 2).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '<t+', 2).size).to eq(n + 1)
create(:project, created_at: Date.tomorrow + 2)
expect(query('projects', 'created_at', '<t+', 2).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '<t+', 2).size).to eq(n + 1)
end
it "does t+ (n days in the future)" do
n = query('projects', 'created_at', 't+', 1).size
n = cost_query('projects', 'created_at', 't+', 1).size
create(:project, created_at: Date.tomorrow)
expect(query('projects', 'created_at', 't+', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 't+', 1).size).to eq(n + 1)
create(:project, created_at: Date.tomorrow + 2)
expect(query('projects', 'created_at', 't+', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 't+', 1).size).to eq(n + 1)
end
it "does >t+ (after the day which is n days in the future)" do
n = query('projects', 'created_at', '>t+', 1).size
create(:project, created_at: Time.now)
expect(query('projects', 'created_at', '>t+', 1).size).to eq(n)
n = cost_query('projects', 'created_at', '>t+', 1).size
create(:project, created_at: Time.zone.now)
expect(cost_query('projects', 'created_at', '>t+', 1).size).to eq(n)
create(:project, created_at: Date.tomorrow + 1)
expect(query('projects', 'created_at', '>t+', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '>t+', 1).size).to eq(n + 1)
end
it "does >t- (after the day which is n days ago)" do
n = query('projects', 'created_at', '>t-', 1).size
n = cost_query('projects', 'created_at', '>t-', 1).size
create(:project, created_at: Date.today)
expect(query('projects', 'created_at', '>t-', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '>t-', 1).size).to eq(n + 1)
create(:project, created_at: Date.yesterday - 1)
expect(query('projects', 'created_at', '>t-', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '>t-', 1).size).to eq(n + 1)
end
it "does t- (n days ago)" do
n = query('projects', 'created_at', 't-', 1).size
n = cost_query('projects', 'created_at', 't-', 1).size
create(:project, created_at: Date.yesterday)
expect(query('projects', 'created_at', 't-', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 't-', 1).size).to eq(n + 1)
create(:project, created_at: Date.yesterday - 2)
expect(query('projects', 'created_at', 't-', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', 't-', 1).size).to eq(n + 1)
end
it "does <t- (before the day which is n days ago)" do
n = query('projects', 'created_at', '<t-', 1).size
n = cost_query('projects', 'created_at', '<t-', 1).size
create(:project, created_at: Date.today)
expect(query('projects', 'created_at', '<t-', 1).size).to eq(n)
expect(cost_query('projects', 'created_at', '<t-', 1).size).to eq(n)
create(:project, created_at: Date.yesterday - 1)
expect(query('projects', 'created_at', '<t-', 1).size).to eq(n + 1)
expect(cost_query('projects', 'created_at', '<t-', 1).size).to eq(n + 1)
end
# Our own operators
it "does =_child_projects" do
expect(query('projects', 'id', '=_child_projects', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '=_child_projects', project1.id).size).to eq(1)
p_c1 = create_project parent: project1
expect(query('projects', 'id', '=_child_projects', project1.id).size).to eq(2)
expect(cost_query('projects', 'id', '=_child_projects', project1.id).size).to eq(2)
create_project parent: p_c1
expect(query('projects', 'id', '=_child_projects', project1.id).size).to eq(3)
expect(cost_query('projects', 'id', '=_child_projects', project1.id).size).to eq(3)
end
it "does =_child_projects on multiple projects" do
expect(query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(2)
expect(cost_query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(2)
p1_c1 = create_project parent: project1
p2_c1 = create_project parent: project2
expect(query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(4)
expect(cost_query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(4)
p1_c1_c1 = create_project parent: p1_c1
create_project parent: p1_c1_c1
create_project parent: p2_c1
expect(query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(7)
expect(cost_query('projects', 'id', '=_child_projects', project1.id, project2.id).size).to eq(7)
end
it "does !_child_projects" do
expect(query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
p_c1 = create_project parent: project1
expect(query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
create_project parent: project1
create_project parent: p_c1
expect(query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
expect(cost_query('projects', 'id', '!_child_projects', project1.id).size).to eq(1)
create_project
expect(query('projects', 'id', '!_child_projects', project1.id).size).to eq(2)
expect(cost_query('projects', 'id', '!_child_projects', project1.id).size).to eq(2)
end
it "does !_child_projects on multiple projects" do
expect(query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(0)
expect(cost_query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(0)
p1_c1 = create_project parent: project1
p2_c1 = create_project parent: project2
create_project
expect(query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(1)
expect(cost_query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(1)
p1_c1_c1 = create_project parent: p1_c1
create_project parent: p1_c1_c1
create_project parent: p2_c1
create_project
expect(query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(2)
expect(cost_query('projects', 'id', '!_child_projects', project1.id, project2.id).size).to eq(2)
end
it "does =n" do
@ -248,7 +248,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
# y/n seem are for filtering overridden costs
it "does y" do
expect(query_on_entries('overridden_costs', 'y').size).to eq(Entry.all.select { |e| !e.overridden_costs.nil? }.count)
expect(query_on_entries('overridden_costs', 'y').size).to eq(Entry.all.reject { |e| e.overridden_costs.nil? }.count)
end
it "does n" do
@ -257,20 +257,20 @@ describe CostQuery, type: :model, reporting_query_helper: true do
it "does =d" do
# assuming that there aren't more than one project created at the same time
expect(query('projects', 'created_at', '=d', Project.order(Arel.sql('id ASC')).first.created_at).size).to eq(1)
expect(cost_query('projects', 'created_at', '=d', Project.order(Arel.sql('id ASC')).first.created_at).size).to eq(1)
end
it "does <d" do
expect(query('projects', 'created_at', '<d', Time.now).size).to eq(Project.count)
expect(cost_query('projects', 'created_at', '<d', Time.zone.now).size).to eq(Project.count)
end
it "does <>d" do
expect(query('projects', 'created_at', '<>d', Time.now, 5.minutes.from_now).size).to eq(0)
expect(cost_query('projects', 'created_at', '<>d', Time.zone.now, 5.minutes.from_now).size).to eq(0)
end
it "does >d" do
# assuming that all projects were created in the past
expect(query('projects', 'created_at', '>d', Time.now).size).to eq(0)
expect(cost_query('projects', 'created_at', '>d', Time.zone.now).size).to eq(0)
end
describe 'arity' do

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
describe CostQuery, type: :model, reporting_query_helper: true do
before do
@ -105,23 +105,23 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "computes count correctly" do
expect(@query.result.count).to eq(Entry.count)
expect(query.result.count).to eq(Entry.count)
end
it "computes units correctly" do
expect(@query.result.units).to eq(Entry.all.map { |e| e.units }.sum)
expect(query.result.units).to eq(Entry.all.map(&:units).sum)
end
it "computes real_costs correctly" do
expect(@query.result.real_costs).to eq(Entry.all.map { |e| e.overridden_costs || e.costs }.sum)
expect(query.result.real_costs).to eq(Entry.all.map { |e| e.overridden_costs || e.costs }.sum)
end
it "computes count for DirectResults" do
expect(@query.result.values[0].count).to eq(1)
expect(query.result.values[0].count).to eq(1)
end
it "computes units for DirectResults" do
id_sorted = @query.result.values.sort_by { |r| r[:id] }
id_sorted = query.result.values.sort_by { |r| r[:id] }
te_result = id_sorted.select { |r| r[:type] == TimeEntry.to_s }.first
ce_result = id_sorted.select { |r| r[:type] == CostEntry.to_s }.first
expect(te_result.units.to_s).to eq("1.0")
@ -129,7 +129,7 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "computes real_costs for DirectResults" do
id_sorted = @query.result.values.sort_by { |r| r[:id] }
id_sorted = query.result.values.sort_by { |r| r[:id] }
[CostEntry].each do |type|
result = id_sorted.select { |r| r[:type] == type.to_s }.first
first = type.all.first
@ -138,18 +138,18 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "is a column if created with CostQuery.column" do
@query.column :project_id
expect(@query.result.type).to eq(:column)
query.column :project_id
expect(query.result.type).to eq(:column)
end
it "is a row if created with CostQuery.row" do
@query.row :project_id
expect(@query.result.type).to eq(:row)
query.row :project_id
expect(query.result.type).to eq(:row)
end
it "shows the type :direct for its direct results" do
@query.column :project_id
expect(@query.result.first.first.type).to eq(:direct)
query.column :project_id
expect(query.result.first.first.type).to eq(:direct)
end
end
end

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path("#{File.dirname(__FILE__)}/../../spec_helper")
describe CostQuery, type: :model, reporting_query_helper: true do
minimal_query
@ -40,12 +40,12 @@ describe CostQuery, type: :model, reporting_query_helper: true do
describe Report::Transformer do
it "walks down row_first" do
@query.group_by :work_package_id
@query.column :tweek
@query.row :project_id
@query.row :user_id
query.group_by :work_package_id
query.column :tweek
query.row :project_id
query.row :user_id
result = @query.transformer.row_first.values.first
result = query.transformer.row_first.values.first
%i[user_id project_id tweek].each do |field|
expect(result.fields).to include(field)
result = result.values.first
@ -53,12 +53,12 @@ describe CostQuery, type: :model, reporting_query_helper: true do
end
it "walks down column_first" do
@query.group_by :work_package_id
@query.column :tweek
@query.row :project_id
@query.row :user_id
query.group_by :work_package_id
query.column :tweek
query.row :project_id
query.row :user_id
result = @query.transformer.column_first.values.first
result = query.transformer.column_first.values.first
%i[tweek work_package_id].each do |field|
expect(result.fields).to include(field)
result = result.values.first

@ -32,9 +32,10 @@ require 'cost_query/operator'
module OpenProject::Reporting
module QueryHelper
def minimal_query
let(:query) { CostQuery.new }
before do
@query = CostQuery.new
@query.send(:minimal_chain!)
query.send(:minimal_chain!)
end
end
end

@ -15,6 +15,7 @@ en:
today: 'Today'
drag_here_to_remove: 'Drag here to remove assignee and start and end dates.'
cannot_drag_here: 'Cannot remove the work package due to permissions or editing restrictions.'
cannot_drag_to_non_working_day : 'This work package cannot start/finish on a non-working day.'
quick_add:
empty_state: 'Use the search field to find work packages and drag them to the planner to assign it to someone and define start and end dates.'

@ -136,6 +136,47 @@ FactoryBot.define do
end
end
factory :group_custom_field, class: 'GroupCustomField' do
sequence(:name) { |n| "User Custom Field #{n}" }
type { 'GroupCustomField' }
factory :boolean_group_custom_field do
name { 'BooleanGroupCustomField' }
field_format { 'bool' }
end
factory :integer_group_custom_field do
name { 'IntegerGroupCustomField' }
field_format { 'int' }
end
factory :text_group_custom_field do
name { 'TextGroupCustomField' }
field_format { 'text' }
end
factory :string_group_custom_field do
name { 'StringGroupCustomField' }
field_format { 'string' }
end
factory :float_group_custom_field do
name { 'FloatGroupCustomField' }
field_format { 'float' }
end
factory :list_group_custom_field do
name { 'ListGroupCustomField' }
field_format { 'list' }
possible_values { ['1', '2', '3', '4', '5', '6', '7'] }
end
factory :date_group_custom_field do
name { 'DateGroupCustomField' }
field_format { 'date' }
end
end
factory :wp_custom_field, class: 'WorkPackageCustomField' do
sequence(:name) { |n| "Work package custom field #{n}" }
type { 'WorkPackageCustomField' }
@ -208,8 +249,44 @@ FactoryBot.define do
end
factory :time_entry_custom_field, class: 'TimeEntryCustomField' do
field_format { 'text' }
sequence(:name) { |n| "TimeEntryCustomField #{n}" }
sequence(:name) { |n| "User Custom Field #{n}" }
type { 'TimeEntryCustomField' }
factory :boolean_time_entry_custom_field do
name { 'BooleanTimeEntryCustomField' }
field_format { 'bool' }
end
factory :integer_time_entry_custom_field do
name { 'IntegerTimeEntryCustomField' }
field_format { 'int' }
end
factory :text_time_entry_custom_field do
name { 'TextTimeEntryCustomField' }
field_format { 'text' }
end
factory :string_time_entry_custom_field do
name { 'StringTimeEntryCustomField' }
field_format { 'string' }
end
factory :float_time_entry_custom_field do
name { 'FloatTimeEntryCustomField' }
field_format { 'float' }
end
factory :list_time_entry_custom_field do
name { 'ListTimeEntryCustomField' }
field_format { 'list' }
possible_values { ['A', 'B'] }
end
factory :date_time_entry_custom_field do
name { 'DateTimeEntryCustomField' }
field_format { 'date' }
end
end
factory :version_custom_field, class: 'VersionCustomField' do

@ -29,29 +29,19 @@
require 'spec_helper'
require 'rack/test'
describe 'API v3 Custom Options resource' do
describe 'API v3 Custom Options resource', :aggregate_failures do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
shared_let(:project) { create(:project) }
let(:user) do
create(:user,
member_in_project: project,
member_through_role: role)
end
let(:project) { create(:project) }
let(:role) { create(:role, permissions:) }
let(:permissions) { [:view_work_packages] }
let(:custom_field) do
cf = create(:list_wp_custom_field)
project.work_package_custom_fields << cf
cf
end
let(:custom_option) do
create(:custom_option,
custom_field:)
end
let(:modification) { nil }
subject(:response) { last_response }
@ -59,56 +49,178 @@ describe 'API v3 Custom Options resource' do
let(:path) { api_v3_paths.custom_option custom_option.id }
before do
modification&.call
allow(User)
.to receive(:current)
.and_return(user)
.and_return(user)
get path
end
context 'when being allowed' do
it 'is successful' do
expect(subject.status)
.to be(200)
describe 'WorkPackageCustomField' do
shared_let(:custom_field) do
cf = create(:list_wp_custom_field)
project.work_package_custom_fields << cf
cf
end
shared_let(:custom_option) do
create(:custom_option,
custom_field:)
end
context 'when being allowed' do
let(:permissions) { [:view_work_packages] }
it 'is successful' do
expect(subject.status)
.to be(200)
expect(response.body)
.to be_json_eql('CustomOption'.to_json)
.at_path('_type')
expect(response.body)
.to be_json_eql(custom_option.id.to_json)
.at_path('id')
expect(response.body)
.to be_json_eql(custom_option.value.to_json)
.at_path('value')
end
end
it 'returns the custom option' do
expect(response.body)
.to be_json_eql('CustomOption'.to_json)
.at_path('_type')
context 'when lacking permission' do
let(:permissions) { [] }
expect(response.body)
.to be_json_eql(custom_option.id.to_json)
.at_path('id')
it 'is 404' do
expect(subject.status)
.to be(404)
end
end
expect(response.body)
.to be_json_eql(custom_option.value.to_json)
.at_path('value')
context 'when custom option not in project' do
let(:permissions) { [:view_work_packages] }
let(:modification) do
-> do
project.work_package_custom_fields = []
project.save!
end
end
it 'is 404' do
expect(subject.status)
.to be(404)
end
end
end
context 'when lacking permission' do
describe 'ProjectCustomField' do
shared_let(:custom_field) { create(:list_project_custom_field) }
shared_let(:custom_option) { create(:custom_option, custom_field:) }
context 'when being allowed' do
let(:permissions) { [:view_project] }
it 'is successful' do
expect(subject.status)
.to be(200)
expect(response.body)
.to be_json_eql('CustomOption'.to_json)
.at_path('_type')
expect(response.body)
.to be_json_eql(custom_option.id.to_json)
.at_path('id')
expect(response.body)
.to be_json_eql(custom_option.value.to_json)
.at_path('value')
end
end
context 'when lacking permission' do
let(:user) { User.anonymous }
let(:permissions) { [] }
it 'is 404' do
expect(subject.status)
.to be(404)
end
end
end
describe 'TimeEntryCustomField' do
shared_let(:custom_field) { create(:list_time_entry_custom_field) }
shared_let(:custom_option) { create(:custom_option, custom_field:) }
context 'when being allowed with log_time' do
let(:permissions) { [:log_time] }
it 'is successful' do
expect(subject.status)
.to be(200)
expect(response.body)
.to be_json_eql('CustomOption'.to_json)
.at_path('_type')
expect(response.body)
.to be_json_eql(custom_option.id.to_json)
.at_path('id')
expect(response.body)
.to be_json_eql(custom_option.value.to_json)
.at_path('value')
end
end
context 'when being allowed with log_own_time' do
let(:permissions) { [:log_own_time] }
it 'is successful' do
expect(subject.status)
.to be(200)
end
end
context 'when lacking permission' do
let(:user) { User.anonymous }
let(:permissions) { [] }
it 'is 404' do
expect(subject.status)
.to be(404)
end
end
end
describe 'UserCustomField' do
shared_let(:custom_field) { create(:list_user_custom_field) }
shared_let(:custom_option) { create(:custom_option, custom_field:) }
let(:permissions) { [] }
it 'is 404' do
it 'is successful' do
expect(subject.status)
.to be(404)
.to be(200)
end
end
context 'when custom option not in project' do
let(:custom_field) do
# not added to project
create(:list_wp_custom_field)
end
describe 'GroupCustomField' do
shared_let(:custom_field) { create(:list_group_custom_field) }
shared_let(:custom_option) { create(:custom_option, custom_field:) }
let(:permissions) { [] }
it 'is 404' do
it 'is successful' do
expect(subject.status)
.to be(404)
.to be(200)
end
end
context 'when not existing' do
let(:path) { api_v3_paths.custom_option 0 }
let(:permissions) { [:view_work_packages] }
it 'is 404' do
expect(subject.status)

@ -73,14 +73,7 @@ describe WorkPackages::SetScheduleService do
let(:parent_follower1_due_date) { follower1_due_date }
let(:parent_following_work_package1) do
work_package = create_follower(parent_follower1_start_date,
parent_follower1_due_date,
{})
following_work_package1.parent = work_package
following_work_package1.save
work_package
create_parent(following_work_package1)
end
let(:follower_sibling_work_package) do
@ -109,6 +102,16 @@ describe WorkPackages::SetScheduleService do
work_package
end
def create_parent(child, start_date: child.start_date, due_date: child.due_date)
create(:work_package,
subject: "parent of #{child.subject}",
start_date:,
due_date:).tap do |parent|
child.parent = parent
child.save
end
end
def create_child(parent, start_date, due_date)
create(:work_package,
subject: "child of #{parent.subject}",
@ -836,4 +839,34 @@ describe WorkPackages::SetScheduleService do
end
end
end
context 'with deep hierarchy of work packages' do
before do
work_package.due_date = Time.zone.today - 5.days
end
def create_hierarchy(parent, nb_children_by_levels)
nb_children, *remaining_levels = nb_children_by_levels
children = create_list(:work_package, nb_children, parent:)
if remaining_levels.any?
children.each do |child|
create_hierarchy(child, remaining_levels)
end
end
end
it 'does not fail with a SystemStackError (regression #43894)' do
parent = create(:work_package, start_date: Date.current, due_date: Date.current)
hierarchy = [1, 1, 1, 1, 2, 4, 4, 4]
create_hierarchy(parent, hierarchy)
# The bug triggers when moving work package is in the middle of the
# hierarchy
work_package.parent = parent.children.first.children.first.children.first
work_package.save
expect { instance.call(attributes) }
.not_to raise_error
end
end
end

@ -69,7 +69,7 @@ describe BackupJob, type: :model do
allow(job).to receive(:arguments).and_return arguments
allow(job).to receive(:job_id).and_return job_id
expect(Open3).to receive(:capture3).and_return [nil, "Dump failed", db_dump_process_status]
allow(Open3).to receive(:capture3).and_return [nil, "Dump failed", db_dump_process_status]
allow_any_instance_of(BackupJob)
.to receive(:tmp_file_name).with("openproject", ".sql").and_return("/tmp/openproject.sql")
@ -85,6 +85,31 @@ describe BackupJob, type: :model do
job.perform **arguments.first
end
describe '#pg_env' do
subject { job.pg_env }
context 'when config has user reference, not username (regression #44251)' do
let(:config_double) do
{
adapter: :postgresql,
password: "blabla",
database: "test",
user: "foobar"
}
end
before do
allow(job).to receive(:database_config).and_return(config_double)
end
it 'still sets a PGUSER' do
expect(subject['PGUSER']).to eq 'foobar'
expect(subject['PGPASSWORD']).to eq 'blabla'
expect(subject['PGDATABASE']).to eq 'test'
end
end
end
context "with a failed database dump" do
let(:db_dump_success) { false }
@ -175,7 +200,7 @@ describe BackupJob, type: :model do
allow_any_instance_of(LocalFileUploader).to receive(:cached?).and_return(true)
allow_any_instance_of(LocalFileUploader)
.to receive(:local_file)
.and_return(File.new(dummy_path))
.and_return(File.new(dummy_path))
end
after do

Loading…
Cancel
Save