Skip to content

Commit a062d6f

Browse files
Merge branch 'production' into issue-922
2 parents daf224e + 7873a51 commit a062d6f

File tree

27 files changed

+348
-176
lines changed

27 files changed

+348
-176
lines changed

specifyweb/context/views.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def set_users_collections_for_sp6(cursor, user, collectionids):
7777
cursor.execute("delete specifyuser_spprincipal "
7878
"from specifyuser_spprincipal "
7979
"join spprincipal using (spprincipalid) "
80-
"where specifyuserid = %s and usergroupscopeid not in %s",
80+
"where specifyuserid = %s and usergroupscopeid not in %s"
81+
"and spprincipal.Name != 'Administrator'",
8182
[user.id, collectionids])
8283

8384
# Next delete the joins from the principals to any permissions.

specifyweb/export/feed.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,21 @@ def update_feed(force=False, notify_user=None):
4242
if force or needs_update(path, int(item_node.attrib['days'])):
4343
logger.info('Generating: %s', filename)
4444
temp_file = os.path.join(FEED_DIR, '%s.tmp.zip' % filename)
45-
collection = Collection.objects.get(id=item_node.attrib['collectionId'])
46-
user = Specifyuser.objects.get(id=item_node.attrib['userId'])
45+
collection_id = item_node.attrib.get('collectionid', item_node.attrib.get('collectionId'))
46+
collection = Collection.objects.get(id=collection_id)
47+
user_id = item_node.attrib.get('userid', item_node.attrib.get('userId'))
48+
user = Specifyuser.objects.get(id=user_id)
4749
dwca_def, _, __ = get_app_resource(collection, user, item_node.attrib['definition'])
4850
eml, _, __ = get_app_resource(collection, user, item_node.attrib['metadata'])
4951
make_dwca(collection, user, dwca_def, temp_file, eml=eml)
5052
os.rename(temp_file, path)
5153

5254
logger.info('Finished updating: %s', filename)
55+
notify_user_id = item_node.attrib.get('notifyuserid', item_node.attrib.get('notifyUserId'))
5356
if notify_user is not None:
5457
create_notification(notify_user, filename)
55-
elif 'notifyUserId' in item_node.attrib:
56-
user = Specifyuser.objects.get(id=item_node.attrib['notifyUserId'])
58+
elif notify_user_id:
59+
user = Specifyuser.objects.get(id=notify_user_id)
5760
create_notification(user, filename)
5861

5962
else:

specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
SpAppResourceDir,
2020
SpViewSetObj,
2121
} from '../DataModel/types';
22+
import { RssExportFeedEditor } from '../ExportFeed';
2223
import { exportFeedSpec } from '../ExportFeed/spec';
2324
import { DataObjectFormatter } from '../Formatters';
2425
import { formattersSpec } from '../Formatters/spec';
@@ -158,12 +159,7 @@ export const visualAppResourceEditors = f.store<
158159
},
159160
leafletLayers: undefined,
160161
rssExportFeed: {
161-
/**
162-
** Disabled in https://github.com/specify/specify7/issues/4653
163-
* needs to be enabled when fixing https://github.com/specify/specify7/issues/4650
164-
* **
165-
*/
166-
// Visual: RssExportFeedEditor,
162+
visual: RssExportFeedEditor,
167163
xml: generateXmlEditor(exportFeedSpec),
168164
},
169165
expressSearchConfig: undefined,

specifyweb/frontend/js_src/lib/components/DataModel/__tests__/collection.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { overrideAjax } from '../../../tests/ajax';
22
import { requireContext } from '../../../tests/helpers';
3+
import { theories } from '../../../tests/utils';
34
import { addMissingFields } from '../addMissingFields';
4-
import { fetchCollection, fetchRelated } from '../collection';
5+
import { exportsForTests, fetchCollection, fetchRelated } from '../collection';
56
import { backendFilter } from '../helpers';
67
import { getResourceApiUrl } from '../resource';
78

89
requireContext();
910

