Skip to content

Commit c4ed4ce

Browse files
authored
Implement Invitation letter generation (#4266)
1 parent 2617a8c commit c4ed4ce

File tree

82 files changed

+7802
-2979
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+7802
-2979
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,5 @@ badge-service/badges.zip
135135
backend/custom_admin/src/types.ts
136136
backend/schema.graphql
137137
backend/__pypackages__/
138+
backend/custom_admin/.astro/
139+
backend/custom_admin/core.*

Dockerfile.node.local

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ RUN npm install -g pnpm; \
1616

1717
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1
1818

19-
RUN pip3 install websockets --break-system-packages
19+
RUN pip3 install websockets==14.1 --break-system-packages
2020

2121
COPY . .

backend/Dockerfile

+38-126
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,48 @@
1-
ARG FUNCTION_DIR="/home/app/"
1+
# check=skip=SecretsUsedInArgOrEnv
2+
ARG FUNCTION_DIR="/home/app"
23

3-
FROM python:3.11-slim as build-stage
4+
FROM python:3.11-slim AS base
45

5-
ARG FUNCTION_DIR
6-
7-
RUN mkdir -p ${FUNCTION_DIR}
8-
WORKDIR ${FUNCTION_DIR}
6+
ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod \
7+
AWS_MEDIA_BUCKET=example \
8+
AWS_REGION_NAME=eu-central-1 \
9+
SECRET_KEY=DEMO \
10+
STRIPE_SECRET_API_KEY=demo \
11+
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
12+
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
13+
CELERY_BROKER_URL=demo \
14+
CELERY_RESULT_BACKEND=demo \
15+
HASHID_DEFAULT_SECRET_SALT=demo
916

1017
RUN apt-get update -y && apt-get install -y \
11-
gcc libpq-dev git \
12-
# Pillow
13-
libtiff5-dev libjpeg62 libopenjp2-7-dev zlib1g-dev \
14-
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
15-
libharfbuzz-dev libfribidi-dev libxcb1-dev libldap2-dev libldap-2.5-0 \
16-
ffmpeg libsm6 libxext6 libglib2.0-0
18+
# weasyprint
19+
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 \
20+
# postgres
21+
libpq-dev
1722

18-
ENV LIBRARY_PATH=/lib:/usr/lib
19-
20-
RUN pip install uv==0.5.5
21-
22-
ARG TARGETPLATFORM
23-
24-
ARG TARGETPLATFORM
25-
26-
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
27-
tar -czvf /libs.tar.gz \
28-
/usr/lib/aarch64-linux-gnu/libpq* \
29-
/usr/lib/aarch64-linux-gnu/libldap_r* \
30-
/usr/lib/aarch64-linux-gnu/libldap* \
31-
/usr/lib/aarch64-linux-gnu/liblber* \
32-
/usr/lib/aarch64-linux-gnu/libsasl* \
33-
/usr/lib/aarch64-linux-gnu/libxml2* \
34-
/usr/lib/aarch64-linux-gnu/libgcrypt* \
35-
/usr/lib/aarch64-linux-gnu/libstdc++* \
36-
/usr/lib/aarch64-linux-gnu/libjpeg* \
37-
/usr/lib/aarch64-linux-gnu/libopenjp2* \
38-
/usr/lib/aarch64-linux-gnu/libdeflate* \
39-
/usr/lib/aarch64-linux-gnu/libjbig* \
40-
/usr/lib/aarch64-linux-gnu/liblcms2* \
41-
/usr/lib/aarch64-linux-gnu/libwebp* \
42-
/usr/lib/aarch64-linux-gnu/libtiff* \
43-
/usr/lib/aarch64-linux-gnu/libGL* \
44-
/usr/lib/aarch64-linux-gnu/libgthread* \
45-
/usr/lib/aarch64-linux-gnu/libglib-* \
46-
/usr/lib/aarch64-linux-gnu/libX11* \
47-
/usr/lib/aarch64-linux-gnu/libxcb* \
48-
/usr/lib/aarch64-linux-gnu/libXau* \
49-
/usr/lib/aarch64-linux-gnu/libXdmcp* \
50-
/usr/lib/aarch64-linux-gnu/libXext* \
51-
/usr/lib/aarch64-linux-gnu/libbsd*; \
52-
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
53-
tar -czvf /libs.tar.gz \
54-
/usr/lib/x86_64-linux-gnu/libpq* \
55-
/usr/lib/x86_64-linux-gnu/libldap_r* \
56-
/usr/lib/x86_64-linux-gnu/libldap* \
57-
/usr/lib/x86_64-linux-gnu/liblber* \
58-
/usr/lib/x86_64-linux-gnu/libsasl* \
59-
/usr/lib/x86_64-linux-gnu/libxml2* \
60-
/usr/lib/x86_64-linux-gnu/libgcrypt* \
61-
/usr/lib/x86_64-linux-gnu/libstdc++* \
62-
/usr/lib/x86_64-linux-gnu/libjpeg* \
63-
/usr/lib/x86_64-linux-gnu/libopenjp2* \
64-
/usr/lib/x86_64-linux-gnu/libdeflate* \
65-
/usr/lib/x86_64-linux-gnu/libjbig* \
66-
/usr/lib/x86_64-linux-gnu/liblcms2* \
67-
/usr/lib/x86_64-linux-gnu/libwebp* \
68-
/usr/lib/x86_64-linux-gnu/libtiff* \
69-
/usr/lib/x86_64-linux-gnu/libGL* \
70-
/usr/lib/x86_64-linux-gnu/libgthread* \
71-
/usr/lib/x86_64-linux-gnu/libglib-* \
72-
/usr/lib/x86_64-linux-gnu/libX11* \
73-
/usr/lib/x86_64-linux-gnu/libxcb* \
74-
/usr/lib/x86_64-linux-gnu/libXau* \
75-
/usr/lib/x86_64-linux-gnu/libXdmcp* \
76-
/usr/lib/x86_64-linux-gnu/libXext* \
77-
/usr/lib/x86_64-linux-gnu/libbsd*; \
78-
fi
79-
80-
81-
COPY pyproject.toml uv.lock ${FUNCTION_DIR}
82-
83-
RUN uv sync --no-dev
84-
85-
# Create GraphQL schema
86-
87-
FROM python:3.11-slim as schema-stage
23+
FROM base AS build-stage
8824

8925
ARG FUNCTION_DIR
9026

27+
RUN apt-get update -y && apt-get install -y \
28+
gcc git
29+
30+
RUN mkdir -p ${FUNCTION_DIR}
9131
WORKDIR ${FUNCTION_DIR}
9232

93-
COPY --from=build-stage ${FUNCTION_DIR}/.venv ${FUNCTION_DIR}/.venv
94-
COPY --from=build-stage /usr/local/lib/*.so* /usr/local/lib/
95-
COPY --from=build-stage /libs.tar.gz /libs.tar.gz
33+
RUN pip install uv==0.5.5
9634

97-
RUN tar -xvf /libs.tar.gz -C / && rm /libs.tar.gz && ldconfig
35+
COPY pyproject.toml uv.lock ./
9836

99-
COPY . ${FUNCTION_DIR}
37+
RUN uv sync --no-dev
10038

101-
ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod
39+
COPY . ./
10240

103-
RUN AWS_MEDIA_BUCKET=example \
104-
AWS_REGION_NAME=eu-central-1 \
105-
SECRET_KEY=DEMO \
106-
STRIPE_SECRET_API_KEY=demo \
107-
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
108-
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
109-
CELERY_BROKER_URL=demo \
110-
CELERY_RESULT_BACKEND=demo \
111-
HASHID_DEFAULT_SECRET_SALT=demo \
112-
${FUNCTION_DIR}/.venv/bin/python manage.py graphql_schema
41+
RUN .venv/bin/python manage.py graphql_schema
11342

11443
# Build custom admin components
11544

116-
FROM node:18.17.1 as js-stage
45+
FROM node:23 AS js-stage
11746

11847
ARG FUNCTION_DIR
11948

@@ -125,52 +54,35 @@ COPY custom_admin/package.json custom_admin/pnpm-lock.yaml ./
12554

12655
RUN pnpm install
12756

128-
COPY custom_admin/ .
57+
COPY --from=build-stage ${FUNCTION_DIR}/schema.graphql schema.graphql
12958

130-
COPY --from=schema-stage ${FUNCTION_DIR}/schema.graphql schema.graphql
59+
COPY custom_admin/ .
13160

132-
RUN ADMIN_GRAPHQL_URL=schema.graphql pnpm codegen
133-
RUN pnpm build
61+
RUN ADMIN_GRAPHQL_URL=schema.graphql pnpm codegen && pnpm build
13462

135-
# Final stage
63+
# Runtime stage
13664

137-
FROM python:3.11-slim
65+
FROM base AS runtime-stage
13866

13967
ARG FUNCTION_DIR
14068

14169
WORKDIR ${FUNCTION_DIR}
14270

71+
ENV LIBRARY_PATH=/lib:/usr/lib LD_LIBRARY_PATH=/lib:/usr/lib
72+
14373
RUN apt-get update -y && apt-get install -y curl
14474

14575
RUN groupadd -r app && useradd -r -g app app && mkdir -p ${FUNCTION_DIR} && chown -R app:app ${FUNCTION_DIR}
14676

14777
COPY --chown=app:app --from=js-stage ${FUNCTION_DIR}/dist/*.html ${FUNCTION_DIR}/custom_admin/templates/astro/
14878
COPY --chown=app:app --from=js-stage ${FUNCTION_DIR}/dist/_astro ${FUNCTION_DIR}/custom_admin/static/_astro/
149-
15079
COPY --chown=app:app --from=build-stage ${FUNCTION_DIR}/.venv ${FUNCTION_DIR}/.venv
151-
COPY --from=build-stage /usr/local/lib/*.so* /usr/local/lib/
152-
COPY --from=build-stage /libs.tar.gz /libs.tar.gz
153-
154-
RUN tar -xvf /libs.tar.gz -C / && rm /libs.tar.gz && ldconfig
15580

15681
COPY --chown=app:app . ${FUNCTION_DIR}
15782

15883
USER app
15984

160-
RUN mkdir -p ${FUNCTION_DIR}/assets
161-
162-
ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod
163-
164-
RUN AWS_MEDIA_BUCKET=example \
165-
AWS_REGION_NAME=eu-central-1 \
166-
SECRET_KEY=DEMO \
167-
STRIPE_SECRET_API_KEY=demo \
168-
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
169-
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
170-
CELERY_BROKER_URL=demo \
171-
CELERY_RESULT_BACKEND=demo \
172-
HASHID_DEFAULT_SECRET_SALT=demo \
173-
${FUNCTION_DIR}/.venv/bin/python manage.py collectstatic --noinput
85+
RUN mkdir -p assets && .venv/bin/python manage.py collectstatic --noinput
17486

175-
ENTRYPOINT ["/home/app/.venv/bin/python", "-m", "awslambdaric"]
176-
CMD [ "wsgi_handler.handler" ]
87+
ENTRYPOINT ["/home/app/.venv/bin/gunicorn"]
88+
CMD [ "pycon.wsgi" ]

backend/api/schedule/tests/test_unassigned_schedule_items.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def _unassigned_schedule_items(client, **input):
1414
id
1515
}
1616
}""",
17-
variables={**input},
17+
variables=input,
1818
)
1919

2020

backend/api/schema.py

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from .association_membership.mutation import AssociationMembershipMutation
2727
from .cms.schema import CMSQuery
2828
from .sponsors.schema import SponsorsMutation
29+
from .visa.queries import VisaQuery
30+
from .visa.mutations import VisaMutation
2931

3032

3133
@strawberry.type
@@ -44,6 +46,7 @@ class Query(
4446
BadgeScannerQuery,
4547
UserQuery,
4648
CMSQuery,
49+
VisaQuery,
4750
):
4851
pass
4952

@@ -64,6 +67,7 @@ class Mutation(
6467
UsersMutations,
6568
AssociationMembershipMutation,
6669
SponsorsMutation,
70+
VisaMutation,
6771
):
6872
pass
6973

backend/api/types.py

+5
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,8 @@ def paginate_list(
141141
),
142142
items=items,
143143
)
144+
145+
146+
@strawberry.type
147+
class NotFound:
148+
message: str = "Not found"

backend/api/visa/mutations.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Annotated
2+
from api.context import Context
3+
from api.types import NotFound
4+
from custom_admin.audit import create_change_admin_log_entry
5+
from visa.models import InvitationLetterDocument as InvitationLetterDocumentModel
6+
from api.visa.permissions import CanEditInvitationLetterDocument
7+
from strawberry.tools import create_type
8+
from api.visa.types import InvitationLetterDocument
9+
import strawberry
10+
11+
12+
@strawberry.input
13+
class UpdateInvitationLetterDocumentPageInput:
14+
id: strawberry.ID
15+
title: str
16+
content: str
17+
18+
19+
@strawberry.input
20+
class UpdateInvitationLetterDocumentStructureInput:
21+
header: str
22+
footer: str
23+
pages: list[UpdateInvitationLetterDocumentPageInput]
24+
25+
26+
@strawberry.input
27+
class UpdateInvitationLetterDocumentInput:
28+
id: strawberry.ID
29+
dynamic_document: UpdateInvitationLetterDocumentStructureInput
30+
31+
32+
@strawberry.type
33+
class InvitationLetterNotEditable:
34+
message: str = "Invitation letter document is not editable"
35+
36+
37+
UpdateInvitationLetterDocumentResult = Annotated[
38+
InvitationLetterDocument | InvitationLetterNotEditable | NotFound,
39+
strawberry.union(name="UpdateInvitationLetterDocumentResult"),
40+
]
41+
42+
43+
@strawberry.field(permission_classes=[CanEditInvitationLetterDocument])
44+
def update_invitation_letter_document(
45+
info: strawberry.Info[Context], input: UpdateInvitationLetterDocumentInput
46+
) -> UpdateInvitationLetterDocumentResult:
47+
invitation_letter_document = InvitationLetterDocumentModel.objects.filter(
48+
id=input.id,
49+
).first()
50+
51+
if not invitation_letter_document:
52+
return NotFound()
53+
54+
if invitation_letter_document.document:
55+
return InvitationLetterNotEditable()
56+
57+
invitation_letter_document.dynamic_document = strawberry.asdict(
58+
input.dynamic_document
59+
)
60+
invitation_letter_document.save(update_fields=["dynamic_document"])
61+
62+
create_change_admin_log_entry(
63+
info.context.request.user,
64+
invitation_letter_document,
65+
change_message="Invitation letter document updated",
66+
)
67+
return InvitationLetterDocument.from_model(invitation_letter_document)
68+
69+
70+
VisaMutation = create_type(
71+
"VisaMutation",
72+
(update_invitation_letter_document,),
73+
)

backend/api/visa/permissions.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from api.permissions import IsStaffPermission
2+
from visa.models import InvitationLetterDocument
3+
4+
5+
class CanViewInvitationLetterDocument(IsStaffPermission):
6+
message = "Cannot view invitation letter document"
7+
8+
def has_permission(self, source, info, **kwargs):
9+
if not super().has_permission(source, info, **kwargs):
10+
return False
11+
12+
self.invitation_letter_document = self.get_invitation_letter_document(kwargs)
13+
user = info.context.request.user
14+
return user.has_perm(
15+
"visa.view_invitationletterdocument", self.invitation_letter_document
16+
)
17+
18+
def get_invitation_letter_document(self, kwargs):
19+
if input := kwargs.get("input", None):
20+
id = input.id
21+
else:
22+
id = kwargs.get("id")
23+
24+
return InvitationLetterDocument.objects.filter(id=id).first()
25+
26+
27+
class CanEditInvitationLetterDocument(CanViewInvitationLetterDocument):
28+
message = "Cannot edit invitation letter document"
29+
30+
def has_permission(self, source, info, **kwargs):
31+
if not super().has_permission(source, info, **kwargs):
32+
return False
33+
34+
invitation_letter_document = self.invitation_letter_document
35+
user = info.context.request.user
36+
return user.has_perm(
37+
"visa.change_invitationletterdocument", invitation_letter_document
38+
)

0 commit comments

Comments
 (0)