Skip to content

Commit

Permalink
Merge pull request #3304 from specify/issue-2292
Browse files Browse the repository at this point in the history
Scope all front-end requests where needed
  • Loading branch information
melton-jason authored Jan 26, 2024
2 parents d0352fc + 2b2cfaa commit 7324c28
Show file tree
Hide file tree
Showing 30 changed files with 238 additions and 97 deletions.
43 changes: 25 additions & 18 deletions specifyweb/frontend/js_src/lib/components/AppResources/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,32 @@ export function useAppResources(
React.useCallback(
async () =>
f.all({
directories: fetchCollection('SpAppResourceDir', { limit: 0 }).then<
AppResources['directories']
>(({ records }) =>
directories: fetchCollection('SpAppResourceDir', {
limit: 0,
domainFilter: false,
}).then<AppResources['directories']>(({ records }) =>
records.map((record) => ({ ...record, scope: getScope(record) }))
),
disciplines: fetchCollection('Discipline', { limit: 0 }).then(
({ records }) => records
),
collections: fetchCollection('Collection', { limit: 0 }).then(
({ records }) => records
),
users: fetchCollection('SpecifyUser', { limit: 0 }).then(
({ records }) => records
),
appResources: fetchCollection('SpAppResource', { limit: 0 }).then(
({ records }) => records
),
viewSets: fetchCollection('SpViewSetObj', { limit: 0 }).then(
({ records }) => records
),
disciplines: fetchCollection('Discipline', {
limit: 0,
domainFilter: false,
}).then(({ records }) => records),
collections: fetchCollection('Collection', {
limit: 0,
domainFilter: false,
}).then(({ records }) => records),
users: fetchCollection('SpecifyUser', {
limit: 0,
domainFilter: false,
}).then(({ records }) => records),
appResources: fetchCollection('SpAppResource', {
limit: 0,
domainFilter: false,
}).then(({ records }) => records),
viewSets: fetchCollection('SpViewSetObj', {
limit: 0,
domainFilter: false,
}).then(({ records }) => records),
}),
[]
),
Expand Down Expand Up @@ -162,6 +168,7 @@ export function useAppResourceData(
? await fetchCollection('SpAppResourceData', {
limit: 1,
[relationshipName]: resource.id,
domainFilter: false,
}).then(
({ records }) =>
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ function Attachments({
'Attachment',
{
limit: 1,
domainFilter: true,
},
allTablesWithAttachments().length === tablesWithAttachments().length
? {}
Expand All @@ -116,7 +117,7 @@ function Attachments({
).then<number>(({ totalCount }) => totalCount),
unused: fetchCollection(
'Attachment',
{ limit: 1 },
{ limit: 1, domainFilter: true },
backendFilter('tableId').isNull()
).then<number>(({ totalCount }) => totalCount),
byTable: f.all(
Expand All @@ -127,6 +128,7 @@ function Attachments({
limit: 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
tableID: tableId,
domainFilter: true,
}).then<number>(({ totalCount }) => totalCount),
])
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,48 @@ describe('fetchCollection', () => {
const baseCoRecord = {
resource_uri: getResourceApiUrl('CollectionObject', 1),
};
overrideAjax('/api/specify/collectionobject/?limit=1', {
overrideAjax('/api/specify/collectionobject/?limit=1&domainfilter=true', {
meta: {
total_count: 2,
},
objects: [baseCoRecord],
});

test('Simple collection objects query', async () =>
expect(fetchCollection('CollectionObject', { limit: 1 })).resolves.toEqual({
expect(
fetchCollection('CollectionObject', { limit: 1, domainFilter: true })
).resolves.toEqual({
records: [addMissingFields('CollectionObject', baseCoRecord)],
totalCount: 2,
}));

const baseInstitutionRecord = {
resource_uri: getResourceApiUrl('Institution', 1),
};
overrideAjax('/api/specify/institution/?limit=1', {
meta: {
total_count: 2,
},
objects: [baseInstitutionRecord],
});

test("If query can't be scoped, it won't be", async () =>
expect(
/*
* Deposit "domainFilter: true", false will be sent to back-end because
* this table can't be scoped
*/
fetchCollection('Institution', { limit: 1, domainFilter: true })
).resolves.toEqual({
records: [addMissingFields('Institution', baseInstitutionRecord)],
totalCount: 2,
}));

const baseLocalityRecord = {
resource_uri: getResourceApiUrl('Locality', 1),
};
overrideAjax(
'/api/specify/locality/?limit=1&localityname=Test&orderby=-latlongaccuracy&yesno1=True&domainfilter=false',
'/api/specify/locality/?limit=1&localityname=Test&orderby=-latlongaccuracy&yesno1=True',
{
meta: {
total_count: 2,
Expand Down Expand Up @@ -65,7 +89,7 @@ describe('fetchCollection', () => {
expect(
fetchCollection(
'Locality',
{ limit: 1 },
{ limit: 1, domainFilter: false },
{
...backendFilter('localityName').caseInsensitiveStartsWith('Test'),
...backendFilter('id').isIn([1, 2]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,19 +278,23 @@ describe('getAggregator', () => {

describe('getScopingRelationship', () => {
test('can get scoping relationship when scoped to Collection Object', () =>
expect(tables.Determination.getDirectScope()?.name).toBe(
expect(tables.Determination.getScopingRelationship()?.name).toBe(
'collectionObject'
));
test('can get scoping relationship when scoped to Collection', () =>
expect(tables.CollectionObject.getDirectScope()?.name).toBe('collection'));
expect(tables.CollectionObject.getScopingRelationship()?.name).toBe(
'collection'
));
test('can get scoping relationship when scoped to Discipline', () =>
expect(tables.CollectingEvent.getDirectScope()?.name).toBe('discipline'));
expect(tables.CollectingEvent.getScopingRelationship()?.name).toBe(
'discipline'
));
test('can get scoping relationship when scoped to Division', () =>
expect(tables.Discipline.getDirectScope()?.name).toBe('division'));
expect(tables.Discipline.getScopingRelationship()?.name).toBe('division'));
test('can get scoping relationship when scoped to Institution', () =>
expect(tables.Division.getDirectScope()?.name).toBe('institution'));
expect(tables.Division.getScopingRelationship()?.name).toBe('institution'));
test('returns undefined if table is not scoped', () =>
expect(tables.SpecifyUser.getDirectScope()).toBeUndefined());
expect(tables.SpecifyUser.getScopingRelationship()).toBeUndefined());
});

describe('getScopingPath', () => {
Expand Down
42 changes: 27 additions & 15 deletions specifyweb/frontend/js_src/lib/components/DataModel/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from './helperTypes';
import { parseResourceUrl } from './resource';
import { serializeResource } from './serializers';
import { genericTables } from './tables';
import { genericTables, tables } from './tables';
import type { Tables } from './types';

export type CollectionFetchFilters<SCHEMA extends AnySchema> = Partial<
Expand All @@ -25,7 +25,7 @@ export type CollectionFetchFilters<SCHEMA extends AnySchema> = Partial<
> & {
readonly limit: number;
readonly offset?: number;
readonly domainFilter?: boolean;
readonly domainFilter: boolean;
readonly orderBy?:
| keyof CommonFields
| keyof SCHEMA['fields']
Expand Down Expand Up @@ -76,20 +76,15 @@ export const fetchCollection = async <
Object.entries({
...filters,
...advancedFilters,
}).map(([key, value]) =>
value === undefined
}).map(([key, value]) => {
const mapped =
value === undefined
? undefined
: mapValue(key, value, tableName);
return mapped === undefined
? undefined
: [
key.toLowerCase(),
key === 'orderBy'
? value.toString().toLowerCase()
: typeof value === 'boolean' && key !== 'domainFilter'
? value
? 'True'
: 'False'
: value.toString(),
]
)
: ([key.toLowerCase(), mapped] as const);
})
)
)
)
Expand All @@ -101,6 +96,22 @@ export const fetchCollection = async <
totalCount: meta.total_count,
}));

function mapValue(
key: string,
value: unknown,
tableName: keyof Tables
): string | undefined {
if (key === 'orderBy') return (value as string).toString().toLowerCase();
else if (key === 'domainFilter') {
const scopingField = tables[tableName].getScope();
return value === true &&
(tableName === 'Attachment' || typeof scopingField === 'object')
? 'true'
: undefined;
} else if (typeof value === 'boolean') return value ? 'True' : 'False';
else return (value as string).toString();
}

/**
* Fetch a related collection via an relationship independent -to-many
* relationship
Expand Down Expand Up @@ -133,6 +144,7 @@ export async function fetchRelated<
const response = fetchCollection(relationship.relatedTable.name, {
limit,
[reverseName]: id,
domainFilter: false,
});
return response as Promise<{
readonly records: RA<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ export const LazyCollection = Base.extend({
this.filters = options.filters || {};
this.domainfilter =
Boolean(options.domainfilter) &&
(typeof this.model?.specifyTable !== 'object' ||
hasHierarchyField(this.model.specifyTable));
this.model?.specifyTable.getScopingRelationship() !== undefined;
},
url() {
return `/api/specify/${this.model.specifyTable.name.toLowerCase()}/`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function getScopingResource(
):
| { readonly relationship: Relationship; readonly resourceUrl: string }
| undefined {
const domainField = table.getDirectScope();
const domainField = table.getScopingRelationship();
if (domainField === undefined) return;

const domainFieldName =
Expand Down Expand Up @@ -126,7 +126,7 @@ export function getCollectionForResource(
const collectionUrl = resource.get('collectionMemberId') as number | null;
if (typeof collectionUrl === 'number') return collectionUrl;

const domainField = resource.specifyTable.getDirectScope();
const domainField = resource.specifyTable.getScopingRelationship();
if (domainField === undefined) return undefined;

const domainResourceId = idFromUrl(resource.get(domainField.name) ?? '');
Expand All @@ -143,7 +143,7 @@ export function getCollectionForResource(
export async function fetchCollectionsForResource(
resource: SpecifyResource<AnySchema>
): Promise<RA<number> | undefined> {
const domainField = resource.specifyTable.getDirectScope();
const domainField = resource.specifyTable.getScopingRelationship();
if (domainField === undefined) return undefined;
const domainResource = await (
resource as SpecifyResource<CollectionObject>
Expand All @@ -162,7 +162,7 @@ export async function fetchCollectionsForResource(
? undefined
: fetchCollection(
'Collection',
{ limit: 0 },
{ limit: 0, domainFilter: false },
{
[fieldsBetween]: domainResource.id.toString(),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ export class SpecifyTable<SCHEMA extends AnySchema = AnySchema> {
return this.localization.aggregator ?? undefined;
}

// eslint-disable-next-line functional/prefer-readonly-type
private scopingRelationship: Relationship | false | undefined;

/**
* Returns the relationship field of this table that places it in
* the collection -> discipline -> division -> institution scoping
Expand All @@ -438,13 +441,17 @@ export class SpecifyTable<SCHEMA extends AnySchema = AnySchema> {
* (which is not a relationship - sad). Back-end looks at that relationship
* for scoping inconsistenly. Front-end does not look at all.
*/
public getDirectScope(): Relationship | undefined {
return schema.orgHierarchy
.map((fieldName) => this.getField(fieldName))
.find(
(field): field is Relationship =>
field?.isRelationship === true && !relationshipIsToMany(field)
);
public getScopingRelationship(): Relationship | undefined {
this.scopingRelationship ??=
schema.orgHierarchy
.map((fieldName) => this.getField(fieldName))
.find(
(field): field is Relationship =>
field?.isRelationship === true && !relationshipIsToMany(field)
) ?? false;
return this.scopingRelationship === false
? undefined
: this.scopingRelationship;
}

/**
Expand All @@ -468,7 +475,7 @@ export class SpecifyTable<SCHEMA extends AnySchema = AnySchema> {
* That is not possible at the moment as it's assumed that scoping is only
* enforced though Relationships
*/
const direct = this.getDirectScope();
const direct = this.getScopingRelationship();
if (typeof direct === 'object') {
/*
* We don't care about scoping to Institution since there is only ever
Expand Down Expand Up @@ -540,7 +547,7 @@ export class SpecifyTable<SCHEMA extends AnySchema = AnySchema> {
*/
public getScopingPath(): RA<string> | undefined {
if (this.name === schema.orgHierarchy.at(-1)) return [];
const up = this.getDirectScope();
const up = this.getScopingRelationship();
return up === undefined
? undefined
: [...defined(up.relatedTable.getScopingPath()), up.name.toLowerCase()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,29 @@ export function ShowLoansCommand({
isResolved: false,
limit: DEFAULT_FETCH_LIMIT,
preparation: preparation.get('id'),
domainFilter: false,
}).then(({ records }) => records.map(deserializeResource))
: undefined,
resolvedLoans: hasTablePermission('LoanPreparation', 'read')
? fetchCollection('LoanPreparation', {
isResolved: true,
limit: DEFAULT_FETCH_LIMIT,
preparation: preparation.get('id'),
domainFilter: false,
}).then(({ records }) => records.map(deserializeResource))
: undefined,
gifts: hasTablePermission('GiftPreparation', 'read')
? fetchCollection('GiftPreparation', {
limit: DEFAULT_FETCH_LIMIT,
preparation: preparation.get('id'),
domainFilter: false,
}).then(({ records }) => records.map(deserializeResource))
: undefined,
exchanges: hasTablePermission('ExchangeOutPrep', 'read')
? fetchCollection('ExchangeOutPrep', {
limit: DEFAULT_FETCH_LIMIT,
preparation: preparation.get('id'),
domainFilter: false,
}).then(({ records }) => records.map(deserializeResource))
: undefined,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function HostTaxon({
fetchCollection('CollectionRelType', {
limit: 1,
name: relationship,
domainFilter: false,
})
.then(async ({ records }) =>
f
Expand Down
Loading

0 comments on commit 7324c28

Please sign in to comment.