11+
const { mapQueryParameter, buildUrl } = exportsForTests;
12+
1013
describe('fetchCollection', () => {
1114
const baseCoRecord = {
1215
resource_uri: getResourceApiUrl('CollectionObject', 1),
@@ -125,3 +128,41 @@ describe('fetchRelated', () => {
125128
totalCount: 2,
126129
}));
127130
});
131+
132+
theories(mapQueryParameter, [
133+
[['orderBy', 'A', 'CollectionObject'], 'a'],
134+
[['domainFilter', true, 'CollectionObject'], 'true'],
135+
[['domainFilter', false, 'CollectionObject'], undefined],
136+
// Not scoped
137+
[['domainFilter', true, 'AddressOfRecord'], undefined],
138+
[['domainFilter', false, 'AddressOfRecord'], undefined],
139+
// Attachment is an exception
140+
[['domainFilter', true, 'Attachment'], 'true'],
141+
[['domainFilter', false, 'Attachment'], undefined],
142+
[['distinct', true, 'CollectionObject'], 'true'],
143+
[['distinct', false, 'CollectionObject'], undefined],
144+
[['test', true, 'CollectionObject'], 'True'],
145+
[['test', false, 'CollectionObject'], 'False'],
146+
[['test', 2, 'CollectionObject'], '2'],
147+
[['test', 'a', 'CollectionObject'], 'a'],
148+
]);
149+
150+
theories(buildUrl, [
151+
[
152+
[
153+
`/api/specify_rows/Agent/`,
154+
'Agent',
155+
{
156+
domainFilter: true,
157+
limit: 1,
158+
offset: 4,
159+
orderBy: '-id',
160+
firstName: 'Test',
161+
},
162+
{
163+
createdByAgent__lastName: 'Test',
164+
},
165+
],
166+
'/api/specify_rows/Agent/?domainfilter=true&limit=1&offset=4&orderby=-id&firstname=Test&createdbyagent__lastname=Test',
167+
],
168+
]);

specifyweb/frontend/js_src/lib/components/DataModel/collection.ts

