Skip to content
Snippets Groups Projects
Commit ef3aec75 authored by Tore.Brede's avatar Tore.Brede
Browse files

Merge branch 'GREG-100_cancel_invitation' into 'master'

GREG-100: Cancel invitation

See merge request !129
parents dd4e3a5b 7f8bb82a
No related branches found
No related tags found
1 merge request!129GREG-100: Cancel invitation
Pipeline #98929 passed
......@@ -48,7 +48,6 @@
"registerText": "Please search for e-mail or phone number before registering a new guest to prevent duplicates.",
"registerButtonText": "Register new guest"
},
"loading": "Loading...",
"termsHeader": "Terms",
"staging": "Staging",
......@@ -94,7 +93,8 @@
"next": "Next",
"save": "Save",
"cancel": "Cancel",
"backToFrontPage": "Go to front page"
"backToFrontPage": "Go to front page",
"cancelInvitation": "Cancel"
},
"registerWizardText": {
"registerPage": "Enter the contact information for the guest below. All fields are mandatory.",
......
......@@ -93,7 +93,8 @@
"next": "Neste",
"save": "Lagre",
"cancel": "Avbryt",
"backToFrontPage": "Tilbake til forsiden"
"backToFrontPage": "Tilbake til forsiden",
"cancelInvitation": "Kanseller"
},
"registerWizardText": {
"registerPage": "Fyll inn kontaktinformasjonen til gjesten under. Alle feltene er obligatoriske.",
......
......@@ -94,7 +94,8 @@
"next": "Neste",
"save": "Lagre",
"cancel": "Avbryt",
"backToFrontPage": "Tilbake til forsida"
"backToFrontPage": "Tilbake til forsida",
"cancelInvitation": "Kanseller"
},
"registerWizardText": {
"registerPage": "Fyll inn kontaktinformasjonen til gjesten under. Alle feltene er obligatoriske.",
......
import { useState } from 'react'
import React, { useState } from 'react'
import {
Table,
TableBody,
......@@ -23,15 +23,25 @@ import SponsorGuestButtons from '../../components/sponsorGuestButtons'
interface GuestProps {
persons: Guest[]
// eslint-disable-next-line react/no-unused-prop-types
cancelCallback?: (roleId: string) => void
}
interface PersonLineProps {
person: Guest
role: Role
showStatusColumn?: boolean
displayCancel?: boolean
cancelCallback?: (roleId: string) => void
}
const PersonLine = ({ person, role, showStatusColumn }: PersonLineProps) => {
const PersonLine = ({
person,
role,
showStatusColumn,
displayCancel,
cancelCallback,
}: PersonLineProps) => {
const [t, i18n] = useTranslation(['common'])
return (
......@@ -65,13 +75,27 @@ const PersonLine = ({ person, role, showStatusColumn }: PersonLineProps) => {
{i18n.language === 'en' ? role.ou_en : role.ou_nb}
</TableCell>
<TableCell align="left">
<Button
variant="contained"
component={Link}
to={`/sponsor/guest/${person.pid}`}
>
{t('common:details')}
</Button>
{displayCancel ? (
<Button
data-testid="button-invite-cancel"
sx={{ color: 'theme.palette.secondary', mr: 1 }}
onClick={() => {
if (cancelCallback) {
cancelCallback(role.id)
}
}}
>
{t('common:button.cancel')}
</Button>
) : (
<Button
variant="contained"
component={Link}
to={`/sponsor/guest/${person.pid}`}
>
{t('common:details')}
</Button>
)}
</TableCell>
</TableRow>
)
......@@ -79,9 +103,15 @@ const PersonLine = ({ person, role, showStatusColumn }: PersonLineProps) => {
PersonLine.defaultProps = {
showStatusColumn: false,
displayCancel: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cancelCallback: (roleId: number) => {},
}
const WaitingForGuestRegistration = ({ persons }: GuestProps) => {
const WaitingForGuestRegistration = ({
persons,
cancelCallback,
}: GuestProps) => {
const [activeExpanded, setActiveExpanded] = useState(false)
// Show guests that have not responded to the invite yet
......@@ -119,7 +149,12 @@ const WaitingForGuestRegistration = ({ persons }: GuestProps) => {
guests.map((person) =>
person.roles ? (
person.roles.map((role) => (
<PersonLine role={role} person={person} />
<PersonLine
role={role}
person={person}
displayCancel
cancelCallback={cancelCallback}
/>
))
) : (
<></>
......@@ -142,6 +177,11 @@ const WaitingForGuestRegistration = ({ persons }: GuestProps) => {
)
}
WaitingForGuestRegistration.defaultProps = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cancelCallback: (roleId: number) => {},
}
const ActiveGuests = ({ persons }: GuestProps) => {
const [activeExpanded, setActiveExpanded] = useState(false)
......@@ -209,6 +249,11 @@ const ActiveGuests = ({ persons }: GuestProps) => {
)
}
ActiveGuests.defaultProps = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cancelCallback: (roleId: number) => {},
}
const WaitingGuests = ({ persons }: GuestProps) => {
const [waitingExpanded, setWaitingExpanded] = useState(false)
......@@ -271,15 +316,24 @@ const WaitingGuests = ({ persons }: GuestProps) => {
)
}
WaitingGuests.defaultProps = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cancelCallback: (roleId: number) => {},
}
interface FrontPageProps {
guests: Guest[]
cancelRole: (roleId: string) => void
}
function FrontPage({ guests }: FrontPageProps) {
function FrontPage({ guests, cancelRole }: FrontPageProps) {
return (
<Page>
<SponsorGuestButtons yourGuestsActive />
<WaitingForGuestRegistration persons={guests} />
<WaitingForGuestRegistration
persons={guests}
cancelCallback={cancelRole}
/>
<WaitingGuests persons={guests} />
<ActiveGuests persons={guests} />
</Page>
......
......@@ -3,7 +3,7 @@ import { Route } from 'react-router-dom'
import FrontPage from 'routes/sponsor/frontpage'
import { FetchedGuest, Guest } from 'interfaces'
import { parseRole } from 'utils'
import { parseRole, submitJsonOpts } from 'utils'
import GuestRoutes from './guest'
function Sponsor() {
......@@ -37,6 +37,29 @@ function Sponsor() {
}
}
const cancelRole = (roleId: string) => {
// There is no body for this request, but using submitJsonOpts still to
// set the CSRF-token
fetch(`/api/ui/v1/invite/?role_id=${roleId}`, submitJsonOpts('DELETE', {}))
.then((res) => {
if (!res.ok) {
return null
}
return res.text()
})
.then((result) => {
if (result !== null) {
// The invitation has been removed. Just reload all the data form the server to get updated data.
// The guests state will be updated by getGuestsInfo and this will trigger a rerender
getGuestsInfo()
}
})
.catch((error) => {
// TODO User should get some feedback telling him something failed
console.log('error', error)
})
}
useEffect(() => {
getGuestsInfo()
}, [])
......@@ -47,7 +70,7 @@ function Sponsor() {
<GuestRoutes />
</Route>
<Route exact path="/sponsor">
<FrontPage guests={guests} />
<FrontPage guests={guests} cancelRole={cancelRole} />
</Route>
</>
)
......
......@@ -4,7 +4,7 @@ from rest_framework.routers import DefaultRouter
from gregui.api.views.invitation import (
CheckInvitationView,
CreateInvitationView,
InvitationView,
InvitedGuestView,
)
from gregui.api.views.person import PersonSearchView, PersonView
......@@ -21,7 +21,7 @@ urlpatterns += [
re_path(r"units/$", UnitsViewSet.as_view(), name="units"),
path("invited/", InvitedGuestView.as_view(), name="invited-info"),
path("invited/<uuid>", CheckInvitationView.as_view(), name="invite-verify"),
path("invite/", CreateInvitationView.as_view(), name="invite-create"),
path("invite/", InvitationView.as_view(), name="invitation"),
path("person/<int:id>", PersonView.as_view(), name="person-get"),
path(
"person/search/<searchstring>", PersonSearchView.as_view(), name="person-search"
......
......@@ -7,7 +7,7 @@ from django.http.response import JsonResponse
from django.utils import timezone
from rest_framework import status
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.generics import CreateAPIView, GenericAPIView
from rest_framework.generics import CreateAPIView, GenericAPIView, DestroyAPIView
from rest_framework.parsers import JSONParser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
......@@ -20,9 +20,9 @@ from gregui.api.serializers.invitation import InviteGuestSerializer
from gregui.models import GregUserProfile
class CreateInvitationView(CreateAPIView):
class InvitationView(CreateAPIView, DestroyAPIView):
"""
Invitation creation endpoint
Endpoint for invitation creation and cancelling
{
......@@ -72,6 +72,26 @@ class CreateInvitationView(CreateAPIView):
print(invitationlink)
return Response(status=status.HTTP_201_CREATED)
def delete(self, request, *args, **kwargs) -> Response:
role_id = request.query_params["role_id"]
invitationlink = InvitationLink.objects.get(invitation__role_id=int(role_id))
# TODO Determine if person should be deleted as well
if invitationlink:
if (
invitationlink.invitation.role.person.is_registered
or invitationlink.invitation.role.person.is_verified
):
# The guest has already gone through the registration step. The guest should
# not be verified, but including that check just in case here
return Response(status.HTTP_400_BAD_REQUEST)
# Delete the role, the cascading will cause all the invitation links connected
# to it to be removed as well
invitationlink.invitation.role.delete()
return Response(status=status.HTTP_200_OK)
class CheckInvitationView(APIView):
authentication_classes = []
......
......@@ -4,8 +4,8 @@ from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory, force_authenticate
from greg.models import Identity, Person
from gregui.api.views.invitation import CreateInvitationView
from greg.models import Identity, Person, Role, Invitation, InvitationLink
from gregui.api.views.invitation import InvitationView
@pytest.mark.django_db
......@@ -25,7 +25,7 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo):
"type": role_type_foo.id,
},
}
url = reverse("gregui-v1:invite-create")
url = reverse("gregui-v1:invitation")
all_persons = Person.objects.all()
assert len(all_persons) == 0
......@@ -34,7 +34,7 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo):
request = factory.post(url, data, format="json")
force_authenticate(request, user=user_sponsor)
view = CreateInvitationView.as_view()
view = InvitationView.as_view()
response = view(request)
assert response.status_code == status.HTTP_201_CREATED
......@@ -49,3 +49,23 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo):
type=Identity.IdentityType.PRIVATE_EMAIL,
value="test@example.com",
).exists()
@pytest.mark.django_db
def test_invite_cancel(
client, invitation_link, invitation, role, log_in, sponsor_guy, user_sponsor
):
# TODO: Should all sponsors be allowed to delete arbitrary invitations?
log_in(user_sponsor)
url = reverse("gregui-v1:invitation")
# Check that the role is there
role = Role.objects.get(id=role.id)
response = client.delete("%s?role_id=%s" % (url, str(role.id)))
assert response.status_code == status.HTTP_200_OK
# The role, invitation and connected links should now have been removed
assert Role.objects.filter(id=role.id).count() == 0
assert Invitation.objects.filter(id=invitation.id).count() == 0
assert InvitationLink.objects.filter(invitation__id=invitation.id).count() == 0
......@@ -9,7 +9,6 @@ from django.utils.timezone import make_aware
from rest_framework.authtoken.admin import User
from rest_framework.test import APIClient
from greg.models import (
Invitation,
InvitationLink,
......@@ -26,6 +25,7 @@ from gregui.models import GregUserProfile
# see https://github.com/joke2k/faker/issues/753
logging.getLogger("faker").setLevel(logging.ERROR)
# OIDC stuff
@pytest.fixture
def claims():
......@@ -246,9 +246,8 @@ def greg_sponsors(data):
@pytest.fixture
def log_in(client, greg_users):
def _log_in(username):
user = greg_users[username]
def log_in(client):
def _log_in(user):
client.force_login(user=user)
# It seems like the session was not updated automatically this way
session = client.session
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment