diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 90cf89f594c2ffad5682dfb6f413132ffb35160f..52db089562520b9aea8e2f38df58e005b65295cf 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -157,8 +157,18 @@ "error": "Error", "invitationCreationFailedHeader": "Failed to create invite", "errorStatusCode": "Status code: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "The server reported:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Message:<1>{{errorBodyText}}</1>", "contactHelp": "Contact help through the link in the footer if the problem persists.", - "unknown": "An unknown error has occurred. If the problem persists, contact support." + "errorLoadOusRoleTypeHeading": "Error loading form data", + "errorLoadOusRoleType": "Could not load organizational units and/or role type from server", + "unknown": "An unknown error has occurred. If the problem persists, contact support.", + "invitationDataFetchFailed": "Failed to fetch invitation data", + "guestRegistrationFailed": "Failed to register your data", + "codes": { + "invalid_invite": "Invalid invite", + "invite_expired": "Invite has expired", + "cannot_update_fields": "Failed to update data", + "update_national_id_not_allowed": "Not allowed to update verified national ID" + } } } diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 459ee2b90a74ce9c68ca909393dde1e3d615773e..a962ab956b81188873db379e15b2a92904e3a3c3 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -157,8 +157,18 @@ "error": "Feil", "invitationCreationFailedHeader": "Kunne ikke opprette invitasjon", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Melding:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", - "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte." + "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte.", + "errorLoadOusRoleTypeHeading": "Feil under lasting av skjemadata", + "errorLoadOusRoleType": "Kunne ikke laste organisasjons og/eller rolletype data fra server", + "invitationDataFetchFailed": "Klarte ikke å hente invitasjonsdata", + "guestRegistrationFailed": "Klarte ikke å registrere dataene dine", + "codes": { + "invalid_invite": "Ugyldig invitasjon", + "invite_expired": "Invitasjonen har utløpt", + "cannot_update_fields": "Klarte ikke å oppdatere dataene", + "update_national_id_not_allowed": "Ikke tillatt å endre verifisert fødselsnummer/D-nummer" + } } } diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 7e6fb203f1dde2c2df94e19c4cb612d1b81aca13..2f85d44f9064d951d8425b5427b30a64709b0d45 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -158,8 +158,18 @@ "error": "Feil", "invitationCreationFailedHeader": "Kunne ikkje opprette invitasjon", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Melding:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", - "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte." + "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte.",, + "errorLoadOusRoleTypeHeading": "Feil under lasting av skjemadata", + "errorLoadOusRoleType": "Kunne ikkje laste organisasjons og/eller rolletype data frå server", + "invitationDataFetchFailed": "Klarte ikkje å hente invitasjonsdata", + "guestRegistrationFailed": "Klarte ikkje å registrere dataene dine", + "codes": { + "invalid_invite": "Ugyldig invitasjon", + "invite_expired": "Invitasjonen har utløpe", + "cannot_update_fields": "Klarte ikkje å oppdatere dataene", + "update_national_id_not_allowed": "Ikkje tillete å endre verifisert fødselsnummer/D-nummer" + } } } diff --git a/frontend/src/components/errorReport/index.tsx b/frontend/src/components/errorReport/index.tsx index 4aace3fff5168fe2edd63667db6240d5fc2420dc..f95a9b33dd126ce42905ec633454e2f41051a3da 100644 --- a/frontend/src/components/errorReport/index.tsx +++ b/frontend/src/components/errorReport/index.tsx @@ -2,7 +2,7 @@ import { Alert, AlertTitle } from '@mui/material' import { Trans, useTranslation } from 'react-i18next' import React from 'react' -interface ErrorReportProps { +export type ServerErrorReportData = { errorHeading: string statusCode?: number statusText?: string @@ -14,8 +14,8 @@ export default function ServerErrorReport({ statusCode, statusText, errorBodyText, -}: ErrorReportProps) { - const [t] = useTranslation(['common']) +}: ServerErrorReportData) { + const { t } = useTranslation(['common']) return ( <Alert severity="error"> <AlertTitle>{errorHeading}</AlertTitle> @@ -27,7 +27,7 @@ export default function ServerErrorReport({ <br /> {errorBodyText !== undefined && ( <Trans i18nKey="error.genericServerErrorBody" t={t}> - The server reported: + Message: <blockquote>{{ errorBodyText }}</blockquote> </Trans> )} diff --git a/frontend/src/contexts/featureContext.ts b/frontend/src/contexts/featureContext.ts index 4213008402c39a13fdc9aabe56cc521885a02e48..b1cec4f71d62e2d0627a259150defde289a5376f 100644 --- a/frontend/src/contexts/featureContext.ts +++ b/frontend/src/contexts/featureContext.ts @@ -5,6 +5,8 @@ export interface IFeatureContext { displayContactAtUnit: boolean // Controls whether the optional field is shown in the register new guest wizard displayComment: boolean + // Should the contact at unit field be shown for the guest when he registers his information? + displayContactAtUnitGuestInput: boolean // Controls whether the gender field is shown for guests showGenderFieldForGuest: boolean } @@ -12,6 +14,7 @@ export interface IFeatureContext { export const FeatureContext = createContext<IFeatureContext>({ displayContactAtUnit: true, displayComment: true, + displayContactAtUnitGuestInput: true, showGenderFieldForGuest: true, }) diff --git a/frontend/src/providers/featureProvider.tsx b/frontend/src/providers/featureProvider.tsx index 7ac6ee48677aec5b9d10d974a7b9f6b765b999da..771b78d93fd0a2fc0c0eeb73f07bda89b9c359a2 100644 --- a/frontend/src/providers/featureProvider.tsx +++ b/frontend/src/providers/featureProvider.tsx @@ -16,6 +16,7 @@ function FeatureProvider(props: FeatureProviderProps) { features = { displayContactAtUnit: false, displayComment: false, + displayContactAtUnitGuestInput: false, showGenderFieldForGuest: true, } break @@ -25,6 +26,7 @@ function FeatureProvider(props: FeatureProviderProps) { features = { displayContactAtUnit: true, displayComment: true, + displayContactAtUnitGuestInput: true, showGenderFieldForGuest: false, } break diff --git a/frontend/src/routes/guest/register/guestDataForm.ts b/frontend/src/routes/guest/register/guestDataForm.ts index 37b96c4f56db7ce3ee96dac3a5d01d3a1fb69231..4e941f730ab29f54419f2d1bebd7a60bf45b0208 100644 --- a/frontend/src/routes/guest/register/guestDataForm.ts +++ b/frontend/src/routes/guest/register/guestDataForm.ts @@ -14,7 +14,7 @@ export type GuestInviteInformation = { role_name_nb: string role_start: string role_end: string - comment?: string + contact_person_unit?: string gender?: string feide_id?: string diff --git a/frontend/src/routes/guest/register/index.test.tsx b/frontend/src/routes/guest/register/index.test.tsx index b8a9c0df406d959b8375c6acdceb89c2eb3b3a9a..0ac01aee3ccd557ed5bfce7a30b38b62ffb1ccde 100644 --- a/frontend/src/routes/guest/register/index.test.tsx +++ b/frontend/src/routes/guest/register/index.test.tsx @@ -25,7 +25,6 @@ const testData = { start: '2021-08-10', end: '2021-08-16', contact_person_unit: 'Test contact person', - comments: 'Test comment', }, meta: { session_type: 'invite', @@ -66,5 +65,7 @@ test('Field showing values correctly', async () => { await screen.findByDisplayValue( `${testData.role.start} - ${testData.role.end}` ) - await screen.findByDisplayValue(testData.role.comments) + + // For the default setup the contact person at unit field should be showing + await screen.findByDisplayValue(testData.role.contact_person_unit) }) diff --git a/frontend/src/routes/guest/register/index.tsx b/frontend/src/routes/guest/register/index.tsx index 7b148dc2919b96af37fa5b9cbde52bad25d6967f..56f11e7705a484e3f889e4187d69dbd314a9255e 100644 --- a/frontend/src/routes/guest/register/index.tsx +++ b/frontend/src/routes/guest/register/index.tsx @@ -23,12 +23,6 @@ import GuestConsentStep from './steps/consent' import GuestSuccessStep from './steps/success' import { FeatureContext } from '../../../contexts' -enum SubmitState { - NotSubmitted, - Submitted, - SubmittedError, -} - enum Step { RegisterStep, ConsentStep, @@ -58,7 +52,7 @@ type InvitationData = { role_name_en: string start: string end: string - comments: string + contact_person_unit: string } meta: { session_type: string @@ -75,12 +69,6 @@ export default function GuestRegister() { const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null) const guestConsentRef = useRef<GuestRegisterCallableMethods>(null) - - // TODO On submit successful the user should be directed to some page telling - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [submitState, setSubmitState] = useState<SubmitState>( - SubmitState.NotSubmitted - ) const [activeStep, setActiveStep] = useState(0) const [initialGuestData, setInitialGuestData] = @@ -90,12 +78,39 @@ export default function GuestRegister() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [guestConsentData, setGuestConsentData] = useState<GuestConsentData | null>(null) + const [fetchInvitationDataError, setFetchInvitationDataError] = + useState<ServerErrorReportData | null>(null) + const [submitGuestDataError, setSubmitGuestDataError] = + useState<ServerErrorReportData | null>(null) const fetchInvitationData = async () => { const response = await fetch('/api/ui/v1/invited/', fetchJsonOpts()) if (!response.ok) { + try { + // Expect that the error will contain some JSON-data in the body + const errorData = await response.json() + setFetchInvitationDataError({ + errorHeading: t('error.invitationDataFetchFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: t(`error.codes.${errorData?.code}`), + }) + } catch (e: unknown) { + console.error(e) + + // Probably some unknown data in the body, create an error message + // using the rest of the information in the response + setFetchInvitationDataError({ + errorHeading: t('error.invitationDataFetchFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: undefined, + }) + } + return } + setFetchInvitationDataError(null) const data: InvitationData = await response.json() const authenticationMethod = @@ -153,7 +168,7 @@ export default function GuestRegister() { role_name_nb: data.role.role_name_nb ?? '', role_start: data.role.start ?? '', role_end: data.role.end ?? '', - comment: data.role.comments ?? '', + contact_person_unit: data.role.contact_person_unit ?? '', authentication_method: authenticationMethod, }) @@ -197,6 +212,8 @@ export default function GuestRegister() { const handleBack = () => { if (activeStep === Step.ConsentStep) { setActiveStep(Step.RegisterStep) + // Clear error if any + setSubmitGuestDataError(null) } } @@ -267,15 +284,43 @@ export default function GuestRegister() { fetch('/api/ui/v1/invited/', submitJsonOpts('POST', payload)) .then((response) => { if (response.ok) { - setSubmitState(SubmitState.Submitted) setActiveStep(Step.SuccessStep) + setSubmitGuestDataError(null) } else { - setSubmitState(SubmitState.SubmittedError) - console.error(`Server responded with status: ${response.status}`) + // Expect that the error will contain JSON-data in the body + // and that there will be a code field there + response + .json() + .then((errorData) => { + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: t(`error.codes.${errorData?.code}`), + }) + }) + .catch((error) => { + console.error(error) + + // Probably some unknown data in the body, create an error message + // using the rest of the information in the response + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: undefined, + }) + }) } }) .catch((error) => { - setSubmitState(SubmitState.SubmittedError) + // Something went wrong before/during the backend was called + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: undefined, + statusText: undefined, + errorBodyText: undefined, + }) console.error(error) }) } @@ -382,6 +427,24 @@ export default function GuestRegister() { </Button> )} </Box> + + {fetchInvitationDataError !== null && ( + <ServerErrorReport + errorHeading={fetchInvitationDataError?.errorHeading} + statusCode={fetchInvitationDataError?.statusCode} + statusText={fetchInvitationDataError?.statusText} + errorBodyText={fetchInvitationDataError?.errorBodyText} + /> + )} + + {submitGuestDataError !== null && ( + <ServerErrorReport + errorHeading={submitGuestDataError?.errorHeading} + statusCode={submitGuestDataError?.statusCode} + statusText={submitGuestDataError?.statusText} + errorBodyText={submitGuestDataError?.errorBodyText} + /> + )} </Page> ) } diff --git a/frontend/src/routes/guest/register/steps/register.tsx b/frontend/src/routes/guest/register/steps/register.tsx index e79615611e393763f9c178ca887eccb3823e7d9a..6d9d2641dca5e7be1a1ac33b7c0a3d370abb3367 100644 --- a/frontend/src/routes/guest/register/steps/register.tsx +++ b/frontend/src/routes/guest/register/steps/register.tsx @@ -62,6 +62,7 @@ const GuestRegisterStep = forwardRef( >(undefined) const [gender, setGender] = useState<string>('') const [idErrorState, setIdErrorState] = useState<string>('') + const { displayContactAtUnitGuestInput } = useContext(FeatureContext) console.log('register step registerData', registerData) @@ -571,14 +572,14 @@ const GuestRegisterStep = forwardRef( disabled /> - <TextField - id="comment" - label={t('input.comment')} - multiline - rows={5} - value={initialGuestData.comment} - disabled - /> + {displayContactAtUnitGuestInput && ( + <TextField + id="contactPersonUnit" + label={t('input.contactPersonUnit')} + value={initialGuestData.contact_person_unit} + disabled + /> + )} </Stack> </form> </Box> diff --git a/frontend/src/routes/sponsor/register/stepRegistration.tsx b/frontend/src/routes/sponsor/register/stepRegistration.tsx index 3b7e98aed639820b29d584a3a034cb7c26ef19ca..9486b9ca60efea9cd81a0cb3f582750b40763a11 100644 --- a/frontend/src/routes/sponsor/register/stepRegistration.tsx +++ b/frontend/src/routes/sponsor/register/stepRegistration.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react' +import React, { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Box, Button } from '@mui/material' @@ -14,7 +14,11 @@ import SubmitState from './submitState' import SponsorGuestButtons from '../../components/sponsorGuestButtons' import { submitJsonOpts } from '../../../utils' import StepSubmitSuccess from './stepSubmitSuccess' -import ServerErrorReport from '../../../components/errorReport' +import ServerErrorReport, { + ServerErrorReportData, +} from '../../../components/errorReport' +import useOus from '../../../hooks/useOus' +import useRoleTypes from '../../../hooks/useRoleTypes' enum Steps { RegisterStep = 0, @@ -22,12 +26,6 @@ enum Steps { SuccessStep = 2, } -interface SubmitErrorData { - statusCode: number - statusText: string - bodyText: string -} - /** * * This component controls the invite process where the sponsor @@ -52,7 +50,16 @@ export default function StepRegistration() { const [activeStep, setActiveStep] = useState(0) const personFormRef = useRef<PersonFormMethods>(null) const [submitState, setSubmitState] = useState(SubmitState.NotSubmitted) - const [errorReport, setErrorReport] = useState<SubmitErrorData>() + const [submitErrorReport, setSubmitErrorReport] = + useState<ServerErrorReportData>() + const [formDataErrorReport, setFormDataErrorReport] = + useState<ServerErrorReportData>() + + // The organizational unit and role types are not used by this component, but + // loading them here anyways to make sure that they can be loaded using the + // hooks. If they cannot, an error message will be shown to the user + const ous = useOus() + const roleTypes = useRoleTypes() const handleNext = () => { if (activeStep === 0) { @@ -91,10 +98,11 @@ export default function StepRegistration() { if (!res.ok) { res.text().then((text) => { setSubmitState(SubmitState.SubmitFailure) - setErrorReport({ + setSubmitErrorReport({ + errorHeading: t('error.invitationCreationFailedHeader'), statusCode: res.status, statusText: res.statusText, - bodyText: text, + errorBodyText: text, }) }) } else { @@ -130,6 +138,20 @@ export default function StepRegistration() { history.push('/') } + useEffect(() => { + if (ous.length === 0 || roleTypes.length === 0) { + // These arrays should have values. There is no information + // about the status code at this level, since the values come + // from hooks and any server errors are handled there + setFormDataErrorReport({ + errorHeading: t('error.errorLoadOusRoleTypeHeading'), + statusCode: undefined, + statusText: undefined, + errorBodyText: t('error.errorLoadOusRoleType'), + }) + } + }, []) + return ( <Page> <SponsorGuestButtons registerNewGuestActive /> @@ -203,14 +225,23 @@ export default function StepRegistration() { {activeStep === Steps.SuccessStep && <StepSubmitSuccess />} {submitState === SubmitState.SubmitFailure && - errorReport !== undefined && ( + submitErrorReport !== undefined && ( <ServerErrorReport - errorHeading={t('error.invitationCreationFailedHeader')} - statusCode={errorReport?.statusCode} - statusText={errorReport?.statusText} - errorBodyText={errorReport?.bodyText} + errorHeading={submitErrorReport?.errorHeading} + statusCode={submitErrorReport?.statusCode} + statusText={submitErrorReport?.statusText} + errorBodyText={submitErrorReport?.errorBodyText} /> )} + + {formDataErrorReport !== undefined && ( + <ServerErrorReport + errorHeading={formDataErrorReport.errorHeading} + statusCode={undefined} + statusText={undefined} + errorBodyText={formDataErrorReport.errorBodyText} + /> + )} </Page> ) } diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx index 7be8884de2a2feb948ca530b2b2464225e5e5be5..fa8739024553136211812cbfeb356c9840f2fd8c 100644 --- a/frontend/src/test-utils.tsx +++ b/frontend/src/test-utils.tsx @@ -19,13 +19,71 @@ export * from '@testing-library/react' // override render method export { customRender as render } -// Mock react-i18next module to return a translation that just returns the key -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (value: string) => value, - i18n: { - language: 'en', - changeLanguage: () => new Promise(() => {}), - }, - }), -})) +// The reason for this complex mock of react-i18next is to make the Trans-component work when running tests +jest.mock('react-i18next', (): object => { + // Need to require React at this level as well to avoid getting an error + // when running the tests saying that the module factory is not allowed + // to reference out-of-scope variables + // eslint-disable-next-line @typescript-eslint/no-shadow,global-require + const React = require('react') + + const hasChildren = (node: any): boolean => + node && (node.children || (node.props && node.props.children)) + + const getChildrenFromProps = (node: any): any => + node.props && node.props.children ? node.props.children : null + + const getChildren = (node: any): any => + node && node.children ? node.children : getChildrenFromProps(node) + + const renderNodes = (reactNodes: any): any => { + if (typeof reactNodes === 'string') { + return reactNodes + } + + return Object.keys(reactNodes).map((key, i) => { + const child = reactNodes[key] + + if (typeof child === 'string') { + return child + } + + if (hasChildren(child)) { + const inner = renderNodes(getChildren(child)) + // eslint-disable-next-line react/no-array-index-key + return React.cloneElement(child, { ...child.props, key: i }, inner) + } + + const isElement = React.isValidElement(child) + if (typeof child === 'object' && !isElement) { + return Object.keys(child).reduce( + (str, childKey) => `${str}${child[childKey]}`, + '' + ) + } + + return child + }) + } + + const useMock: any = [(k: any) => k, {}] + // Mock react-i18next module to return a translation that just returns the key + useMock.t = (k: any) => k + useMock.i18n = { + // Return "en" as the selected language if the code asks for it + language: 'en', + changeLanguage: () => new Promise(() => {}), + } + + return { + withTranslation: + () => + (Component: any): any => + (props: any): any => + <Component t={(k: any): any => k} {...props} />, + Trans: ({ children }: any) => renderNodes(children), + Translation: ({ children }: any) => + children((k: any): any => k, { i18n: {} }), + useTranslation: () => useMock, + } +}) diff --git a/greg/admin.py b/greg/admin.py index c850afc5309afa8dbd95e8acedeafa01a265b6e4..2d9252c444828ffde1b216a6882189c88b3dd62a 100644 --- a/greg/admin.py +++ b/greg/admin.py @@ -135,12 +135,20 @@ class OrganizationalUnitInline(admin.TabularInline): class SponsorAdmin(VersionAdmin): - list_display = ("id", "feide_id") + list_display = ("id", "feide_id", "first_name", "last_name") inlines = (OrganizationalUnitInline,) readonly_fields = ("id", "created", "updated") class SponsorOrganizationalUnitAdmin(VersionAdmin): + list_display = ( + "id", + "sponsor", + "organizational_unit", + "hierarchical_access", + "source", + "automatic", + ) readonly_fields = ("id", "created", "updated") diff --git a/greg/models.py b/greg/models.py index 1bb0c2cef9f14a88b7e8054d48d0cb4ec410af05..e2af784fe6751bbb727def62a9bbf0a132d02889 100644 --- a/greg/models.py +++ b/greg/models.py @@ -59,7 +59,7 @@ class Person(BaseModel): objects = PersonManager() def __str__(self): - return "{} {} ({})".format(self.first_name, self.last_name, self.pk) + return f"{self.first_name} {self.last_name} ({self.pk})" def __repr__(self): return "{}(id={!r}, first_name={!r}, last_name={!r})".format( @@ -202,7 +202,7 @@ class RoleType(BaseModel): max_days = models.IntegerField(default=365) def __str__(self): - return "{} ({})".format(str(self.name_en or self.name_nb), self.identifier) + return f"{str(self.name_en or self.name_nb)} ({self.identifier})" def __repr__(self): return "{}(pk={!r}, identifier={!r}, name_nb={!r}, name_en={!r})".format( @@ -248,6 +248,12 @@ class Role(BaseModel): self.__class__.__name__, self.pk, self.person, self.type ) + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, " + f"role={self.person}/{self.type}@{self.orgunit.name_nb})" + ) + class Notification(BaseModel): """A change notification that should be delivered to a message queue.""" @@ -258,7 +264,7 @@ class Notification(BaseModel): issued_at = models.IntegerField() meta = models.JSONField(null=True, blank=True) - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, identifier={!r}, object_type={!r}, operation={!r}, issued_at={!r}, meta={!r})".format( self.__class__.__name__, self.pk, @@ -300,7 +306,7 @@ class Identity(BaseModel): ) verified_at = models.DateTimeField(null=True, blank=True) - def __str__(self): + def __str__(self) -> str: return "{}(id={!r}, type={!r}, value={!r})".format( self.__class__.__name__, self.pk, @@ -308,7 +314,7 @@ class Identity(BaseModel): self.value, ) - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, person_id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_at={!r})".format( self.__class__.__name__, self.pk, @@ -340,10 +346,10 @@ class ConsentType(BaseModel): user_allowed_to_change = models.BooleanField() mandatory = models.BooleanField(default=False) - def __str__(self): - return "{} ({})".format(str(self.name_en or self.name_nb), self.identifier) + def __str__(self) -> str: + return f"{str(self.name_en or self.name_nb)} ({self.identifier})" - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, identifier={!r}, name_en={!r}, valid_from={!r}, user_allowed_to_change={!r})".format( self.__class__.__name__, self.pk, @@ -375,12 +381,13 @@ class ConsentChoice(BaseModel): ), ) - def __str__(self): - return "{}: {} ({})".format( - str(self.consent_type), str(self.text_en or self.text_nb), self.value + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, value={self.value}, " + f"type={self.consent_type}))" ) - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, consent_type={!r} value={!r}, text_en={!r}, text_nb={!r}, text_nn={!r})".format( self.__class__.__name__, self.pk, @@ -418,7 +425,14 @@ class Consent(BaseModel): ) ] - def __repr__(self): + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, " + f"person={self.person}, type={self.type}, " + f"consent_given_at={self.consent_given_at})" + ) + + def __repr__(self) -> str: return "{}(id={!r}, person={!r}, type={!r}, consent_given_at={!r})".format( self.__class__.__name__, self.pk, @@ -443,7 +457,13 @@ class OuIdentifier(BaseModel): models.UniqueConstraint(name="unique_identifier", fields=["name", "value"]) ] - def __repr__(self): + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, " + f"name={self.name}, value={self.value})" + ) + + def __repr__(self) -> str: return "{}(id={!r}, name={!r}, value={!r})".format( self.__class__.__name__, self.pk, self.name, self.value ) @@ -466,13 +486,13 @@ class OrganizationalUnit(BaseModel): active = models.BooleanField(default=True) deleted = models.BooleanField(default=False) - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, name_en={!r}, parent={!r})".format( self.__class__.__name__, self.pk, self.name_en, self.parent ) - def __str__(self): - return "{}".format(str(self.name_en or self.name_nb)) + def __str__(self) -> str: + return f"{self.name_en or self.name_nb}" def fetch_tree(self): """ @@ -514,10 +534,10 @@ class Sponsor(BaseModel): related_name="sponsor_unit", ) - def __str__(self): - return "{} ({} {})".format(self.feide_id, self.first_name, self.last_name) + def __str__(self) -> str: + return f"{self.feide_id} ({self.first_name} {self.last_name})" - def __repr__(self): + def __repr__(self) -> str: return "{}(id={!r}, feide_id={!r}, first_name={!r}, last_name={!r})".format( self.__class__.__name__, self.pk, @@ -573,7 +593,13 @@ class SponsorOrganizationalUnit(BaseModel): ) ] - def __repr__(self): + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, sponsor={self.sponsor}, " + f"org_unit={self.organizational_unit})" + ) + + def __repr__(self) -> str: return "{}(id={!r}, sponsor={!r}, organizational_unit={!r}, hierarchical_access={!r})".format( self.__class__.__name__, self.pk, @@ -597,6 +623,12 @@ class InvitationLink(BaseModel): ) expire = models.DateTimeField(blank=False, null=False) + def __str__(self) -> str: + return ( + f"{self.__class__.__name__}(id={self.pk}, invitation={self.invitation}, " + f"uuid={self.uuid}, expire={self.expire})" + ) + class Invitation(BaseModel): """ @@ -606,3 +638,6 @@ class Invitation(BaseModel): """ role = models.ForeignKey("Role", null=False, blank=False, on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"{self.__class__.__name__}(id={self.pk}, role={self.role})" diff --git a/gregui/admin.py b/gregui/admin.py index 7c3f771755952b78e409029ce4c17be9bd6412eb..7b80a29b329de04566c2edcd312c97918686c126 100644 --- a/gregui/admin.py +++ b/gregui/admin.py @@ -5,7 +5,7 @@ from gregui.models import EmailTemplate, GregUserProfile class GregUserProfileAdmin(VersionAdmin): - pass + list_display = ["id", "userid_feide", "person", "sponsor"] class EmailTemplateAdmin(VersionAdmin): diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 6ac668fe429ca108ed9cfe701336eeae72bd6fe3..1df8e74ba20219de72d713bdb8483723cc49fdf1 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -197,9 +197,15 @@ class InvitedGuestView(GenericAPIView): try: invite_link = InvitationLink.objects.get(uuid=invite_id) except (InvitationLink.DoesNotExist, exceptions.ValidationError): - return Response(status=status.HTTP_403_FORBIDDEN) + return Response( + status=status.HTTP_403_FORBIDDEN, + data={"code": "invalid_invite", "message": "Invalid invite"}, + ) if invite_link.expire <= timezone.now(): - return Response(status=status.HTTP_403_FORBIDDEN) + return Response( + status=status.HTTP_403_FORBIDDEN, + data={"code": "invite_expired", "message": "Invite expired"}, + ) # if invite_id: invite_link = InvitationLink.objects.get(uuid=invite_id) @@ -237,7 +243,7 @@ class InvitedGuestView(GenericAPIView): "role_name_en": role.type.name_en, "start": role.start_date, "end": role.end_date, - "comments": role.comments, + "contact_person_unit": role.contact_person_unit, }, "meta": {"session_type": session_type}, } @@ -277,12 +283,21 @@ class InvitedGuestView(GenericAPIView): if illegal_fields: return Response( status=status.HTTP_400_BAD_REQUEST, - data={"error": {"cannot_update_fields": illegal_fields}}, + data={ + "code": "cannot_update_fields", + "message": f"cannot_update_fields: {illegal_fields}", + }, ) if self._verified_fnr_already_exists(person) and fnr: # The user should not be allowed to change a verified fnr - return Response(status=status.HTTP_400_BAD_REQUEST) + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "code": "update_national_id_not_allowed", + "message": "Not allowed to update verified national ID", + }, + ) with transaction.atomic(): # Note this only serializes data for the person, it does not look at other sections diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py index 3df5de6b2821681af110c4bb351e17ebb4fff129..755cdc3daa5fb6c7e0970dd2354b48b0abb65a08 100644 --- a/gregui/tests/api/views/test_invitation.py +++ b/gregui/tests/api/views/test_invitation.py @@ -102,7 +102,7 @@ def test_get_invited_info_session_okay( assert data.get("role") == dict( start=None, end="2050-10-15", - comments="", + contact_person_unit="", ou_name_en=unit_foo.name_en, ou_name_nb=unit_foo.name_nb, role_name_en=role_type_foo.name_en,