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 (7)
...@@ -121,7 +121,8 @@ ...@@ -121,7 +121,8 @@
"phoneNumberAndCountryCode": "Both country code and phone number must be set", "phoneNumberAndCountryCode": "Both country code and phone number must be set",
"passportNationalityAndNumber": "Both passport nationality and number need to be set", "passportNationalityAndNumber": "Both passport nationality and number need to be set",
"doubleIdentity": "*You have input both national ID and passport. Please choose only one.", "doubleIdentity": "*You have input both national ID and passport. Please choose only one.",
"nationalIdOrPassport": "National ID or passport information need to be entered" "nationalIdOrPassport": "National ID or passport information need to be entered",
"genderIsRequired": "Gender is required"
}, },
"button": { "button": {
"yes": "Yes", "yes": "Yes",
......
...@@ -121,7 +121,8 @@ ...@@ -121,7 +121,8 @@
"phoneNumberAndCountryCode": "Både landkode og telefonnummer må være satt", "phoneNumberAndCountryCode": "Både landkode og telefonnummer må være satt",
"passportNationalityAndNumber": "Både passnasjonalitet og nummer må være satt", "passportNationalityAndNumber": "Både passnasjonalitet og nummer må være satt",
"doubleIdentity": "*Du har fylt inn begge feltene over. Bruk kun ett av de.", "doubleIdentity": "*Du har fylt inn begge feltene over. Bruk kun ett av de.",
"nationalIdOrPassport": "Fødselsnummer/D-nummer eller passinformasjon må spesifiseres" "nationalIdOrPassport": "Fødselsnummer/D-nummer eller passinformasjon må spesifiseres",
"genderIsRequired": "Kjønn er obligatorisk"
}, },
"button": { "button": {
"yes": "Ja", "yes": "Ja",
......
...@@ -121,7 +121,8 @@ ...@@ -121,7 +121,8 @@
"phoneNumberAndCountryCode": "Både landkode og telefonnummer må vere satt", "phoneNumberAndCountryCode": "Både landkode og telefonnummer må vere satt",
"passportNationalityAndNumber": "Både passnasjonalitet og nummer må vere satt", "passportNationalityAndNumber": "Både passnasjonalitet og nummer må vere satt",
"doubleIdentity": "*Du har fylt inn begge felta over. Bruk berre eitt av dei.", "doubleIdentity": "*Du har fylt inn begge felta over. Bruk berre eitt av dei.",
"nationalIdOrPassport": "Fødselsnummer/D-nummer eller passinformasjon må spesifiserast" "nationalIdOrPassport": "Fødselsnummer/D-nummer eller passinformasjon må spesifiserast",
"genderIsRequired": "Kjønn er obligatorisk"
}, },
"button": { "button": {
"yes": "Ja", "yes": "Ja",
......
import React from 'react'
import { render, screen, waitFor } from 'test-utils' import { render, screen, waitFor } from 'test-utils'
import AdapterDateFns from '@mui/lab/AdapterDateFns' import AdapterDateFns from '@mui/lab/AdapterDateFns'
import { LocalizationProvider } from '@mui/lab' import { LocalizationProvider } from '@mui/lab'
import { addYears } from 'date-fns/fp'
import { FeatureContext } from 'contexts'
import GuestRegisterStep from './register' import GuestRegisterStep from './register'
import { GuestRegisterData } from '../enteredGuestData' import { GuestRegisterData } from '../enteredGuestData'
import AuthenticationMethod from '../authenticationMethod' import AuthenticationMethod from '../authenticationMethod'
...@@ -175,3 +176,115 @@ test('Identifier fields disabled and name fields enabled if invite is ID-porten' ...@@ -175,3 +176,115 @@ test('Identifier fields disabled and name fields enabled if invite is ID-porten'
expect(screen.queryByTestId('passport-nationality-input')).toBeEnabled() expect(screen.queryByTestId('passport-nationality-input')).toBeEnabled()
}) })
}) })
test('Gender required to be set if gender field is showing', async () => {
const nextHandler = (registerData: GuestRegisterData) => {
console.log(`Entered data: ${registerData}`)
}
const allFeaturesOn = {
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
showGenderFieldForGuest: true,
}
const formData: GuestRegisterData = {
firstName: 'Test',
lastName: 'Test2',
mobilePhoneCountry: 'NO',
mobilePhone: '97543980',
nationalIdNumber: '',
passportNumber: '12345678',
passportNationality: 'NO',
dateOfBirth: addYears(-20)(new Date()),
gender: null,
}
const testData = getEmptyGuestData()
// Need this to be able to call on the submit-method on the form
const reference = {
current: {
doSubmit: jest.fn(),
},
}
render(
<FeatureContext.Provider value={allFeaturesOn}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegisterStep
nextHandler={nextHandler}
initialGuestData={testData}
registerData={formData}
ref={reference}
/>
</LocalizationProvider>
</FeatureContext.Provider>
)
reference.current?.doSubmit()
// The validation should fail for gender since to value has been set
const validationMessage = await waitFor(() =>
screen.getByText('validation.genderIsRequired')
)
expect(validationMessage).toBeInTheDocument()
})
test('Gender not required to be set if gender field is not showing', async () => {
const nextHandler = (registerData: GuestRegisterData) => {
console.log(`Entered data: ${registerData}`)
}
const showGenderFieldOff = {
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
showGenderFieldForGuest: false,
}
const formData: GuestRegisterData = {
firstName: 'Test',
lastName: 'Test2',
mobilePhoneCountry: 'NO',
mobilePhone: '97543980',
nationalIdNumber: '',
passportNumber: '12345678',
passportNationality: 'NO',
dateOfBirth: addYears(-20)(new Date()),
gender: null,
}
const testData = getEmptyGuestData()
const reference = {
current: {
doSubmit: jest.fn(),
},
}
render(
<FeatureContext.Provider value={showGenderFieldOff}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<GuestRegisterStep
nextHandler={nextHandler}
initialGuestData={testData}
registerData={formData}
ref={reference}
/>
</LocalizationProvider>
</FeatureContext.Provider>
)
reference.current?.doSubmit()
await waitFor(
() => {
expect(
screen.queryByText('validation.genderIsRequired')
).not.toBeInTheDocument()
},
{
timeout: 5000,
}
)
})
...@@ -68,7 +68,7 @@ const GuestRegisterStep = forwardRef( ...@@ -68,7 +68,7 @@ const GuestRegisterStep = forwardRef(
string | undefined string | undefined
>(undefined) >(undefined)
// Set suggestion for gender field is a gender is not already given in the input // Set suggestion for the gender field if gender is not already given in the input
const [gender, setGender] = useState<string>( const [gender, setGender] = useState<string>(
!initialGuestData.gender || !initialGuestData.gender.trim() !initialGuestData.gender || !initialGuestData.gender.trim()
? extractGenderOrBlank(initialGuestData.fnr) ? extractGenderOrBlank(initialGuestData.fnr)
...@@ -77,6 +77,7 @@ const GuestRegisterStep = forwardRef( ...@@ -77,6 +77,7 @@ const GuestRegisterStep = forwardRef(
const [idErrorState, setIdErrorState] = useState<string>('') const [idErrorState, setIdErrorState] = useState<string>('')
const [phoneErrorState, setPhoneErrorState] = useState<string>('') const [phoneErrorState, setPhoneErrorState] = useState<string>('')
const [genderErrorState, setGenderErrorState] = useState<string>('')
const { displayContactAtUnitGuestInput } = useContext(FeatureContext) const { displayContactAtUnitGuestInput } = useContext(FeatureContext)
console.log('register step registerData', registerData) console.log('register step registerData', registerData)
...@@ -158,6 +159,12 @@ const GuestRegisterStep = forwardRef( ...@@ -158,6 +159,12 @@ const GuestRegisterStep = forwardRef(
} }
setIdErrorState('') setIdErrorState('')
if (showGenderFieldForGuest && !gender) {
setGenderErrorState(t('validation.genderIsRequired'))
return
}
setGenderErrorState('')
console.log('register submit errors', errors) console.log('register submit errors', errors)
if (!Object.keys(errors).length) { if (!Object.keys(errors).length) {
...@@ -369,31 +376,36 @@ const GuestRegisterStep = forwardRef( ...@@ -369,31 +376,36 @@ const GuestRegisterStep = forwardRef(
/> />
{showGenderFieldForGuest && ( {showGenderFieldForGuest && (
<Select <>
sx={{ <Select
maxHeight: '2.5rem', sx={{
minWidth: '5rem', maxHeight: '2.5rem',
marginRight: '0.5rem', minWidth: '5rem',
}} marginRight: '0.5rem',
labelId="gender-select" }}
id="gender-select-id" labelId="gender-select"
displayEmpty id="gender-select-id"
onChange={handleGenderChange} displayEmpty
value={gender} onChange={handleGenderChange}
renderValue={(selected: any) => { value={gender}
if (!selected) { renderValue={(selected: any) => {
return t('input.gender') if (!selected) {
} return t('input.gender')
return t(`input.${selected}`) }
}} return t(`input.${selected}`)
> }}
{/* Keep it simple and hardcode the gender values */} >
<MenuItem disabled value=""> {/* Keep it simple and hardcode the gender values */}
{t('input.gender')} <MenuItem disabled value="">
</MenuItem> {t('input.gender')}
<MenuItem value="male">{t('input.male')}</MenuItem> </MenuItem>
<MenuItem value="female">{t('input.female')}</MenuItem> <MenuItem value="male">{t('input.male')}</MenuItem>
</Select> <MenuItem value="female">{t('input.female')}</MenuItem>
</Select>
{genderErrorState && (
<Typography color="error">{genderErrorState}</Typography>
)}
</>
)} )}
<Controller <Controller
......
...@@ -10,13 +10,13 @@ from django.conf import settings ...@@ -10,13 +10,13 @@ from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend, UserModel from django.contrib.auth.backends import BaseBackend, UserModel
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.utils.timezone import make_aware from django.utils import timezone
from mozilla_django_oidc.auth import OIDCAuthenticationBackend from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from greg.models import Identity, Person, Sponsor, InvitationLink from greg.models import Identity, Person, Sponsor, InvitationLink
from gregui.models import GregUserProfile from gregui.models import GregUserProfile
from gregui.utils import name_diff from gregui.utils import name_diff_too_large
logger = structlog.getLogger(__name__) logger = structlog.getLogger(__name__)
...@@ -339,7 +339,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -339,7 +339,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
existing_nin.source = source existing_nin.source = source
existing_nin.value = new_nin existing_nin.value = new_nin
existing_nin.verified = Identity.Verified.AUTOMATIC existing_nin.verified = Identity.Verified.AUTOMATIC
existing_nin.verified_at = make_aware(datetime.datetime.now()) existing_nin.verified_at = timezone.make_aware(datetime.datetime.now())
existing_nin.save() existing_nin.save()
logger.info("identity_update", identity_id=existing_nin.id) logger.info("identity_update", identity_id=existing_nin.id)
...@@ -357,7 +357,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -357,7 +357,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER, type=Identity.IdentityType.NORWEGIAN_NATIONAL_ID_NUMBER,
value=new_nin, value=new_nin,
verified=Identity.Verified.AUTOMATIC, verified=Identity.Verified.AUTOMATIC,
verified_at=make_aware(datetime.datetime.now()), verified_at=timezone.make_aware(datetime.datetime.now()),
) )
nin_id.save() nin_id.save()
logger.info("identity_add", identity_id=nin_id.id) logger.info("identity_add", identity_id=nin_id.id)
...@@ -399,7 +399,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -399,7 +399,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
existing_feide_id.source = source existing_feide_id.source = source
existing_feide_id.value = new_feide_id existing_feide_id.value = new_feide_id
existing_feide_id.verified = Identity.Verified.AUTOMATIC existing_feide_id.verified = Identity.Verified.AUTOMATIC
existing_feide_id.verified_at = make_aware(datetime.datetime.now()) existing_feide_id.verified_at = timezone.make_aware(datetime.datetime.now())
existing_feide_id.save() existing_feide_id.save()
else: else:
...@@ -421,7 +421,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -421,7 +421,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
type=Identity.IdentityType.FEIDE_ID, type=Identity.IdentityType.FEIDE_ID,
value=new_feide_id, value=new_feide_id,
verified=Identity.Verified.AUTOMATIC, verified=Identity.Verified.AUTOMATIC,
verified_at=make_aware(datetime.datetime.now()), verified_at=timezone.make_aware(datetime.datetime.now()),
) )
feide_id.save() feide_id.save()
logger.info("identity_add", identity_id=feide_id.id, feide_id=new_feide_id) logger.info("identity_add", identity_id=feide_id.id, feide_id=new_feide_id)
...@@ -445,7 +445,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -445,7 +445,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
old_email = existing_email.value old_email = existing_email.value
existing_email.value = new_email existing_email.value = new_email
existing_email.verified = Identity.Verified.AUTOMATIC existing_email.verified = Identity.Verified.AUTOMATIC
existing_email.verified_at = make_aware(datetime.datetime.now()) existing_email.verified_at = timezone.make_aware(datetime.datetime.now())
existing_email.save() existing_email.save()
logger.info( logger.info(
"identity_update", "identity_update",
...@@ -472,19 +472,19 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -472,19 +472,19 @@ class GregOIDCBackend(ValidatingOIDCBackend):
type=Identity.IdentityType.FEIDE_EMAIL, type=Identity.IdentityType.FEIDE_EMAIL,
value=new_email, value=new_email,
verified=Identity.Verified.AUTOMATIC, verified=Identity.Verified.AUTOMATIC,
verified_at=make_aware(datetime.datetime.now()), verified_at=timezone.make_aware(datetime.datetime.now()),
) )
email_id.save() email_id.save()
logger.info("identity_add", identity_id=email_id.id, value=new_email) logger.info("identity_add", identity_id=email_id.id, value=new_email)
def _update_person_name( def _update_person_name(
self, person: Person, first_name: str, last_name: str, source: str self, person: Person, first_name: str, last_name: str
) -> None: ) -> None:
new_combined_name = f"{first_name} {last_name}" new_combined_name = f"{first_name} {last_name}"
existing_combined_name = f"{person.first_name} {person.last_name}" existing_combined_name = f"{person.first_name} {person.last_name}"
if name_diff(existing_combined_name, new_combined_name) > 4: if name_diff_too_large(existing_combined_name, new_combined_name, 4):
logger.warning( logger.warning(
"name_diff_to_large", "name_diff_to_large",
existing_name=existing_combined_name, existing_name=existing_combined_name,
...@@ -518,9 +518,7 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -518,9 +518,7 @@ class GregOIDCBackend(ValidatingOIDCBackend):
self._update_person_feide_id(person, userinfo["userid_feide"], "feide") self._update_person_feide_id(person, userinfo["userid_feide"], "feide")
if "email" in userinfo: if "email" in userinfo:
self._update_person_email(person, userinfo["email"], "feide") self._update_person_email(person, userinfo["email"], "feide")
self._update_person_name( self._update_person_name(person, userinfo["first_name"], userinfo["last_name"])
person, userinfo["first_name"], userinfo["last_name"], "feide"
)
def _update_sponsor_from_userinfo(self, userinfo: dict, sponsor: Sponsor) -> None: def _update_sponsor_from_userinfo(self, userinfo: dict, sponsor: Sponsor) -> None:
"""Updates a sponsor object with data from OIDC.""" """Updates a sponsor object with data from OIDC."""
...@@ -578,26 +576,19 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -578,26 +576,19 @@ class GregOIDCBackend(ValidatingOIDCBackend):
def _get_or_create_greg_user_profile(self, userinfo: dict, user: UserModel): def _get_or_create_greg_user_profile(self, userinfo: dict, user: UserModel):
"""Get or create a GregUserProfile.""" """Get or create a GregUserProfile."""
def _get_invite_person( def _get_invitation_link(session) -> Optional[InvitationLink]:
session, old_person: Optional[Person]
) -> Optional[Person]:
try: try:
invitation_link = InvitationLink.objects.get(uuid=session["invite_id"]) return InvitationLink.objects.get(uuid=session["invite_id"])
except InvitationLink.DoesNotExist: except InvitationLink.DoesNotExist:
logger.error("invite_not_found", invite_id=session["invite_id"]) logger.error("invite_not_found", invite_id=session["invite_id"])
logger.bind(authenticating_invite=False) logger.bind(authenticating_invite=False)
return old_person return None
def _get_invite_person(invitation_link) -> Person:
logger.bind(authenticating_invite=True) logger.bind(authenticating_invite=True)
role = invitation_link.invitation.role role = invitation_link.invitation.role
invite_person = role.person invite_person = role.person
logger.info("invite_found", invite_person=invite_person.id) logger.info("invite_found", invite_person=invite_person.id)
if old_person and not old_person == invite_person:
role.person = old_person
role.save()
invite_person.delete()
return old_person
return invite_person return invite_person
logger.bind(user=user) logger.bind(user=user)
...@@ -618,7 +609,49 @@ class GregOIDCBackend(ValidatingOIDCBackend): ...@@ -618,7 +609,49 @@ class GregOIDCBackend(ValidatingOIDCBackend):
old_person = None old_person = None
if user_profile and user_profile.person: if user_profile and user_profile.person:
old_person = self._get_person_from_userinfo(userinfo) old_person = self._get_person_from_userinfo(userinfo)
person = _get_invite_person(session, old_person) person = old_person
invitation_link = _get_invitation_link(session)
if invitation_link:
inv_person = _get_invite_person(invitation_link)
if old_person and old_person != inv_person:
# Logged in person has a person object in greg but has followed an
# invite.
inv_name = f"{inv_person.first_name} {inv_person.last_name}"
old_name = f"{old_person.first_name} {old_person.last_name}"
if (
not inv_person.registration_completed_date
and not name_diff_too_large(old_name, inv_name, 4)
):
logger.info(
"Invitation (%s) opened by existing person (%s)."
" Giving role to them and deleting invited person (%s)",
invitation_link.invitation.id,
old_person.id,
inv_person.id,
)
# The name is close and the invited person has not completed
# registration. Give the role to the existing person, and
# delete the invited one.
role = invitation_link.invitation.role
role.person = old_person
role.save()
inv_person.delete()
else:
# The logged in user has gotten someone else's invitation, and
# the invitation should be disabled.
logger.warning(
"Illegal person accessed invitation (%s). Invitation belongs to"
" person id %s, but was access by person id %s. Expiring"
" InvitationLink %s.",
invitation_link.invitation.id,
inv_person.id,
old_person.id,
invitation_link.id,
)
invitation_link.expire = timezone.now()
invitation_link.save()
else:
person = inv_person
else: else:
person = self._get_person_from_userinfo(userinfo) person = self._get_person_from_userinfo(userinfo)
logger.bind(authenticating_invite=False) logger.bind(authenticating_invite=False)
......
...@@ -21,7 +21,7 @@ def test_hierarchy_access_sponsor_ous(client, user_sponsor, unit_bar, log_in): ...@@ -21,7 +21,7 @@ def test_hierarchy_access_sponsor_ous(client, user_sponsor, unit_bar, log_in):
# Only direct connection visible # Only direct connection visible
response = client.get(url) response = client.get(url)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.json() == [{"id": 1, "en": "Foo EN", "nb": "Foo NB"}] assert len(response.json()) == 1
# Tree visible with hierarchy access # Tree visible with hierarchy access
sou = SponsorOrganizationalUnit.objects.get(pk=1) sou = SponsorOrganizationalUnit.objects.get(pk=1)
...@@ -29,7 +29,4 @@ def test_hierarchy_access_sponsor_ous(client, user_sponsor, unit_bar, log_in): ...@@ -29,7 +29,4 @@ def test_hierarchy_access_sponsor_ous(client, user_sponsor, unit_bar, log_in):
sou.save() sou.save()
response = client.get(url) response = client.get(url)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.json() == [ assert len(response.json()) == 2
{"id": 1, "en": "Foo EN", "nb": "Foo NB"},
{"id": 2, "en": "Bar EN", "nb": "Bar NB"},
]
from gregui.utils import name_diff, restricted_damarau_levenshtein from gregui.utils import name_diff, name_diff_too_large, restricted_damarau_levenshtein
def test_rdm_replace(): def test_rdm_replace():
...@@ -21,3 +21,9 @@ def test_name_longer(): ...@@ -21,3 +21,9 @@ def test_name_longer():
def test_name_threshold(): def test_name_threshold():
"""Verify continues with raised threshold""" """Verify continues with raised threshold"""
assert name_diff("Abcdefgh Ijklmnop", "Qrstuvw Xyz", 100) == 10 assert name_diff("Abcdefgh Ijklmnop", "Qrstuvw Xyz", 100) == 10
def test_name_too_large():
"""Verify boolean version responds with expected boolean"""
assert not name_diff_too_large("Test Name", "Testing Name", 3)
assert name_diff_too_large("Test Name", "Testing Name", 2)
...@@ -60,3 +60,7 @@ def name_diff(full_name1: str, full_name2: str, threshold: int = 2) -> int: ...@@ -60,3 +60,7 @@ def name_diff(full_name1: str, full_name2: str, threshold: int = 2) -> int:
if total_difference > threshold: if total_difference > threshold:
break break
return total_difference return total_difference
def name_diff_too_large(full_name1: str, full_name2: str, threshold: int) -> bool:
return name_diff(full_name1, full_name2, threshold) > threshold