+51-48
Original file line numberDiff line numberDiff line change
@@ -68,49 +68,63 @@ export const fetchCollection = async <
6868
};
6969
readonly objects: RA<SerializedRecord<SCHEMA>>;
7070
}>(
71-
formatUrl(
71+
buildUrl(
7272
`/api/specify/${tableName.toLowerCase()}/`,
73-
Object.fromEntries(
74-
filterArray(
75-
Array.from(
76-
Object.entries({
77-
...filters,
78-
...advancedFilters,
79-
}).map(([key, value]) => {
80-
const mapped =
81-
value === undefined
82-
? undefined
83-
: mapValue(key, value, tableName);
84-
return mapped === undefined
85-
? undefined
86-
: ([key.toLowerCase(), mapped] as const);
87-
})
88-
)
89-
)
90-
)
73+
tableName,
74+
filters,
75+
advancedFilters
9176
),
92-
9377
{ headers: { Accept: 'application/json' } }
9478
).then(({ data: { meta, objects } }) => ({
9579
records: objects.map(serializeResource),
9680
totalCount: meta.total_count,
9781
}));
9882

99-
function mapValue(
83+
const buildUrl = <
84+
TABLE_NAME extends keyof Tables,
85+
SCHEMA extends Tables[TABLE_NAME]
86+
>(
87+
baseUrl: string,
88+
tableName: TABLE_NAME,
89+
filters: CollectionFetchFilters<SCHEMA>,
90+
advancedFilters: IR<boolean | number | string> = {}
91+
): string =>
92+
formatUrl(
93+
baseUrl,
94+
Object.fromEntries(
95+
filterArray(
96+
Object.entries({
97+
...filters,
98+
...advancedFilters,
99+
}).map(([key, value]) => {
100+
const mapped =
101+
value === undefined
102+
? undefined
103+
: mapQueryParameter(key, value, tableName);
104+
return mapped === undefined
105+
? undefined
106+
: ([key.toLowerCase(), mapped] as const);
107+
})
108+
)
109+
)
110+
);
111+
112+
function mapQueryParameter(
100113
key: string,
101114
value: unknown,
102115
tableName: keyof Tables
103116
): string | undefined {
104-
if (key === 'orderBy') return (value as string).toString().toLowerCase();
117+
if (key === 'orderBy') return String(value).toLowerCase();
105118
else if (key === 'domainFilter') {
106119
// GetScope() returns undefined for tables with only collectionmemberid.
107120
const scopingField = tables[tableName].getScope();
108121
return value === true &&
109122
(tableName === 'Attachment' || typeof scopingField === 'object')
110123
? 'true'
111124
: undefined;
112-
} else if (typeof value === 'boolean') return value ? 'True' : 'False';
113-
else return (value as string).toString();
125+
} else if (key === 'distinct') return value === true ? 'true' : undefined;
126+
else if (typeof value === 'boolean') return value ? 'True' : 'False';
127+
else return String(value);
114128
}
115129

116130
/**
@@ -189,7 +203,6 @@ export const fetchRows = async <
189203
}: Omit<CollectionFetchFilters<SCHEMA>, 'fields'> & {
190204
readonly fields: FIELDS;
191205
readonly distinct?: boolean;
192-
readonly limit?: number;
193206
},
194207
/**
195208
* Advanced filters, not type-safe.
@@ -201,31 +214,16 @@ export const fetchRows = async <
201214
advancedFilters: IR<number | string> = {}
202215
): Promise<RA<FieldsToTypes<FIELDS>>> => {
203216
const { data } = await ajax<RA<RA<boolean | number | string | null>>>(
204-
formatUrl(
217+
buildUrl(
205218
`/api/specify_rows/${tableName.toLowerCase()}/`,
206-
Object.fromEntries(
207-
filterArray(
208-
Array.from(
209-
Object.entries({
210-
...filters,
211-
...advancedFilters,
212-
...(distinct ? { distinct: 'true' } : {}),
213-
fields: Object.keys(fields).join(',').toLowerCase(),
214-
}).map(([key, value]) =>
215-
value === undefined
216-
? undefined
217-
: [
218-
key.toLowerCase(),
219-
key === 'orderBy'
220-
? value.toString().toLowerCase()
221-
: value.toString(),
222-
]
223-
)
224-
)
225-
)
226-
)
219+
tableName,
220+
filters as CollectionFetchFilters<SCHEMA>,
221+
{
222+
...advancedFilters,
223+
distinct,
224+
fields: Object.keys(fields).join(',').toLowerCase(),
225+
}
227226
),
228-
229227
{ headers: { Accept: 'application/json' } }
230228
);
231229
const keys = Object.keys(fields);
@@ -236,3 +234,8 @@ export const fetchRows = async <
236234
) as FieldsToTypes<FIELDS>
237235
);
238236
};
237+
238+
export const exportsForTests = {
239+
buildUrl,
240+
mapQueryParameter,
241+
};

specifyweb/frontend/js_src/lib/components/ExportFeed/Dwca.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ export function PickAppResource({
109109
filters={filters}
110110
isEmbedded
111111
resources={resources}
112-
onOpen={(selected): void =>
113-
f.maybe(toResource(selected, 'SpAppResource'), handleSelected)
114-
}
112+
onOpen={(selected): void => {
113+
f.maybe(toResource(selected, 'SpAppResource'), handleSelected);
114+
handleClose();
115+
}}
115116
/>
116117
</ReadOnlyContext.Provider>
117118
</Dialog>

specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { LocalizedString } from 'typesafe-i18n';
2+
13
import { f } from '../../utils/functools';
24
import type { IR, RA, RR } from '../../utils/types';
35
import { filterArray, localized } from '../../utils/types';
@@ -6,6 +8,7 @@ import type { LiteralField, Relationship } from '../DataModel/specifyField';
68
import type { SpecifyTable } from '../DataModel/specifyTable';
79
import { genericTables } from '../DataModel/tables';
810
import type { Tables } from '../DataModel/types';
11+
import type { FormCondition } from '../FormParse';
912
import { paleoPluginTables } from '../FormPlugins/PaleoLocation';
1013
import { toLargeSortConfig, toSmallSortConfig } from '../Molecules/Sorting';
1114
import type { SpecToJson, Syncer } from '../Syncer';
@@ -131,25 +134,29 @@ const rowsSpec = (table: SpecifyTable | undefined) =>
131134
),
132135
condition: pipe(
133136
syncers.xmlAttribute('condition', 'skip', false),
134-
syncer(
137+
syncer<LocalizedString | undefined, FormCondition>(
135138
(rawCondition) => {
136139
if (rawCondition === undefined) return undefined;
140+
const isAlways = rawCondition.trim() === 'always';
141+
if (isAlways) return { type: 'Always' } as const;
137142
const [rawField, ...condition] = rawCondition.split('=');
138143
const field = syncers.field(table?.name).serializer(rawField);
139144
return field === undefined
140145
? undefined
141-
: {
146+
: ({
147+
type: 'Value',
142148
field,
143-
condition: condition.join('='),
144-
};
149+
value: condition.join('='),
150+
} as const);
145151
},
146152
(props) => {
147153
if (props === undefined) return undefined;
148-
const { field, condition } = props;
154+
if (props.type === 'Always') return localized('always');
155+
const { field, value } = props;
149156
const joined = syncers.field(table?.name).deserializer(field);
150157
return joined === undefined || joined.length === 0
151158
? undefined
152-
: localized(`${joined}=${condition}`);
159+
: localized(`${joined}=${value}`);
153160
}
154161
)
155162
),

specifyweb/frontend/js_src/lib/components/FormParse/index.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -457,18 +457,19 @@ function parseFormTableColumns(
457457
}).fill(undefined),
458458
];
459459
}
460+
export type FormCondition =
461+
| State<
462+
'Value',
463+
{
464+
readonly field: RA<LiteralField | Relationship>;
465+
readonly value: string;
466+
}
467+
>
468+
| State<'Always'>
469+
| undefined;
460470

461471
export type ConditionalFormDefinition = RA<{
462-
readonly condition:
463-
| State<
464-
'Value',
465-
{
466-
readonly field: RA<LiteralField | Relationship>;
467-
readonly value: string;
468-
}
469-
>
470-
| State<'Always'>
471-
| undefined;
472+
readonly condition: FormCondition;
472473
readonly definition: ParsedFormDefinition;
473474
}>;
474475

0 commit comments

Comments
 (0)