diff --git a/README.md b/README.md index a6babf2d45d8b8fb38759ff195aecb6f07ba523c..b9b8f371b6c9c7021a05801e0c3b8572979ff899 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Greg + [](https://git.app.uib.no/it-bott-integrasjoner/greg/-/commits/master) [](https://git.app.uib.no/it-bott-integrasjoner/greg/-/commits/master) @@ -50,6 +51,13 @@ Use pytest with the pytest-django library to run unit tests. pytest +There are two scripts for adding data to the database: + +- greg/tests/populate_fixtures.py +- greg/tests/populate_database.py + +where the former uses randomized data, and the latter uses specific data useful in combination with the frontend. See the respective files for how to use them. + ## Static type analysis Use [mypy](http://mypy-lang.org/) to run static type checks using type hints. diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 4d6d0bf2af6583d41c2c630476eb13148598dab0..ede4d0c0abe71ae84669845f6c1df1dddcce0049 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -24,7 +24,8 @@ "mobilePhone": "Mobile phone", "passportNumber": "Passport number", "passportNationality": "Passport nationality", - "countryCallingCode": "Country code" + "countryCallingCode": "Country code", + "contactPersonUnit": "Contact at unit" }, "sponsor": { "addRole": "Add role", @@ -86,7 +87,9 @@ "invalidMobilePhoneNumber": "Invalid phone number", "invalidEmail": "Invalid e-mail address", "passportNumberRequired": "Passport number required", - "mobilePhoneRequired": "Mobile phone is required" + "mobilePhoneRequired": "Mobile phone is required", + "startDateMustBeSet": "Start date must be set", + "startDateMustBeBeforeEndDate": "Start date has to be before end date" }, "button": { "back": "Back", diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 0054273107f3395a1ece25ff3c06bfe755eb3d49..d9e825659c3f14bac22fd9adf4041152a426a3ec 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -24,7 +24,8 @@ "mobilePhone": "Mobilnummer", "passportNumber": "Passnummer", "passportNationality": "Passnasjonalitet", - "countryCallingCode": "Landkode" + "countryCallingCode": "Landkode", + "contactPersonUnit": "Kontakt ved avdeling" }, "sponsor": { "addRole": "Legg til rolle", @@ -86,7 +87,9 @@ "invalidMobilePhoneNumber": "Ugyldig telefonnummer", "invalidEmail": "Ugyldig e-postadresse", "passportNumberRequired": "Passnummer er obligatorisk", - "mobilePhoneRequired": "Mobilnummer er obligatorisk" + "mobilePhoneRequired": "Mobilnummer er obligatorisk", + "startDateMustBeSet": "Startdato må være satt", + "startDateMustBeBeforeEndDate": "Startdato må være før sluttdato" }, "button": { "back": "Tilbake", diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index cc1da0d414515fe44ab177ed74a0eaae6415a69b..36c4ce1e4dd26a18b37b0c06500a5b0ed2184efa 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -25,7 +25,8 @@ "mobilePhone": "Mobilnummer", "passportNumber": "Passnummer", "passportNationality": "Passnasjonalitet", - "countryCallingCode": "Landkode" + "countryCallingCode": "Landkode", + "contactPersonUnit": "Kontakt ved avdeling" }, "sponsor": { "addRole": "Legg til role", @@ -87,7 +88,9 @@ "invalidMobilePhoneNumber": "Ugyldig telefonnummer", "invalidEmail": "Ugyldig e-postadresse", "passportNumberRequired": "Passnummer er obligatorisk", - "mobilePhoneRequired": "Mobilnummer er obligatorisk" + "mobilePhoneRequired": "Mobilnummer er obligatorisk", + "startDateMustBeSet": "Startdato må vere satt", + "startDateMustBeBeforeEndDate": "Startdato må vere før sluttdato" }, "button": { "back": "Tilbake", diff --git a/frontend/src/routes/sponsor/frontpage/index.tsx b/frontend/src/routes/sponsor/frontpage/index.tsx index 0f35c19a0716f54aa3603b6304b9ca4bc383f858..3c9ecdee9af823f2a4c185192a248c944e7694e1 100644 --- a/frontend/src/routes/sponsor/frontpage/index.tsx +++ b/frontend/src/routes/sponsor/frontpage/index.tsx @@ -18,7 +18,8 @@ import Page from 'components/page' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { Guest, Role } from 'interfaces' -import format from 'date-fns/format' +import { isBefore, format } from 'date-fns' + import SponsorGuestButtons from '../../components/sponsorGuestButtons' interface GuestProps { @@ -33,7 +34,8 @@ interface PersonLineProps { const PersonLine = ({ person, role, showStatusColumn }: PersonLineProps) => { const [t, i18n] = useTranslation(['common']) - + const today = new Date() + today.setHours(0, 0, 0, 0) return ( <TableRow key={`${person.first} ${person.last}`} @@ -47,7 +49,7 @@ const PersonLine = ({ person, role, showStatusColumn }: PersonLineProps) => { </TableCell> {showStatusColumn && - (person.active ? ( + (!isBefore(role.end_date, today) ? ( <TableCell sx={{ color: 'green' }} align="left"> {t('common:active')} </TableCell> diff --git a/frontend/src/routes/sponsor/register/formData.ts b/frontend/src/routes/sponsor/register/formData.ts index 5e518b30d580c96186d66215ae12c4c6a77d3cda..8e96d7a625f1da93f7e34360939c4ccb0f267cb9 100644 --- a/frontend/src/routes/sponsor/register/formData.ts +++ b/frontend/src/routes/sponsor/register/formData.ts @@ -4,6 +4,7 @@ export type RegisterFormData = { role_type?: string role_start?: Date role_end?: Date + contact_person_unit?: string comment?: string ou_id?: number email?: string diff --git a/frontend/src/routes/sponsor/register/stepPersonForm.tsx b/frontend/src/routes/sponsor/register/stepPersonForm.tsx index bbfd2431a8f3b5a200e865dd451bd7f203a08fa4..c13e7b5e541a58e145e2c1db3dee6e70cc220e69 100644 --- a/frontend/src/routes/sponsor/register/stepPersonForm.tsx +++ b/frontend/src/routes/sponsor/register/stepPersonForm.tsx @@ -62,6 +62,7 @@ const StepPersonForm = forwardRef( formState: { errors }, reset, setValue, + getValues, } = useForm<RegisterFormData>() const onSubmit = handleSubmit(submit) @@ -93,6 +94,19 @@ const StepPersonForm = forwardRef( useImperativeHandle(ref, () => ({ doSubmit })) + const validateStartDateBeforeEndDate = (startDate: Date | undefined) => { + if (!startDate) { + return t('validation.startDateMustBeSet') + } + + const roleEnd = getValues('role_end') + if (roleEnd && startDate > roleEnd) { + // The role end date is set, but is is before the start date + return t('validation.startDateMustBeBeforeEndDate') + } + return true + } + return ( <> <Typography @@ -185,9 +199,11 @@ const StepPersonForm = forwardRef( ))} </TextField> + {/* There are no particular constraints on the date pickers. It should be allowed to add a role with a start date that is in the past for instance */} <Controller name="role_start" control={control} + rules={{ validate: validateStartDateBeforeEndDate }} render={({ field }) => ( <DatePicker mask="____-__-__" @@ -201,7 +217,11 @@ const StepPersonForm = forwardRef( /> )} /> - + {!!errors.role_start && ( + <Box sx={{ typography: 'caption', color: 'red' }}> + {errors.role_start.message} + </Box> + )} <Controller name="role_end" control={control} @@ -219,6 +239,12 @@ const StepPersonForm = forwardRef( )} /> + <TextField + id="contact_person" + label={t('input.contactPersonUnit')} + {...register(`contact_person_unit`)} + /> + <TextField id="comment" label={t('input.comment')} diff --git a/frontend/src/routes/sponsor/register/stepRegistration.tsx b/frontend/src/routes/sponsor/register/stepRegistration.tsx index 8e4bd716e21d118d6f5c2dfdafad4f01ffe38319..36ba38bb127bcd61b14376ad435d4076febd99f3 100644 --- a/frontend/src/routes/sponsor/register/stepRegistration.tsx +++ b/frontend/src/routes/sponsor/register/stepRegistration.tsx @@ -70,6 +70,7 @@ export default function StepRegistration() { formData.role_end === null ? null : format(formData.role_end as Date, 'yyyy-MM-dd'), + contact_person_unit: formData.contact_person_unit, comments: formData.comment, orgunit: formData.ou_id, }, diff --git a/greg/management/commands/import_from_orgreg.py b/greg/management/commands/import_from_orgreg.py index 732a68206ba4bb8599e3de7c7cc4d8972cff9367..2411ed04921d25484d88af2c451043c80b262504 100644 --- a/greg/management/commands/import_from_orgreg.py +++ b/greg/management/commands/import_from_orgreg.py @@ -28,6 +28,75 @@ class Command(BaseCommand): help = __doc__ processed: Dict[int, OrganizationalUnit] = {} + def _upsert_extra_identities( + self, + ou: OrgUnit, + ): + """Upsert any configured extra IDs from orgreg.""" + for extra_id in settings.ORGREG_EXTRA_IDS: + + matching_ids = [ + x + for x in ou.external_keys + if x.source_system == extra_id["source"] and x.type == extra_id["type"] + ] + + if not matching_ids: + logger.warning( + "No %s id from %s found in OrgReg for ou %s", + extra_id["type"], + extra_id["source"], + ou.ou_id, + ) + continue + + if len(matching_ids) > 1: + # External_ids + logger.warning( + "Found multiple ids matching type: %s source: %s in OrgReg. Using the first one: %s", + extra_id["type"], + extra_id["source"], + matching_ids[0].value, + ) + + identity_in_orgreg = matching_ids[0] + + # Check if the id exists + identify_in_db = ( + self.processed[ou.ou_id] + .identifiers.filter( + source=extra_id["source"], + name=extra_id["type"], + ) + .first() + ) + + if identify_in_db: + if identify_in_db.value != identity_in_orgreg.value: + logger.info( + "Updating id: source: %s, type: %s, old_id: %s, new_id %s", + extra_id["source"], + extra_id["type"], + identify_in_db.value, + identity_in_orgreg.value, + ) + identify_in_db.value = identity_in_orgreg.value + identify_in_db.save() + else: + OuIdentifier.objects.create( + name=extra_id["type"], + source=extra_id["source"], + value=identity_in_orgreg["value"], + orgunit=self.processed[ou.ou_id], + ) + logger.info( + "Added new id to ou: %s, type: %s, source: %s value: %s", + ou.ou_id, + extra_id["type"], + extra_id["source"], + identity_in_orgreg["value"], + ) + def _get_or_create_and_set_values( self, ou: OrgUnit, values: Mapping[str, Union[str, int, bool]] ): @@ -50,6 +119,7 @@ class Command(BaseCommand): created = False for k, v in values.items(): setattr(self.processed[ou.ou_id], k, v) + self._upsert_extra_identities(ou) self.processed[ou.ou_id].save() logger.info( "%s %s with %s", diff --git a/greg/models.py b/greg/models.py index 8948adb47f0195d473717e8eb342eb21d88d2a02..58ed1c2e3ca065b9bc8bc9dbc0aaa19e8366838a 100644 --- a/greg/models.py +++ b/greg/models.py @@ -83,6 +83,14 @@ class Person(BaseModel): type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER ).first() + @property + def passport(self) -> Optional["Identity"]: + """The person's passport if they have one registered. + This property was introduced to make updating of passport easier when the guest registers himself.""" + return self.identities.filter( + type=Identity.IdentityType.PASSPORT_NUMBER + ).first() + @property def is_registered(self) -> bool: """ diff --git a/greg/tests/populate_fixtures.py b/greg/tests/populate_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..d7e03f4ed11a58a702c93944b76be8d384b513c7 --- /dev/null +++ b/greg/tests/populate_fixtures.py @@ -0,0 +1,343 @@ +""" +Adds a few more specific models for testing purposes. Alternative to the similiar +script for adding random data to various fields. + +WARNING: This script removes all entries in most tables. Do not execute it unless you +are absolutely certain you know what you are doing. + +There are 4 guests: + - One has been invited and not done anything yet + - One has followed the invite link and provided a passport number, and is waiting for + the sponsor to act + - One has had their passport number confirmed by the sponsor and the role is active + - One like the previous but the role has ended + +There is an OU tree with 4 OUs on three levels, with respective OUIdentifiers. +There is an invitation object related to each guest with InvitationLinks that are +expired for all but they invited guest that has not responded. + +There are Consent Types, and the active guests have consented to the mandatory one, but +one of them has denied the other one. + +""" + +import datetime +import logging + +from django.db import connection +from django.conf import settings +from django.utils import timezone + +from greg.models import ( + Consent, + ConsentType, + Identity, + Invitation, + InvitationLink, + OrganizationalUnit, + OuIdentifier, + Person, + Role, + RoleType, + Sponsor, + SponsorOrganizationalUnit, +) + + +ROLE_TYPE_EXT_SCI = "extsci" +ROLE_TYPE_EMERITUS = "emeritus" +TESTDATA_SOURCE = "testsource" +SPONSOR_FEIDEID = "sponsor@feide.no" +OU_EUROPE_NAME_EN = "Europe" +CONSENT_IDENT_MANDATORY = "mandatory" +CONSENT_IDENT_OPTIONAL = "optional" + +logger = logging.getLogger(__name__) + + +class DatabasePopulation: + """ + Helper class for populating database with specific data + + Run the file in the Django shell: exec(open('greg/tests/populate_fixtures.py').read()) + """ + + def truncate_tables(self): + logger.info("truncating tables...") + with connection.cursor() as cursor: + for table in ( + "greg_consent", + "greg_consenttype", + "greg_notification", + "greg_identity", + "greg_invitationlink", + "greg_invitation", + "greg_role", + "greg_sponsororganizationalunit", + "greg_roletype", + "greg_ouidentifier", + "greg_organizationalunit", + "greg_person", + "gregui_greguserprofile", + "greg_sponsor", + ): + logging.info("purging table %s", table) + cursor.execute(f"DELETE FROM {table}") + logger.info("...tables purged") + + def _add_consenttypes(self): + ConsentType.objects.create( + identifier=CONSENT_IDENT_MANDATORY, + name_en="Mandatory consent type", + name_nb="Påkrevd samtykketype", + user_allowed_to_change=False, + mandatory=True, + ) + ConsentType.objects.create( + identifier=CONSENT_IDENT_OPTIONAL, + name_en="Optional consent type", + name_nb="Valgfri samtykketype", + user_allowed_to_change=False, + ) + + def _add_ous_with_identifiers(self): + """ + Create a simple tree + + earth - america + - europe - norway + """ + earth = OrganizationalUnit.objects.create( + name_nb="Universitetet i Jorden", name_en="University of Earth" + ) + OuIdentifier.objects.create( + name=settings.ORGREG_NAME, + source=settings.ORGREG_SOURCE, + value="2", + orgunit=earth, + ) + europe = OrganizationalUnit.objects.create( + name_nb="Europa", name_en=OU_EUROPE_NAME_EN, parent=earth + ) + OuIdentifier.objects.create( + name=settings.ORGREG_NAME, + source=settings.ORGREG_SOURCE, + value="3", + orgunit=europe, + ) + america = OrganizationalUnit.objects.create( + name_nb="Amerika", name_en="America", parent=earth + ) + OuIdentifier.objects.create( + name=settings.ORGREG_NAME, + source=settings.ORGREG_SOURCE, + value="4", + orgunit=america, + ) + norway = OrganizationalUnit.objects.create( + name_nb="Norge", name_en="Norway", parent=europe + ) + OuIdentifier.objects.create( + name=settings.ORGREG_NAME, + source=settings.ORGREG_SOURCE, + value="5", + orgunit=norway, + ) + + def _add_roletypes(self): + RoleType.objects.create( + identifier=ROLE_TYPE_EXT_SCI, + name_nb="Gjesteforsker", + name_en="Guest researcher", + description_nb="Gjesteforsker som ikke skal ha lønn", + description_en="Guest reasearcher without payment", + ) + RoleType.objects.create( + identifier=ROLE_TYPE_EMERITUS, + name_nb="Emeritus", + name_en="Emeritus", + description_nb="Emeritus", + description_en="Emeritus", + max_days=700, + ) + + def _add_sponsors(self): + """Add a sponsor connected to the Europe unit""" + sam = Sponsor.objects.create( + feide_id=SPONSOR_FEIDEID, first_name="Sam", last_name="Sponsorson" + ) + SponsorOrganizationalUnit.objects.create( + sponsor=sam, + organizational_unit=OrganizationalUnit.objects.get( + name_en=OU_EUROPE_NAME_EN + ), + hierarchical_access=False, + ) + + def _add_invited_person(self): + """Person that has been invited and has not followed their invite""" + iggy = Person.objects.create(first_name="Iggy", last_name="Invited") + role = Role.objects.create( + person=iggy, + type=RoleType.objects.get(identifier=ROLE_TYPE_EXT_SCI), + orgunit=OrganizationalUnit.objects.get(name_en=OU_EUROPE_NAME_EN), + start_date=datetime.date.today() + datetime.timedelta(days=2), + end_date=datetime.date.today() + datetime.timedelta(days=100), + sponsor=Sponsor.objects.get(feide_id=SPONSOR_FEIDEID), + ) + invitation = Invitation.objects.create( + role=role, + ) + InvitationLink.objects.create( + invitation=invitation, + expire=timezone.now() + datetime.timedelta(days=30), + ) + + def _add_waiting_person(self): + """ + A person with an active role but missing a verified identity of type national + id or passport. + """ + walter = Person.objects.create( + first_name="Walter", + last_name="Waiting", + registration_completed_date=datetime.date.today() + - datetime.timedelta(days=10), + ) + role = Role.objects.create( + person=walter, + type=RoleType.objects.get(identifier=ROLE_TYPE_EXT_SCI), + orgunit=OrganizationalUnit.objects.get(name_en=OU_EUROPE_NAME_EN), + start_date=datetime.date.today() - datetime.timedelta(days=30), + end_date=datetime.date.today() + datetime.timedelta(days=100), + sponsor=Sponsor.objects.get(feide_id=SPONSOR_FEIDEID), + ) + Identity.objects.create( + person=walter, + type=Identity.IdentityType.PASSPORT_NUMBER, + source=TESTDATA_SOURCE, + value="SE-123456789", + ) + invitation = Invitation.objects.create( + role=role, + ) + InvitationLink.objects.create( + invitation=invitation, + expire=timezone.now() - datetime.timedelta(days=35), + ) + + def _add_active_person(self): + """ + A person with an active role and a verified identity of type national id or + passport. + """ + adam = Person.objects.create( + first_name="Adam", + last_name="Active", + registration_completed_date=datetime.date.today() + - datetime.timedelta(days=10), + ) + role = Role.objects.create( + person=adam, + type=RoleType.objects.get(identifier=ROLE_TYPE_EXT_SCI), + orgunit=OrganizationalUnit.objects.get(name_en=OU_EUROPE_NAME_EN), + start_date=datetime.date.today() - datetime.timedelta(days=30), + end_date=datetime.date.today() + datetime.timedelta(days=100), + sponsor=Sponsor.objects.get(feide_id=SPONSOR_FEIDEID), + ) + Identity.objects.create( + person=adam, + type=Identity.IdentityType.PASSPORT_NUMBER, + source=TESTDATA_SOURCE, + value="NO-123456789", + verified_at=timezone.now() - datetime.timedelta(days=31), + ) + Identity.objects.create( + person=adam, + type=Identity.IdentityType.PRIVATE_MOBILE_NUMBER, + source=TESTDATA_SOURCE, + value="+4792492412", + verified_at=timezone.now() - datetime.timedelta(days=205), + ) + invitation = Invitation.objects.create( + role=role, + ) + InvitationLink.objects.create( + invitation=invitation, + expire=timezone.now() - datetime.timedelta(days=32), + ) + Consent.objects.create( + person=adam, + type=ConsentType.objects.get(identifier=CONSENT_IDENT_MANDATORY), + consent_given_at=datetime.date.today() - datetime.timedelta(days=10), + ) + Consent.objects.create( + person=adam, + type=ConsentType.objects.get(identifier=CONSENT_IDENT_OPTIONAL), + consent_given_at=datetime.date.today() - datetime.timedelta(days=10), + ) + + def _add_expired_person(self): + """ + A person with an inactive role, and a verified identity of type national id or + passport. + """ + esther = Person.objects.create( + first_name="Esther", + last_name="Expired", + registration_completed_date=timezone.now() - datetime.timedelta(days=206), + ) + role = Role.objects.create( + person=esther, + type=RoleType.objects.get(identifier=ROLE_TYPE_EXT_SCI), + orgunit=OrganizationalUnit.objects.get(name_en=OU_EUROPE_NAME_EN), + start_date=datetime.date.today() - datetime.timedelta(days=200), + end_date=datetime.date.today() - datetime.timedelta(days=100), + sponsor=Sponsor.objects.get(feide_id=SPONSOR_FEIDEID), + ) + Identity.objects.create( + person=esther, + type=Identity.IdentityType.PASSPORT_NUMBER, + source=TESTDATA_SOURCE, + value="DK-123456789", + verified_at=timezone.now() - datetime.timedelta(days=205), + ) + Identity.objects.create( + person=esther, + type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER, + source=TESTDATA_SOURCE, + value="12345678901", + verified_at=timezone.now() - datetime.timedelta(days=205), + ) + invitation = Invitation.objects.create( + role=role, + ) + InvitationLink.objects.create( + invitation=invitation, + expire=timezone.now() - datetime.timedelta(days=204), + ) + Consent.objects.create( + person=esther, + type=ConsentType.objects.get(identifier=CONSENT_IDENT_MANDATORY), + consent_given_at=datetime.date.today() - datetime.timedelta(days=206), + ) + + def populate_database(self): + logger.info("populating db...") + # Add the types, sponsors and ous + self._add_consenttypes() + self._add_ous_with_identifiers() + self._add_roletypes() + self._add_sponsors() + # Add the four guests + self._add_active_person() + self._add_waiting_person() + self._add_invited_person() + self._add_expired_person() + logger.info("...done populating db") + + +if __name__ == "__main__": + database_population = DatabasePopulation() + database_population.truncate_tables() + database_population.populate_database() diff --git a/gregsite/settings/base.py b/gregsite/settings/base.py index 7a618734d1a449a891d4e8078810d3064a4500e1..81e06942dd58b9fc1a355ea0bfeabf8f25148220 100644 --- a/gregsite/settings/base.py +++ b/gregsite/settings/base.py @@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ from pathlib import Path -from typing import List, Literal +from typing import List # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -268,6 +268,21 @@ INTERNAL_RK_PREFIX = "no.{instance}.greg".format(instance=INSTANCE_NAME) FEIDE_SOURCE = "feide" +# Rate limit settings of invite endpoint +REST_FRAMEWORK = { + "DEFAULT_THROTTLE_CLASSES": [ + "rest_framework.throttling.AnonRateThrottle", + ], + "DEFAULT_THROTTLE_RATES": { + "anon": "100/day", + }, +} + + # Used by the OU import from orgreg to distinguish the OuIdentifiers from others ORGREG_SOURCE = "orgreg" ORGREG_NAME = "orgreg_id" + +# Extra ids to be imported from orgreg, list of dict with source/type. +# [{"source": "sapuio", "type": "legacy_stedkode"}] +ORGREG_EXTRA_IDS = [] diff --git a/gregsite/settings/dev.py b/gregsite/settings/dev.py index 0d50da5f288ab1fec646eaf68bb821e4008f9363..5cd39430464c501ad8e133dbc9581f634d02752e 100644 --- a/gregsite/settings/dev.py +++ b/gregsite/settings/dev.py @@ -45,6 +45,14 @@ SESSION_COOKIE_SAMESITE = "Lax" SESSION_COOKIE_AGE = 1209600 # two weeks for easy development +# Disable throttling in development, uncomment CACHES to test +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } +} + + try: from .local import * except ImportError: diff --git a/gregsite/settings/prod.py b/gregsite/settings/prod.py index fddaf490faf58b375a52ffea9e40ed0deb7b229d..0336f7bc6af9dd96381348351c2053dc900549c7 100644 --- a/gregsite/settings/prod.py +++ b/gregsite/settings/prod.py @@ -27,6 +27,13 @@ ALLOWED_HOSTS = ( else [] ) +# This is the default values for CACHES, only present for clarity +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } +} + try: from .local import * diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py index 3ae182f4ca405ed2eaf77743e4845ea997b47e1a..18d18158e3e8b42d26732ef6fc4b96013e3a5b2a 100644 --- a/gregui/api/serializers/guest.py +++ b/gregui/api/serializers/guest.py @@ -20,6 +20,7 @@ class GuestRegisterSerializer(serializers.ModelSerializer): fnr = serializers.CharField( required=False, validators=[validate_norwegian_national_id_number] ) + passport = serializers.CharField(required=False) def update(self, instance, validated_data): mobile_phone = validated_data.pop("mobile_phone") @@ -41,6 +42,19 @@ class GuestRegisterSerializer(serializers.ModelSerializer): Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER, fnr, instance ) + if "passport" in validated_data: + passport = validated_data.pop("passport") + if not instance.passport: + Identity.objects.create( + person=instance, + type=Identity.IdentityType.PASSPORT_NUMBER, + value=passport, + ) + else: + passport_existing = instance.passport + passport_existing.value = passport + passport_existing.save() + # TODO: we only want to allow changing the name if we don't have one # from a reliable source (Feide/KORR) # TODO Comment back in after it is decided if name updates are allowed @@ -51,7 +65,15 @@ class GuestRegisterSerializer(serializers.ModelSerializer): class Meta: model = Person - fields = ("id", "first_name", "last_name", "email", "mobile_phone", "fnr") + fields = ( + "id", + "first_name", + "last_name", + "email", + "mobile_phone", + "fnr", + "passport", + ) read_only_fields = ("id",) diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 17f37f6096878fc2f918f5b529b11561441221cb..bd8bc50a946e7ce8d83cb17abf63bec23e564c53 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -13,6 +13,7 @@ from rest_framework.mixins import UpdateModelMixin from rest_framework.parsers import JSONParser from rest_framework.permissions import AllowAny from rest_framework.response import Response +from rest_framework.throttling import AnonRateThrottle from rest_framework.views import APIView from greg.models import Identity, InvitationLink, Person @@ -103,6 +104,7 @@ class InvitationView(CreateAPIView, DestroyAPIView): class CheckInvitationView(APIView): authentication_classes = [] permission_classes = [AllowAny] + throttle_classes = [AnonRateThrottle] def post(self, request, *args, **kwargs): """ diff --git a/gregui/tests/api/test_invitation.py b/gregui/tests/api/test_invitation.py index 99b6adc0b8048978a388dbec518bc834f5f1841d..95da05946e5c5781f405d396dac5537b224fb7e3 100644 --- a/gregui/tests/api/test_invitation.py +++ b/gregui/tests/api/test_invitation.py @@ -220,3 +220,32 @@ def test_email_update(client, invitation_link, person_foo_data, person): person.private_email.refresh_from_db() assert person.private_email.value == "test2@example.com" + + +@pytest.mark.django_db +def test_register_passport(client, invitation_link, person_foo_data, person): + passport_information = "EN-123456789" + data = {"person": {"mobile_phone": "+4797543992", "passport": passport_information}} + url = reverse("gregui-v1:invited-info") + + session = client.session + session["invite_id"] = str(invitation_link.uuid) + session.save() + + assert ( + Identity.objects.filter( + person__id=person.id, type=Identity.IdentityType.PASSPORT_NUMBER + ).count() + == 0 + ) + + response = client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + person.refresh_from_db() + + registered_passport = Identity.objects.filter( + person__id=person.id, type=Identity.IdentityType.PASSPORT_NUMBER + ).get() + + assert registered_passport.value == passport_information diff --git a/gregui/tests/api/test_invite_guest.py b/gregui/tests/api/test_invite_guest.py index 9447535e82e55055db4bb0b9211e82ef900b5cd0..f2e7e13a3cbbe82dd3cf0fe1600f7c1fc96c8112 100644 --- a/gregui/tests/api/test_invite_guest.py +++ b/gregui/tests/api/test_invite_guest.py @@ -14,28 +14,30 @@ from gregui.api.views.invitation import InvitationView def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker): # Mock function to avoid exception because there are no e-mail templates in the database mocker.patch("gregui.api.views.invitation.send_invite_mail") + + test_comment = "This is a test comment" + contact_person_unit = "This is a test contact person" + role_start_date = datetime.datetime.today() + datetime.timedelta(days=1) + role_end_date = datetime.datetime.today() + datetime.timedelta(days=10) + data = { "first_name": "Foo", "last_name": "Bar", "email": "test@example.com", "role": { - "start_date": ( - datetime.datetime.today() + datetime.timedelta(days=1) - ).strftime("%Y-%m-%d"), - "end_date": ( - datetime.datetime.today() + datetime.timedelta(days=10) - ).strftime("%Y-%m-%d"), + "start_date": (role_start_date).strftime("%Y-%m-%d"), + "end_date": (role_end_date).strftime("%Y-%m-%d"), "orgunit": unit_foo.id, "type": role_type_foo.id, + "comments": test_comment, + "contact_person_unit": contact_person_unit, }, } url = reverse("gregui-v1:invitation") - all_persons = Person.objects.all() - assert len(all_persons) == 0 + assert len(Person.objects.all()) == 0 - factory = APIRequestFactory() - request = factory.post(url, data, format="json") + request = APIRequestFactory().post(url, data, format="json") force_authenticate(request, user=user_sponsor) view = InvitationView.as_view() @@ -54,6 +56,14 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker): value="test@example.com", ).exists() + role = Role.objects.filter(person__id=person.id).get() + assert role.orgunit == unit_foo + assert role.type == role_type_foo + assert role.start_date == role_start_date.date() + assert role.end_date == role_end_date.date() + assert role.contact_person_unit == contact_person_unit + assert role.comments == test_comment + @pytest.mark.django_db def test_invite_cancel(