Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • andretol/greg
1 result
Show changes
Commits on Source (16)
Showing
with 355 additions and 96 deletions
......@@ -154,8 +154,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"
}
}
}
......@@ -154,8 +154,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"
}
}
}
......@@ -155,8 +155,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"
}
}
}
......@@ -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>
)}
......
......@@ -5,11 +5,14 @@ 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
}
export const FeatureContext = createContext<IFeatureContext>({
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
})
export const useFeatureContext = () => useContext(FeatureContext)
......@@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) {
let features: IFeatureContext
switch (appInst) {
case 'uib':
features = { displayContactAtUnit: false, displayComment: false }
features = {
displayContactAtUnit: false,
displayComment: false,
displayContactAtUnitGuestInput: false,
}
break
case 'uio':
default:
features = { displayContactAtUnit: true, displayComment: true }
features = {
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
}
break
}
......
......@@ -14,7 +14,7 @@ export type GuestInviteInformation = {
role_name_nb: string
role_start: string
role_end: string
comment?: string
contact_person_unit?: string
feide_id?: string
email?: string
......
......@@ -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)
})
......@@ -21,12 +21,9 @@ import AuthenticationMethod from './authenticationMethod'
import GuestRegisterStep from './steps/register'
import GuestConsentStep from './steps/consent'
import GuestSuccessStep from './steps/success'
enum SubmitState {
NotSubmitted,
Submitted,
SubmittedError,
}
import ServerErrorReport, {
ServerErrorReportData,
} from '../../../components/errorReport'
enum Step {
RegisterStep,
......@@ -56,7 +53,7 @@ type InvitationData = {
role_name_en: string
start: string
end: string
comments: string
contact_person_unit: string
}
meta: {
session_type: string
......@@ -72,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] =
......@@ -87,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 =
......@@ -149,7 +167,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,
})
......@@ -192,6 +210,8 @@ export default function GuestRegister() {
const handleBack = () => {
if (activeStep === Step.ConsentStep) {
setActiveStep(Step.RegisterStep)
// Clear error if any
setSubmitGuestDataError(null)
}
}
......@@ -257,15 +277,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)
})
}
......@@ -372,6 +420,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>
)
}
......@@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form'
import React, {
forwardRef,
Ref,
useContext,
useEffect,
useImperativeHandle,
useState,
......@@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm'
import { GuestRegisterData } from '../enteredGuestData'
import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods'
import AuthenticationMethod from '../authenticationMethod'
import { FeatureContext } from '../../../../contexts'
interface GuestRegisterProperties {
nextHandler(registerData: GuestRegisterData): void
initialGuestData: GuestInviteInformation
registerData: GuestRegisterData | null
}
......@@ -57,6 +60,7 @@ const GuestRegisterStep = forwardRef(
string | undefined
>(undefined)
const [idErrorState, setIdErrorState] = useState<string>('')
const { displayContactAtUnitGuestInput } = useContext(FeatureContext)
console.log('register step registerData', registerData)
......@@ -528,14 +532,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>
......
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>
)
}
......@@ -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,
}
})
......@@ -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")
......
......@@ -54,7 +54,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(
......@@ -197,7 +197,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(
......@@ -243,6 +243,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."""
......@@ -253,7 +259,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,
......@@ -295,7 +301,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,
......@@ -303,7 +309,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,
......@@ -335,10 +341,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,
......@@ -370,12 +376,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,
......@@ -413,7 +420,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,
......@@ -438,7 +452,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
)
......@@ -461,13 +481,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):
"""
......@@ -509,10 +529,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,
......@@ -568,7 +588,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,
......@@ -592,6 +618,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):
"""
......@@ -601,3 +633,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})"
......@@ -5,7 +5,7 @@ from gregui.models import EmailTemplate, GregUserProfile
class GregUserProfileAdmin(VersionAdmin):
pass
list_display = ["id", "userid_feide", "person", "sponsor"]
class EmailTemplateAdmin(VersionAdmin):
......
......@@ -196,9 +196,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)
......@@ -235,7 +241,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},
}
......@@ -275,12 +281,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
......
......@@ -101,7 +101,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,
......