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 (9)
Showing
with 351 additions and 187 deletions
...@@ -18,4 +18,5 @@ disable= ...@@ -18,4 +18,5 @@ disable=
redefined-outer-name, redefined-outer-name,
too-few-public-methods, too-few-public-methods,
too-many-ancestors, too-many-ancestors,
too-many-arguments,
unused-argument, unused-argument,
{ {
"test": "This is a test",
"nested": {
"test": "This is a nested value"
},
"language": { "language": {
"change": "Change language to {{lang}}" "change": "Change language to {{lang}}"
}, },
...@@ -75,5 +71,7 @@ ...@@ -75,5 +71,7 @@
"guestRole": "Guest role", "guestRole": "Guest role",
"guestPeriod":"Period", "guestPeriod":"Period",
"guestDepartment": "Department" "guestDepartment": "Department"
} },
"yourGuests": "Your guests",
"registerNewGuest": "Register new guest"
} }
{ {
"test": "Dette er en test",
"nested": {
"test": "Dette er en 'nested' verdi"
},
"language": { "language": {
"change": "Bytt språk til {{lang}}" "change": "Bytt språk til {{lang}}"
}, },
...@@ -75,5 +71,7 @@ ...@@ -75,5 +71,7 @@
"guestRole": "Gjesterolle", "guestRole": "Gjesterolle",
"guestPeriod": "Periode", "guestPeriod": "Periode",
"guestDepartment": "Avdeling" "guestDepartment": "Avdeling"
} },
"yourGuests": "Dine gjester",
"registerNewGuest": "Registrer ny gjest"
} }
{ {
"test": "Dette er ein test",
"nested": {
"test": "Dette er ein 'nested' verdi"
},
"language": { "language": {
"languageName": "Språk", "languageName": "Språk",
"change": "Bytt språk til {{lang}}" "change": "Bytt språk til {{lang}}"
...@@ -76,5 +72,7 @@ ...@@ -76,5 +72,7 @@
"guestRole": "Gjesterolle", "guestRole": "Gjesterolle",
"guestPeriod": "Periode", "guestPeriod": "Periode",
"guestDepartment": "Avdeling" "guestDepartment": "Avdeling"
} },
"yourGuests": "Dine gjestar",
"registerNewGuest": "Registrer ny gjest"
} }
import PersonIcon from '@mui/icons-material/Person'
import { Box, IconButton, Theme } from '@mui/material'
import PersonAddIcon from '@mui/icons-material/PersonAdd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
interface SponsorGuestButtonsProps {
yourGuestsActive?: boolean,
registerNewGuestActive?: boolean
}
export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) {
const { yourGuestsActive, registerNewGuestActive } = props
const { t } = useTranslation(['common'])
const history = useHistory()
const goToOverview = () => {
history.push('/sponsor')
}
const goToRegister = () => {
history.push('/register')
}
return (
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', marginBottom: '2rem' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton onClick={goToOverview}>
<PersonIcon
fontSize='large'
sx={{
borderRadius: '2rem',
borderStyle: 'solid',
borderColor: (theme: Theme) => yourGuestsActive ? theme.palette.primary.main : theme.greg.deactivatedColor,
fill: 'white',
backgroundColor: (theme: Theme) => yourGuestsActive ? theme.palette.primary.main : theme.greg.deactivatedColor,
}} />
</IconButton>
<Box sx={{
typography: 'caption',
}}>
{t('yourGuests')}
</Box>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton onClick={goToRegister}>
<PersonAddIcon
fontSize='large'
sx={{
borderRadius: '2rem',
borderStyle: 'solid',
borderColor: (theme: Theme) => registerNewGuestActive ? theme.palette.primary.main : theme.greg.deactivatedColor,
fill: 'white',
backgroundColor: (theme: Theme) => registerNewGuestActive ? theme.palette.primary.main : theme.greg.deactivatedColor,
}} />
</IconButton>
<Box sx={{
typography: 'caption',
}}>
{t('registerNewGuest')}
</Box>
</Box>
</Box>
)
}
SponsorGuestButtons.defaultProps = {
yourGuestsActive: false,
registerNewGuestActive: false,
}
...@@ -51,9 +51,9 @@ export default function App() { ...@@ -51,9 +51,9 @@ export default function App() {
<ProtectedRoute path="/sponsor"> <ProtectedRoute path="/sponsor">
<Sponsor /> <Sponsor />
</ProtectedRoute> </ProtectedRoute>
<Route path="/register"> <ProtectedRoute path="/register">
<Register /> <Register />
</Route> </ProtectedRoute>
<Route path="/invite/:id" component={InviteLink} /> <Route path="/invite/:id" component={InviteLink} />
<Route path="/invite/" component={Invite} /> <Route path="/invite/" component={Invite} />
<Route> <Route>
......
import React, { useState, useRef } from 'react' import React, { useState, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Box, Button, Step, StepLabel, Stepper } from '@mui/material' import { Box, Button } from '@mui/material'
import Page from 'components/page' import Page from 'components/page'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
...@@ -11,8 +11,8 @@ import StepPersonForm from './stepPersonForm' ...@@ -11,8 +11,8 @@ import StepPersonForm from './stepPersonForm'
import { PersonFormMethods } from './personFormMethods' import { PersonFormMethods } from './personFormMethods'
import { SummaryFormMethods } from './summaryFormMethods' import { SummaryFormMethods } from './summaryFormMethods'
import SubmitState from './submitState' import SubmitState from './submitState'
import SponsorGuestButtons from '../components/sponsorGuestButtons'
const steps = ['Register', 'Summary']
export default function StepRegistration() { export default function StepRegistration() {
const { t } = useTranslation(['common']) const { t } = useTranslation(['common'])
...@@ -56,7 +56,7 @@ export default function StepRegistration() { ...@@ -56,7 +56,7 @@ export default function StepRegistration() {
} }
const handleForwardFromRegister = ( const handleForwardFromRegister = (
updateFormData: RegisterFormData updateFormData: RegisterFormData,
): void => { ): void => {
setFormData(updateFormData) setFormData(updateFormData)
setActiveStep((prevActiveStep) => prevActiveStep + 1) setActiveStep((prevActiveStep) => prevActiveStep + 1)
...@@ -76,21 +76,7 @@ export default function StepRegistration() { ...@@ -76,21 +76,7 @@ export default function StepRegistration() {
return ( return (
<Page> <Page>
{/* Stepper at top of wizard */} <SponsorGuestButtons registerNewGuestActive />
<Stepper sx={{ paddingTop: '2rem' }}
activeStep={activeStep}>
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
{steps.map((label, index) => {
const stepProps = {}
const labelProps = {}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
)
})}
</Stepper>
{/* Current page in wizard */} {/* Current page in wizard */}
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
{activeStep === REGISTER_STEP && ( {activeStep === REGISTER_STEP && (
...@@ -110,7 +96,7 @@ export default function StepRegistration() { ...@@ -110,7 +96,7 @@ export default function StepRegistration() {
)} )}
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2, color: 'primary.main', paddingBottom: '1rem'}}> <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2, color: 'primary.main', paddingBottom: '1rem' }}>
{activeStep === REGISTER_STEP && ( {activeStep === REGISTER_STEP && (
<Button data-testid='button-next' <Button data-testid='button-next'
sx={{ color: 'theme.palette.secondary', mr: 1 }} sx={{ color: 'theme.palette.secondary', mr: 1 }}
......
...@@ -158,13 +158,11 @@ const StepPersonForm = forwardRef((props: StepPersonFormProperties, ref: Ref<Per ...@@ -158,13 +158,11 @@ const StepPersonForm = forwardRef((props: StepPersonFormProperties, ref: Ref<Per
ous ous
.sort(i18n.language === 'en' ? enSort : nbSort) .sort(i18n.language === 'en' ? enSort : nbSort)
.map((ou) => ( .map((ou) => (
<MenuItem value={ou.id}> <MenuItem key={ou.id.toString()} value={ou.id}>
{i18n.language === 'en' ? ou.en : ou.nb} ({ou.id}) {i18n.language === 'en' ? ou.en : ou.nb} ({ou.id})
</MenuItem> </MenuItem>
)) ))
) : ( ) : ('')}
<></>
)}
</Select> </Select>
</FormControl> </FormControl>
...@@ -180,6 +178,7 @@ const StepPersonForm = forwardRef((props: StepPersonFormProperties, ref: Ref<Per ...@@ -180,6 +178,7 @@ const StepPersonForm = forwardRef((props: StepPersonFormProperties, ref: Ref<Per
roleTypes.sort(roleTypeSort()) roleTypes.sort(roleTypeSort())
.map((roleType) => ( .map((roleType) => (
<MenuItem <MenuItem
key={roleType.id.toString()}
value={roleType.id}>{i18n.language === 'en' ? roleType.name_en : roleType.name_nb}</MenuItem> value={roleType.id}>{i18n.language === 'en' ? roleType.name_en : roleType.name_nb}</MenuItem>
)) ))
} }
......
...@@ -6,12 +6,13 @@ import { ...@@ -6,12 +6,13 @@ import {
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
Paper, Paper, Accordion, AccordionSummary, AccordionDetails,
} from '@mui/material' } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import Page from 'components/page' import Page from 'components/page'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import SponsorGuestButtons from '../../components/sponsorGuestButtons'
type PersonInfo = { type PersonInfo = {
name: string name: string
...@@ -50,24 +51,26 @@ const PersonLine = ({ person }: PersonLineProps) => { ...@@ -50,24 +51,26 @@ const PersonLine = ({ person }: PersonLineProps) => {
key={person.name} key={person.name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
> >
<TableCell component="th" scope="row"> <TableCell component='th' scope='row'>
{person.name} {person.name}
</TableCell> </TableCell>
<TableCell align="left"> <TableCell align='left'>
{i18n.language === 'en' ? person.role_en : person.role_nb} {i18n.language === 'en' ? person.role_en : person.role_nb}
</TableCell> </TableCell>
<TableCell align="left">{person.period}</TableCell> <TableCell align='left'>{person.period}</TableCell>
<TableCell align="left"> <TableCell align='left'>
{i18n.language === 'en' ? person.ou_en : person.ou_nb} {i18n.language === 'en' ? person.ou_en : person.ou_nb}
</TableCell> </TableCell>
<TableCell align="left"> <TableCell align='left'>
<button type="button">{t('common:details')}</button> <button type='button'>{t('common:details')}</button>
</TableCell> </TableCell>
</TableRow> </TableRow>
) )
} }
const ActiveGuests = ({ persons }: GuestProps) => { const ActiveGuests = ({ persons }: GuestProps) => {
const [activeExpanded, setActiveExpanded] = useState(false)
// Only show active people // Only show active people
let guests = persons.length > 0 ? persons : [] let guests = persons.length > 0 ? persons : []
if (guests.length > 0) { if (guests.length > 0) {
...@@ -75,38 +78,46 @@ const ActiveGuests = ({ persons }: GuestProps) => { ...@@ -75,38 +78,46 @@ const ActiveGuests = ({ persons }: GuestProps) => {
} }
const [t] = useTranslation(['common']) const [t] = useTranslation(['common'])
return ( return (
<> <Accordion expanded={activeExpanded} onChange={() => {
<h1>{t('common:activeGuests')}</h1> setActiveExpanded(!activeExpanded)
<p>{t('common:activeGuestsDescription')}</p> }}>
<TableContainer component={Paper}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Table sx={{ minWidth: 650 }} aria-label="simple table"> <h4>{t('common:activeGuests')}</h4>
<TableHead> </AccordionSummary>
<TableRow> <AccordionDetails>
<TableCell>{t('common:name')}</TableCell> <p>{t('common:activeGuestsDescription')}</p>
<TableCell align="left">{t('common:role')}</TableCell> <TableContainer component={Paper}>
<TableCell align="left">{t('common:period')}</TableCell> <Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableCell align="left">{t('common:ou')}</TableCell> <TableHead sx={{ backgroundColor: 'primary.light' }}>
<TableCell align="left">{t('common:choice')}</TableCell> <TableRow>
</TableRow> <TableCell>{t('common:name')}</TableCell>
</TableHead> <TableCell align='left'>{t('common:role')}</TableCell>
<TableBody> <TableCell align='left'>{t('common:period')}</TableCell>
{guests.map((person) => ( <TableCell align='left'>{t('common:ou')}</TableCell>
<PersonLine person={person} /> <TableCell align='left'>{t('common:choice')}</TableCell>
))} </TableRow>
</TableHead>
<TableRow> <TableBody>
<TableCell> {guests.map((person) => (
{guests.length > 0 ? '' : t('common:noActiveGuests')} <PersonLine person={person} />
</TableCell> ))}
</TableRow>
</TableBody> <TableRow>
</Table> <TableCell>
</TableContainer> {guests.length > 0 ? '' : t('common:noActiveGuests')}
</> </TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
) )
} }
const WaitingGuests = ({ persons }: GuestProps) => { const WaitingGuests = ({ persons }: GuestProps) => {
const [waitingExpanded, setWaitingExpanded] = useState(false)
// Only show non-active people // Only show non-active people
let guests = persons.length > 0 ? persons : [] let guests = persons.length > 0 ? persons : []
if (guests.length > 0) { if (guests.length > 0) {
...@@ -115,41 +126,48 @@ const WaitingGuests = ({ persons }: GuestProps) => { ...@@ -115,41 +126,48 @@ const WaitingGuests = ({ persons }: GuestProps) => {
const [t] = useTranslation(['common']) const [t] = useTranslation(['common'])
return ( return (
<> <Accordion expanded={waitingExpanded} onChange={() => {
<h1>{t('common:waitingGuests')}</h1> setWaitingExpanded(!waitingExpanded)
<p>{t('common:waitingGuestsDescription')}</p> }}
sx={{ border: 'none' }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table"> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<TableHead> <h4>{t('common:waitingGuests')}</h4>
<TableRow> </AccordionSummary>
<TableCell>{t('common:name')}</TableCell> <AccordionDetails>
<TableCell align="left">{t('common:role')}</TableCell> <p>{t('common:waitingGuestsDescription')}</p>
<TableCell align="left">{t('common:period')}</TableCell>
<TableCell align="left">{t('common:ou')}</TableCell> <TableContainer component={Paper}>
<TableCell align="left">{t('common:choice')}</TableCell> <Table sx={{ minWidth: 650 }} aria-label='simple table'>
</TableRow> <TableHead sx={{ backgroundColor: 'primary.light' }}>
</TableHead> <TableRow>
<TableBody> <TableCell>{t('common:name')}</TableCell>
{guests.map((person) => ( <TableCell align='left'>{t('common:role')}</TableCell>
<PersonLine person={person} /> <TableCell align='left'>{t('common:period')}</TableCell>
))} <TableCell align='left'>{t('common:ou')}</TableCell>
<TableRow> <TableCell align='left'>{t('common:choice')}</TableCell>
<TableCell> </TableRow>
{guests.length > 0 ? '' : t('common:noWaitingGuests')} </TableHead>
</TableCell> <TableBody>
</TableRow> {guests.map((person) => (
</TableBody> <PersonLine person={person} />
</Table> ))}
</TableContainer> <TableRow>
</> <TableCell>
{guests.length > 0 ? '' : t('common:noWaitingGuests')}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
) )
} }
function FrontPage() { function FrontPage() {
const [persons, setPersons] = useState<Array<PersonInfo>>([]) const [persons, setPersons] = useState<Array<PersonInfo>>([])
const [t] = useTranslation(['common'])
const fetchGuestsInfo = async () => { const fetchGuestsInfo = async () => {
const response = await fetch('/api/ui/v1/guests/?format=json') const response = await fetch('/api/ui/v1/guests/?format=json')
const jsonResponse = await response.json() const jsonResponse = await response.json()
...@@ -173,10 +191,9 @@ function FrontPage() { ...@@ -173,10 +191,9 @@ function FrontPage() {
}, []) }, [])
return ( return (
<Page header="Sponsor front page"> <Page>
<Link to="/register">{t('common:registerText')}</Link> <SponsorGuestButtons yourGuestsActive />
<WaitingGuests persons={persons} /> <WaitingGuests persons={persons} />
<hr className="rounded" />
<ActiveGuests persons={persons} /> <ActiveGuests persons={persons} />
</Page> </Page>
) )
......
...@@ -17,6 +17,7 @@ declare module '@mui/material/styles' { ...@@ -17,6 +17,7 @@ declare module '@mui/material/styles' {
h2TextColor: string h2TextColor: string
footerBackgroundColor: string footerBackgroundColor: string
footerTextColor: string footerTextColor: string
deactivatedColor: string
} }
} }
// allow configuration using `createTheme` // allow configuration using `createTheme`
...@@ -28,7 +29,8 @@ declare module '@mui/material/styles' { ...@@ -28,7 +29,8 @@ declare module '@mui/material/styles' {
h1TextColor?: string h1TextColor?: string
h2TextColor?: string h2TextColor?: string
footerBackgroundColor?: string footerBackgroundColor?: string
footerTextColor?: string footerTextColor?: string,
deactivatedColor?: string
} }
} }
} }
......
...@@ -11,11 +11,13 @@ const mainTheme: ThemeOptions = { ...@@ -11,11 +11,13 @@ const mainTheme: ThemeOptions = {
h2TextColor: '#373F41', h2TextColor: '#373F41',
footerBackgroundColor: 'black', footerBackgroundColor: 'black',
footerTextColor: 'white', footerTextColor: 'white',
deactivatedColor: '#C9C9C9'
}, },
palette: { palette: {
primary: { primary: {
main: '#3293ED', main: '#01579B',
dark: '#1565c0' dark: '#1565c0',
light: '#A4C8E4',
}, },
}, },
components: { components: {
......
import uuid import uuid
from datetime import date from datetime import date
from typing import Optional
from dirtyfields import DirtyFieldsMixin from dirtyfields import DirtyFieldsMixin
from django.conf import settings from django.conf import settings
...@@ -63,6 +64,18 @@ class Person(BaseModel): ...@@ -63,6 +64,18 @@ class Person(BaseModel):
self.last_name, self.last_name,
) )
@property
def private_email(self) -> Optional["Identity"]:
"""The user provided private email address."""
return self.identities.filter(type=Identity.IdentityType.PRIVATE_EMAIL).first()
@property
def private_mobile(self) -> Optional["Identity"]:
"""The user provided private mobile number."""
return self.identities.filter(
type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER
).first()
@property @property
def is_registered(self) -> bool: def is_registered(self) -> bool:
""" """
...@@ -257,10 +270,19 @@ class Identity(BaseModel): ...@@ -257,10 +270,19 @@ class Identity(BaseModel):
) )
verified_at = models.DateTimeField(null=True) verified_at = models.DateTimeField(null=True)
def __str__(self):
return "{}(id={!r}, type={!r}, value={!r})".format(
self.__class__.__name__,
self.pk,
self.type,
self.value,
)
def __repr__(self): def __repr__(self):
return "{}(id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_at={!r})".format( return "{}(id={!r}, person_id={!r}, type={!r}, source={!r}, value={!r}, verified_by={!r}, verified_at={!r})".format(
self.__class__.__name__, self.__class__.__name__,
self.pk, self.pk,
self.person_id,
self.type, self.type,
self.source, self.source,
self.value, self.value,
......
...@@ -5,5 +5,5 @@ import pytest ...@@ -5,5 +5,5 @@ import pytest
def test_identity_repr(person_foo_verified): def test_identity_repr(person_foo_verified):
assert ( assert (
repr(person_foo_verified) repr(person_foo_verified)
== "Identity(id=3, type='passport_number', source='Test', value='12345', verified_by=Sponsor(id=1, feide_id='guy@example.org', first_name='Sponsor', last_name='Guy'), verified_at=datetime.datetime(2021, 6, 15, 12, 34, 56, tzinfo=<UTC>))" == "Identity(id=3, person_id=1, type='passport_number', source='Test', value='12345', verified_by=Sponsor(id=1, feide_id='guy@example.org', first_name='Sponsor', last_name='Guy'), verified_at=datetime.datetime(2021, 6, 15, 12, 34, 56, tzinfo=<UTC>))"
) )
from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from greg.models import Identity, Person from greg.models import Identity, Person
class GuestRegisterSerializer(serializers.ModelSerializer): class GuestRegisterSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(required=True)
last_name = serializers.CharField(required=True)
email = serializers.CharField(required=True) email = serializers.CharField(required=True)
mobile_phone = serializers.CharField(required=True)
def create(self, validated_data): def update(self, instance, validated_data):
# TODO: this serializer is untested
email = validated_data.pop("email") email = validated_data.pop("email")
with transaction.atomic(): mobile_phone = validated_data.pop("mobile_phone")
person = super().create(**validated_data)
if not instance.private_email:
Identity.objects.create( Identity.objects.create(
person=person, person=instance,
type=Identity.IdentityType.PRIVATE_EMAIL, type=Identity.IdentityType.PRIVATE_EMAIL,
value=email, value=email,
) )
return person else:
instance.private_email.value = email
instance.private_email.save()
if not instance.private_mobile:
Identity.objects.create(
person=instance,
type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER,
value=mobile_phone,
)
else:
instance.private_mobile.value = mobile_phone
instance.private_mobile.save()
# TODO: we only want to allow changing the name if we don't have one
# from a reliable source (Feide/KORR)
instance.first_name = validated_data["first_name"]
instance.last_name = validated_data["last_name"]
return instance
class Meta: class Meta:
model = Person model = Person
fields = ("id", "first_name", "last_name", "email") fields = ("id", "first_name", "last_name", "email", "mobile_phone")
read_only_fields = ("id",) read_only_fields = ("id",)
extra_kwargs = {
"first_name": {"required": True},
"last_name": {"required": True},
}
...@@ -51,6 +51,3 @@ class InviteGuestSerializer(serializers.ModelSerializer): ...@@ -51,6 +51,3 @@ class InviteGuestSerializer(serializers.ModelSerializer):
"uuid", "uuid",
) )
read_only_field = ("uuid",) read_only_field = ("uuid",)
foo = InviteGuestSerializer()
...@@ -8,7 +8,7 @@ from django.http.response import JsonResponse ...@@ -8,7 +8,7 @@ from django.http.response import JsonResponse
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers, status from rest_framework import serializers, status
from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView, GenericAPIView
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
...@@ -64,6 +64,8 @@ class CreateInvitationView(CreateAPIView): ...@@ -64,6 +64,8 @@ class CreateInvitationView(CreateAPIView):
data=request.data, context={"request": request} data=request.data, context={"request": request}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
# TODO: check that sponsor has access to OU
person = serializer.save() person = serializer.save()
invitationlink = InvitationLink.objects.filter( invitationlink = InvitationLink.objects.filter(
...@@ -99,7 +101,7 @@ class CheckInvitationView(APIView): ...@@ -99,7 +101,7 @@ class CheckInvitationView(APIView):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class InvitedGuestView(APIView): class InvitedGuestView(GenericAPIView):
authentication_classes = [SessionAuthentication, BasicAuthentication] authentication_classes = [SessionAuthentication, BasicAuthentication]
permission_classes = [AllowAny] permission_classes = [AllowAny]
parser_classes = [JSONParser] parser_classes = [JSONParser]
...@@ -140,8 +142,8 @@ class InvitedGuestView(APIView): ...@@ -140,8 +142,8 @@ class InvitedGuestView(APIView):
"person": { "person": {
"first_name": person.first_name, "first_name": person.first_name,
"last_name": person.last_name, "last_name": person.last_name,
"email": person.email, "email": person.private_email and person.private_email.value,
"mobile_phone": person.mobile_phone, "mobile_phone": person.private_mobile and person.private_mobile.value,
"fnr": fnr, "fnr": fnr,
"passport": passport, "passport": passport,
}, },
...@@ -172,22 +174,20 @@ class InvitedGuestView(APIView): ...@@ -172,22 +174,20 @@ class InvitedGuestView(APIView):
invite_id = request.session.get("invite_id") invite_id = request.session.get("invite_id")
data = request.data data = request.data
# Ensure the invitation link is valid and not expired
try:
invite_link = InvitationLink.objects.get(uuid=invite_id)
except (InvitationLink.DoesNotExist, exceptions.ValidationError):
return Response(status=status.HTTP_403_FORBIDDEN)
if invite_link.expire <= timezone.now():
return Response(status=status.HTTP_403_FORBIDDEN)
person = invite_link.invitation.role.person
with transaction.atomic(): with transaction.atomic():
# Ensure the invitation link is valid and not expired serializer = self.get_serializer(instance=person, data=request.data)
try: serializer.is_valid(raise_exception=True)
invite_link = InvitationLink.objects.get(uuid=invite_id) person = serializer.save()
except (InvitationLink.DoesNotExist, exceptions.ValidationError):
return Response(status=status.HTTP_403_FORBIDDEN)
if invite_link.expire <= timezone.now():
return Response(status=status.HTTP_403_FORBIDDEN)
# Get objects to update
person = invite_link.invitation.role.person
# Update with input from the guest
mobile = data.get("mobile_phone")
if mobile:
person.mobile_phone = data["mobile_phone"]
# Mark guest interaction done # Mark guest interaction done
person.registration_completed_date = timezone.now().date() person.registration_completed_date = timezone.now().date()
...@@ -197,4 +197,4 @@ class InvitedGuestView(APIView): ...@@ -197,4 +197,4 @@ class InvitedGuestView(APIView):
invite_link.expire = timezone.now() invite_link.expire = timezone.now()
invite_link.save() invite_link.save()
# TODO: Send an email to the sponsor? # TODO: Send an email to the sponsor?
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_200_OK)
...@@ -43,7 +43,9 @@ def test_get_invited_info_no_session(client, invitation_link): ...@@ -43,7 +43,9 @@ def test_get_invited_info_no_session(client, invitation_link):
@pytest.mark.django_db @pytest.mark.django_db
def test_get_invited_info_session_okay(client, invitation_link): def test_get_invited_info_session_okay(
client, invitation_link, person_foo_data, sponsor_guy_data, role_type_foo, unit_foo
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
...@@ -52,20 +54,39 @@ def test_get_invited_info_session_okay(client, invitation_link): ...@@ -52,20 +54,39 @@ def test_get_invited_info_session_okay(client, invitation_link):
response = client.get(reverse("gregui-v1:invited-info")) response = client.get(reverse("gregui-v1:invited-info"))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
data = response.json() data = response.json()
assert data.get("person") assert data.get("person") == dict(
assert data.get("sponsor") **person_foo_data,
assert data.get("role") email=None,
mobile_phone=None,
fnr=None,
passport=None,
)
assert data.get("sponsor") == dict(
first_name=sponsor_guy_data["first_name"],
last_name=sponsor_guy_data["last_name"],
)
assert data.get("role") == dict(
start=None,
end="2050-10-15",
comments="",
ou_name_en=unit_foo.name_en,
ou_name_nb=unit_foo.name_nb,
role_name_en=role_type_foo.name_en,
role_name_nb=role_type_foo.name_nb,
)
@pytest.mark.django_db @pytest.mark.django_db
def test_get_invited_info_expired_link(client, invitation_link): def test_get_invited_info_expired_link(
client, invitation_link, invitation_expired_date
):
# Get a session while link is valid # Get a session while link is valid
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
# Set expire link to expire long ago # Set expire link to expire long ago
invlink = InvitationLink.objects.get(uuid=invitation_link.uuid) invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
invlink.expire = "1970-01-01" invlink.expire = invitation_expired_date
invlink.save() invlink.save()
# Make a get request that should fail because invite expired after login, but # Make a get request that should fail because invite expired after login, but
# before get to userinfo # before get to userinfo
...@@ -74,45 +95,46 @@ def test_get_invited_info_expired_link(client, invitation_link): ...@@ -74,45 +95,46 @@ def test_get_invited_info_expired_link(client, invitation_link):
@pytest.mark.django_db @pytest.mark.django_db
def test_post_invited_info_ok_mobile_update(client: APIClient, invitation_link): def test_invited_guest_can_post_information(
client: APIClient, invitation_link, person_foo_data
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
person = invitation_link.invitation.role.person
assert person.private_mobile is None
# post updated info to confirm from guest # post updated info to confirm from guest
new_phone = "12345678" new_email = "private@example.org"
post_data = {"mobile_phone": new_phone} new_phone = "+4712345678"
data = dict(email=new_email, mobile_phone=new_phone, **person_foo_data)
response = client.post( response = client.post(
reverse("gregui-v1:invited-info"), reverse("gregui-v1:invited-info"),
post_data, data,
format="json", format="json",
) )
assert response.status_code == status.HTTP_201_CREATED print(response.content)
# Check that the object was updated in the database assert response.status_code == status.HTTP_200_OK
person = Person.objects.get(id=invitation_link.invitation.role.person.id)
assert person.mobile_phone == new_phone
@pytest.mark.django_db # Check that the object was updated in the database
def test_post_invited_info_ok(client, invitation_link): assert Person.objects.count() == 1
# get a session assert person.private_email.value == new_email
client.get( assert person.private_mobile.value == new_phone
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
)
# post updated info to confirm from guest
response = client.post(reverse("gregui-v1:invited-info"))
assert response.status_code == status.HTTP_201_CREATED
@pytest.mark.django_db @pytest.mark.django_db
def test_post_invited_info_expired_session(client, invitation_link): def test_post_invited_info_expired_session(
client, invitation_link, invitation_expired_date
):
# get a session # get a session
client.get( client.get(
reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid}) reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
) )
# Set expire link to expire long ago # Set expire link to expire long ago
invlink = InvitationLink.objects.get(uuid=invitation_link.uuid) invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
invlink.expire = "1970-01-01" invlink.expire = invitation_expired_date
invlink.save() invlink.save()
# post updated info to confirm from guest, should fail because of expired # post updated info to confirm from guest, should fail because of expired
# invitation link # invitation link
......
import datetime
import logging import logging
import pytest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.timezone import make_aware
from rest_framework.authtoken.admin import User from rest_framework.authtoken.admin import User
from rest_framework.test import APIClient from rest_framework.test import APIClient
import pytest
from greg.models import ( from greg.models import (
Invitation, Invitation,
...@@ -30,21 +34,28 @@ def client() -> APIClient: ...@@ -30,21 +34,28 @@ def client() -> APIClient:
@pytest.fixture @pytest.fixture
def unit_foo() -> OrganizationalUnit: def unit_foo() -> OrganizationalUnit:
ou = OrganizationalUnit.objects.create(orgreg_id="12345", name_en="foo_unit") ou = OrganizationalUnit.objects.create(
orgreg_id="12345", name_en="Foo EN", name_nb="Foo NB"
)
return OrganizationalUnit.objects.get(id=ou.id) return OrganizationalUnit.objects.get(id=ou.id)
@pytest.fixture @pytest.fixture
def role_type_foo() -> RoleType: def role_type_foo() -> RoleType:
rt = RoleType.objects.create(identifier="role_foo", name_en="Role Foo") rt = RoleType.objects.create(
identifier="role_foo", name_en="Role Foo EN", name_nb="Role Foo NB"
)
return RoleType.objects.get(id=rt.id) return RoleType.objects.get(id=rt.id)
@pytest.fixture @pytest.fixture
def sponsor_guy(unit_foo: OrganizationalUnit) -> Sponsor: def sponsor_guy_data() -> dict:
sponsor = Sponsor.objects.create( return dict(feide_id="guy@example.org", first_name="Sponsor", last_name="Guy")
feide_id="guy@example.org", first_name="Sponsor", last_name="Guy"
)
@pytest.fixture
def sponsor_guy(unit_foo: OrganizationalUnit, sponsor_guy_data) -> Sponsor:
sponsor = Sponsor.objects.create(**sponsor_guy_data)
sponsor.units.add(unit_foo, through_defaults={"hierarchical_access": False}) sponsor.units.add(unit_foo, through_defaults={"hierarchical_access": False})
return Sponsor.objects.get(id=sponsor.id) return Sponsor.objects.get(id=sponsor.id)
...@@ -67,8 +78,16 @@ def user_sponsor(sponsor_guy: Sponsor) -> User: ...@@ -67,8 +78,16 @@ def user_sponsor(sponsor_guy: Sponsor) -> User:
@pytest.fixture @pytest.fixture
def person() -> Person: def person_foo_data() -> dict:
pe = Person.objects.create() return dict(
first_name="Foo",
last_name="Bar",
)
@pytest.fixture
def person(person_foo_data) -> Person:
pe = Person.objects.create(**person_foo_data)
return Person.objects.get(id=pe.id) return Person.objects.get(id=pe.id)
...@@ -91,12 +110,26 @@ def invitation(role) -> Invitation: ...@@ -91,12 +110,26 @@ def invitation(role) -> Invitation:
@pytest.fixture @pytest.fixture
def invitation_link(invitation) -> InvitationLink: def invitation_valid_date() -> datetime.datetime:
il = InvitationLink.objects.create(invitation=invitation, expire="2060-10-15") return make_aware(datetime.datetime(2060, 10, 15))
@pytest.fixture
def invitation_expired_date() -> datetime.datetime:
return make_aware(datetime.datetime(1970, 1, 1))
@pytest.fixture
def invitation_link(invitation, invitation_valid_date) -> InvitationLink:
il = InvitationLink.objects.create(
invitation=invitation, expire=invitation_valid_date
)
return InvitationLink.objects.get(id=il.id) return InvitationLink.objects.get(id=il.id)
@pytest.fixture @pytest.fixture
def invitation_link_expired(invitation) -> InvitationLink: def invitation_link_expired(invitation, invitation_expired_date) -> InvitationLink:
il = InvitationLink.objects.create(invitation=invitation, expire="1970-01-01") il = InvitationLink.objects.create(
invitation=invitation, expire=invitation_expired_date
)
return InvitationLink.objects.get(id=il.id) return InvitationLink.objects.get(id=il.id)