Skip to content

Commit

Permalink
Merge branch 'main' into xsalonx/QCF/O2B-1216/delete-admin-only-for-a…
Browse files Browse the repository at this point in the history
…dmins
  • Loading branch information
xsalonx authored Apr 19, 2024
2 parents b299fd5 + f7b01b1 commit 0265413
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 36 deletions.
6 changes: 4 additions & 2 deletions lib/domain/dtos/filters/RunFilterDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
const Joi = require('joi');
const { CustomJoi } = require('../CustomJoi.js');
const { RUN_DEFINITIONS } = require('../../../server/services/run/getRunDefinition.js');
const { TagsFilterDto } = require('./TagsFilterDto.js');
const { FromToFilterDto } = require('./FromToFilterDto.js');
const { RUN_QUALITIES } = require('../../enums/RunQualities.js');
const { NumericalComparisonDto } = require('./NumericalComparisonDto.js');
Expand All @@ -35,7 +34,10 @@ exports.RunFilterDto = Joi.object({
calibrationStatuses: Joi.array().items(...RUN_CALIBRATION_STATUS),
definitions: CustomJoi.stringArray().items(Joi.string().uppercase().trim().valid(...RUN_DEFINITIONS)),
eorReason: EorReasonFilterDto,
tags: TagsFilterDto,
tags: Joi.object({
values: CustomJoi.stringArray().items(Joi.string()).single().required(),
operation: Joi.string().valid('and', 'or', 'none-of').required(),
}),
fillNumbers: Joi.string().trim(),
o2start: FromToFilterDto,
o2end: FromToFilterDto,
Expand Down
10 changes: 7 additions & 3 deletions lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* or submit itself to any jurisdiction.
*/
import { Observable } from '/js/src/index.js';
import { CombinationOperatorChoiceModel, NoneCombinationOperator } from '../common/CombinationOperatorChoiceModel.js';
import { CombinationOperator, CombinationOperatorChoiceModel } from '../common/CombinationOperatorChoiceModel.js';
import { DetectorSelectionDropdownModel } from '../../detector/DetectorSelectionDropdownModel.js';

/**
Expand All @@ -26,7 +26,11 @@ export class DetectorsFilterModel extends Observable {
this._dropdownModel = new DetectorSelectionDropdownModel();
this._dropdownModel.bubbleTo(this);

this._combinationOperatorModel = new CombinationOperatorChoiceModel(true);
this._combinationOperatorModel = new CombinationOperatorChoiceModel([
CombinationOperator.AND,
CombinationOperator.OR,
CombinationOperator.NONE,
]);
this._combinationOperatorModel.bubbleTo(this);
}

Expand All @@ -45,7 +49,7 @@ export class DetectorsFilterModel extends Observable {
* @return {boolean} true if the current combination operator is none
*/
isNone() {
return this.combinationOperator === NoneCombinationOperator.value;
return this.combinationOperator === CombinationOperator.NONE.value;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,11 @@ import { SelectionModel } from '../../common/selection/SelectionModel.js';
export const CombinationOperator = {
OR: { label: 'OR', value: 'or' },
AND: { label: 'AND', value: 'and' },
NONE: { label: 'NONE', value: 'none' },
NONE_OF: { label: 'NONE-OF', value: 'none-of' },
};

export const NoneCombinationOperator = { label: 'NONE', value: 'none' };

/**
* Returns the list of available combination operator
*
* @param {boolean} allowNone if true, a "NONE" combination operator will be added
* @return {SelectionOption[]} the available options
*/
const getAvailableCombinations = (allowNone) => {
const ret = [...Object.values(CombinationOperator)];
if (allowNone) {
ret.push(NoneCombinationOperator);
}
return ret;
};
const DEFAULT_OPERATORS = [CombinationOperator.AND, CombinationOperator.OR];

/**
* Model storing the state of a combination operator choice
Expand All @@ -45,12 +33,12 @@ export class CombinationOperatorChoiceModel extends SelectionModel {
/**
* Constructor
*
* @param {boolean} allowNone if true, a "NONE" option will be added to the available combination options
* @param {SelectionOption[]} [operators] the list of possible operators
*/
constructor(allowNone = false) {
constructor(operators) {
super({
availableOptions: getAvailableCombinations(allowNone),
defaultSelection: [CombinationOperator.AND],
availableOptions: operators?.length ? operators : DEFAULT_OPERATORS,
defaultSelection: operators?.length ? operators.slice(0, 1) : [CombinationOperator.AND],
multiple: false,
allowEmpty: false,
});
Expand Down
5 changes: 3 additions & 2 deletions lib/public/components/Filters/common/TagFilterModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ export class TagFilterModel extends Observable {
/**
* Constructor
*
* @param {SelectionOption} [operators] optionally the list of available operators for the filter
* @constructor
*/
constructor() {
constructor(operators) {
super();
this._selectionModel = new TagSelectionDropdownModel({ includeArchived: true });
this._selectionModel.bubbleTo(this);

this._combinationOperatorModel = new CombinationOperatorChoiceModel();
this._combinationOperatorModel = new CombinationOperatorChoiceModel(operators);
this._combinationOperatorModel.bubbleTo(this);
}

Expand Down
9 changes: 7 additions & 2 deletions lib/public/views/Runs/Overview/RunsOverviewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/Eor
import pick from '../../../utilities/pick.js';
import { OverviewPageModel } from '../../../models/OverviewModel.js';
import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js';
import { CombinationOperator } from '../../../components/Filters/common/CombinationOperatorChoiceModel.js';

/**
* Model representing handlers for runs page
Expand All @@ -35,7 +36,11 @@ export class RunsOverviewModel extends OverviewPageModel {
constructor(model) {
super();

this._listingTagsFilterModel = new TagFilterModel();
this._listingTagsFilterModel = new TagFilterModel([
CombinationOperator.AND,
CombinationOperator.OR,
CombinationOperator.NONE_OF,
]);
this._listingTagsFilterModel.observe(() => this._applyFilters(true));
this._listingTagsFilterModel.visualChange$.bubbleTo(this);

Expand Down Expand Up @@ -96,7 +101,7 @@ export class RunsOverviewModel extends OverviewPageModel {
return [key, formatExport(value, selectedRun)];
});
return Object.fromEntries(formattedEntries);
}),
});
this.getSelectedExportType() === 'CSV'
? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;')
: createJSONExport(runs, `${fileName}.json`, 'application/json');
Expand Down
15 changes: 13 additions & 2 deletions lib/usecases/run/GetAllRunsUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ class GetAllRunsUseCase {
filteringQueryBuilder.where('runNumber').oneOf(...runNumbers);
}

if (tags && tags.values.length > 0) {
if (tags?.values?.length) {
if (tags.operation === 'and') {
const runsWithExpectedTags = await RunRepository.findAll({
attributes: ['id'],
Expand All @@ -275,13 +275,24 @@ class GetAllRunsUseCase {
raw: true,
});
filteringQueryBuilder.where('id').oneOf(...runsWithExpectedTags.map(({ id }) => id));
} else {
} else if (tags.operation === 'or') {
filteringQueryBuilder.include({
association: 'tags',
where: {
text: { [Op.in]: tags.values },
},
});
} else {
const runsWithExpectedTags = await RunRepository.findAll({
attributes: ['id'],
include: {
association: 'tags',
where: { text: { [Op.in]: tags.values } },
},
group: 'run_number',
raw: true,
});
filteringQueryBuilder.where('id').not().oneOf(...runsWithExpectedTags.map(({ id }) => id));
}
}
}
Expand Down
43 changes: 41 additions & 2 deletions test/api/runs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,45 @@ module.exports = () => {
expect(data).to.have.lengthOf(8);
});

it('should successfully filter on tags', async () => {
{
const response = await request(server).get('/api/runs?filter[tags][operation]=and&filter[tags][values]=FOOD,RUN');

expect(response.status).to.equal(200);

const { data } = response.body;
expect(data).to.be.an('array');
expect(data).to.have.lengthOf(1);
expect(data.map(({ runNumber }) => runNumber)).to.eql([106]);
}

{
const response = await request(server).get('/api/runs?filter[tags][operation]=or&filter[tags][values]=FOOD,TEST-TAG-41');

expect(response.status).to.equal(200);

const { data } = response.body;
expect(data).to.be.an('array');
expect(data).to.have.lengthOf(2);
expect(data.map(({ runNumber }) => runNumber)).to.eql([106, 2]);
}

{
const response = await request(server)
.get('/api/runs?filter[tags][operation]=none-of&filter[tags][values]=FOOD,TEST-TAG-41');

expect(response.status).to.equal(200);

const { data: runs } = response.body;
expect(runs).to.be.an('array');
expect(runs).to.have.lengthOf(100);

for (const run of runs) {
expect(run.tags.every(({ text }) => text !== 'FOOD' && text !== 'TEST-TAG-41')).to.be.true;
}
}
});

it('should successfully return 400 if the given definitions are not valid', async () => {
const response = await request(server).get('/api/runs?filter[definitions]=bad,definition');
expect(response.status).to.equal(400);
Expand All @@ -219,7 +258,7 @@ module.exports = () => {
expect(data.every(({ definition }) => definition === RunDefinition.Physics)).to.be.true;
});

it ('should succefully filter on data pass id', async () => {
it('should succefully filter on data pass id', async () => {
const response = await request(server).get('/api/runs?filter[dataPassIds][]=2&filter[dataPassIds][]=3');
expect(response.status).to.equal(200);

Expand All @@ -228,7 +267,7 @@ module.exports = () => {
expect(data.map(({ runNumber }) => runNumber)).to.have.all.members([1, 2, 55, 49, 54, 56, 105]);
});

it ('should succefully filter on simulation pass id', async () => {
it('should succefully filter on simulation pass id', async () => {
const response = await request(server).get('/api/runs?filter[simulationPassIds][]=1');
expect(response.status).to.equal(200);

Expand Down
21 changes: 18 additions & 3 deletions test/lib/usecases/run/GetAllRunsUseCase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ module.exports = () => {
}
});

it('should successfully return an array only containing runs found with tags', async () => {
it('should successfully filter on tags', async () => {
{
getAllRunsDto.query = {
filter: {
Expand All @@ -133,8 +133,23 @@ module.exports = () => {
const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto);
expect(runs).to.lengthOf(2);
for (const run of runs) {
const tagTexts = run.tags.map(({ text }) => text);
expect(tagTexts.includes('FOOD') || tagTexts.includes('TEST-TAG-41')).to.be.true;
expect(run.tags.some(({ text }) => text.includes('FOOD') || text.includes('TEST-TAG-41'))).to.be.true;
}

{
getAllRunsDto.query = {
filter: {
tags: { operation: 'none-of', values: ['FOOD', 'TEST-TAG-41'] },
},
page: {
limit: 200,
},
};
const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto);
expect(runs).to.lengthOf(106);
for (const run of runs) {
expect(run.tags.every(({ text }) => text !== 'FOOD' && text !== 'TEST-TAG-41')).to.be.true;
}
}
}
});
Expand Down
32 changes: 31 additions & 1 deletion test/public/runs/overview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,35 @@ module.exports = () => {
expect(table.length).to.equal(2);
});

it('should successfully filter on tags', async () => {
await pressElement(page, '#reset-filters');

// Open filter toggle
await page.waitForSelector('.tags-filter .dropdown-trigger');
await page.$eval('.tags-filter .dropdown-trigger', (element) => element.click());
await pressElement(page, '#tag-dropdown-option-FOOD');
await pressElement(page, '#tag-dropdown-option-RUN');
await waitForTimeout(300);

table = await page.$$('tbody tr');
expect(table.length).to.equal(1);

await page.$eval('#tag-filter-combination-operator-radio-button-or', (element) => element.click());
await page.$eval('.tags-filter .dropdown-trigger', (element) => element.click());
await pressElement(page, '#tag-dropdown-option-RUN');
await pressElement(page, '#tag-dropdown-option-TEST-TAG-41');
await page.waitForSelector('tbody tr:nth-child(2)', { timeout: 500 });

table = await page.$$('tbody tr');
expect(table.length).to.equal(2);

await page.$eval('#tag-filter-combination-operator-radio-button-none-of', (element) => element.click());
await page.waitForSelector('tbody tr:nth-child(2)', { hidden: true, timeout: 500 });

// Multiple pages, not very representative
expectInnerText('#totalRowsCount', '108');
});

it('should successfully filter on definition', async () => {
await goToPage(page, 'run-overview');
const filterInputSelectorPrefix = '#runDefinitionCheckbox';
Expand Down Expand Up @@ -1192,6 +1221,7 @@ module.exports = () => {
`${popoverSelector} a:nth-child(3)`,
({ href }) => href,
// eslint-disable-next-line max-len
)).to.equal('http://localhost:8082/?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS');
)).to.equal('http://localhost:8082/' +
'?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS');
});
};

0 comments on commit 0265413

Please sign in to comment.