diff --git a/specifyweb/backend/context/tests/test_app_resource.py b/specifyweb/backend/context/tests/test_app_resource.py index 900576db564..4098b325afe 100644 --- a/specifyweb/backend/context/tests/test_app_resource.py +++ b/specifyweb/backend/context/tests/test_app_resource.py @@ -1,6 +1,14 @@ from django.test import Client from specifyweb.specify.tests.test_api import ApiTests +from specifyweb.specify.models import ( + Agent, + Spappresourcedata, + Spappresourcedir, + Spappresource, + Specifyuser, +) +from specifyweb.backend.permissions.models import UserPolicy from unittest.mock import Mock, patch from specifyweb.backend.context.app_resource import get_data_view_queries_resource @@ -90,3 +98,50 @@ def test_not_found_quiet(self, get_app_resource: Mock): response = c.get("/context/app.resource?name='simple'&quiet='true'") self._assertStatusCodeEqual(response, 204) + + def test_collection_preferences_cannot_be_modified_without_permission(self): + directory = Spappresourcedir.objects.create( + collection=self.collection, + ispersonal=False, + ) + resource = Spappresource.objects.create( + spappresourcedir=directory, + specifyuser=self.specifyuser, + level=0, + name="CollectionPreferences", + mimetype="application/json", + ) + Spappresourcedata.objects.create( + spappresource=resource, + data=b'{"general":{}}', + ) + + user = Specifyuser.objects.create( + isloggedin=False, + isloggedinreport=False, + name="no-permission-user", + password="test", + ) + UserPolicy.objects.create( + collection=self.collection, + specifyuser=user, + resource="/system/sp7/collection", + action="access", + ) + Agent.objects.create( + agenttype=0, + firstname="No", + lastname="Permission", + division=self.division, + specifyuser=user, + ) + + c = Client() + c.force_login(user) + response = c.put( + f"/api/specify/spappresource/{resource.id}/", + data={"version": resource.version, "name": "Changed"}, + content_type="application/json", + ) + + self._assertStatusCodeEqual(response, 403) diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/UploadAttachment.test.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/UploadAttachment.test.tsx index 25267303627..85e2f35a4b4 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/UploadAttachment.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/UploadAttachment.test.tsx @@ -13,6 +13,7 @@ import { deserializeResource } from '../../DataModel/serializers'; import type { Attachment } from '../../DataModel/types'; import { overrideAttachmentSettings } from '../attachments'; import * as Attachments from '../attachments'; +import { collectionPreferences } from '../../Preferences/collectionPreferences'; import { UploadAttachment } from '../Plugin'; import { testAttachment } from './utils'; @@ -26,6 +27,17 @@ beforeEach(() => { clearIdStore(); }); +let originalPreferences: ReturnType; + +beforeEach(() => { + originalPreferences = collectionPreferences.getRaw(); +}); + +afterEach(() => { + jest.restoreAllMocks(); + collectionPreferences.setRaw(originalPreferences); +}); + describe('UploadAttachment', () => { const testToken = 'testToken'; const testAttachmentLocation = 'testLocation'; @@ -61,4 +73,39 @@ describe('UploadAttachment', () => { expect(handleUploaded).toHaveBeenCalled(); }); }); + + test.each([false, true])( + 'uses the collection public attachment preference (%s)', + async (isPublic) => { + collectionPreferences.setRaw({ + general: { + attachments: { 'attachment.is_public_default': isPublic }, + }, + }); + const uploadFile = jest + .spyOn(Attachments, 'uploadFile') + .mockImplementation(uploadFileMock); + const handleUploaded = jest.fn(); + + overrideAttachmentSettings(attachmentSettings); + const { container, user } = mount( + + + + ); + + const input = Array.from(container.getElementsByTagName('input'))[0]; + const testFile = new File(['Some Text Contents'], 'testName', { + type: 'text/plain', + }); + + await user.upload(input, testFile); + + await waitFor(() => { + expect(uploadFile).toHaveBeenCalledWith( + expect.objectContaining({ attachmentIsPublicDefault: isPublic }) + ); + }); + } + ); }); diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts index 7439350e268..2a55b6f54f3 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts @@ -115,6 +115,18 @@ describe('uploadFile', () => { expect(attachment?.get('title')).toBe(testFileName); }); + test.each([false, true])( + 'preserves the public attachment default (%s)', + async (isPublic) => { + const attachment = await uploadFile({ + file: testFile, + attachmentIsPublicDefault: isPublic, + }); + + expect(attachment?.get('isPublic')).toBe(isPublic); + } + ); + test('sends request correctly (override spec)', async () => { const newToken = 'newTestToken'; const newLocation = 'newTestLocation'; diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts index 60db6689519..7de17d881b7 100644 --- a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts @@ -4,12 +4,14 @@ import type { RA } from '../../../utils/types'; import { localized } from '../../../utils/types'; import { formatNumber } from '../../Atoms/Internationalization'; import { + applyRefreshLayout, applyStatBackendResponse, generateStatUrl, getBackendUrlToFetch, getDefaultLayoutFlagged, getOffsetOne, } from '../hooks'; +import { statsSpec } from '../StatsSpec'; import type { StatLayout } from '../types'; import { defaultLayoutTest, statsSpecTest } from './layout.tests'; @@ -297,3 +299,85 @@ theories(generateStatUrl, [ out: '/stats/collection/user/preparations/', }, ]); + +test('preparation totals follow the collection preference', () => { + const spec = + statsSpec.collection.categories.preparations.items.phantomItem.spec; + + if (spec.type !== 'BackEndStat') throw new Error('Expected BackEndStat'); + + expect( + spec.formatterGenerator({ showPreparationsTotal: true })({ + lots: 826, + total: 430, + }) + ).toBe('826 / 430'); + expect( + spec.formatterGenerator({ showPreparationsTotal: false })({ + lots: 826, + total: 430, + }) + ).toBe('826'); +}); + +test('stale statistics are cleared after the refresh interval', () => { + const layout: StatLayout = { + label: 'page', + categories: [ + { + label: 'category', + items: [ + { + type: 'DefaultStat', + itemType: 'BackEndStat', + pageName: 'collection', + categoryName: 'preparations', + itemName: 'phantomItem', + label: localized('preparations'), + itemValue: '826 / 430', + }, + ], + }, + ], + lastUpdated: new Date('2024-01-01T00:00:00Z').toJSON(), + }; + jest + .spyOn(Date, 'now') + .mockReturnValue(new Date('2024-01-01T01:00:00Z').valueOf()); + + const [refreshed] = applyRefreshLayout([layout], 30) ?? []; + + expect(refreshed.lastUpdated).toBeUndefined(); + expect(refreshed.categories[0].items?.[0].itemValue).toBeUndefined(); +}); + +test('fresh statistics are kept before the refresh interval', () => { + const layout: StatLayout = { + label: 'page', + categories: [ + { + label: 'category', + items: [ + { + type: 'DefaultStat', + itemType: 'BackEndStat', + pageName: 'collection', + categoryName: 'preparations', + itemName: 'phantomItem', + label: localized('preparations'), + itemValue: '826 / 430', + }, + ], + }, + ], + lastUpdated: new Date('2024-01-01T00:00:00Z').toJSON(), + }; + jest + .spyOn(Date, 'now') + .mockReturnValue(new Date('2024-01-01T00:10:00Z').valueOf()); + + const [refreshed] = applyRefreshLayout([layout], 30) ?? []; + + expect(refreshed.lastUpdated).toBe(layout.lastUpdated); + expect(refreshed.categories[0].items?.[0].itemValue).toBe('826 / 430'); +});