Skip to content

Commit

Permalink
Implement Invitation letter generation (#4266)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoacierno authored Dec 25, 2024
1 parent 2617a8c commit c4ed4ce
Show file tree
Hide file tree
Showing 82 changed files with 7,802 additions and 2,979 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,5 @@ badge-service/badges.zip
backend/custom_admin/src/types.ts
backend/schema.graphql
backend/__pypackages__/
backend/custom_admin/.astro/
backend/custom_admin/core.*
2 changes: 1 addition & 1 deletion Dockerfile.node.local
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ RUN npm install -g pnpm; \

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

RUN pip3 install websockets --break-system-packages
RUN pip3 install websockets==14.1 --break-system-packages

COPY . .
164 changes: 38 additions & 126 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,119 +1,48 @@
ARG FUNCTION_DIR="/home/app/"
# check=skip=SecretsUsedInArgOrEnv
ARG FUNCTION_DIR="/home/app"

FROM python:3.11-slim as build-stage
FROM python:3.11-slim AS base

ARG FUNCTION_DIR

RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}
ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod \
AWS_MEDIA_BUCKET=example \
AWS_REGION_NAME=eu-central-1 \
SECRET_KEY=DEMO \
STRIPE_SECRET_API_KEY=demo \
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
CELERY_BROKER_URL=demo \
CELERY_RESULT_BACKEND=demo \
HASHID_DEFAULT_SECRET_SALT=demo

RUN apt-get update -y && apt-get install -y \
gcc libpq-dev git \
# Pillow
libtiff5-dev libjpeg62 libopenjp2-7-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
libharfbuzz-dev libfribidi-dev libxcb1-dev libldap2-dev libldap-2.5-0 \
ffmpeg libsm6 libxext6 libglib2.0-0
# weasyprint
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 \
# postgres
libpq-dev

ENV LIBRARY_PATH=/lib:/usr/lib

RUN pip install uv==0.5.5

ARG TARGETPLATFORM

ARG TARGETPLATFORM

RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
tar -czvf /libs.tar.gz \
/usr/lib/aarch64-linux-gnu/libpq* \
/usr/lib/aarch64-linux-gnu/libldap_r* \
/usr/lib/aarch64-linux-gnu/libldap* \
/usr/lib/aarch64-linux-gnu/liblber* \
/usr/lib/aarch64-linux-gnu/libsasl* \
/usr/lib/aarch64-linux-gnu/libxml2* \
/usr/lib/aarch64-linux-gnu/libgcrypt* \
/usr/lib/aarch64-linux-gnu/libstdc++* \
/usr/lib/aarch64-linux-gnu/libjpeg* \
/usr/lib/aarch64-linux-gnu/libopenjp2* \
/usr/lib/aarch64-linux-gnu/libdeflate* \
/usr/lib/aarch64-linux-gnu/libjbig* \
/usr/lib/aarch64-linux-gnu/liblcms2* \
/usr/lib/aarch64-linux-gnu/libwebp* \
/usr/lib/aarch64-linux-gnu/libtiff* \
/usr/lib/aarch64-linux-gnu/libGL* \
/usr/lib/aarch64-linux-gnu/libgthread* \
/usr/lib/aarch64-linux-gnu/libglib-* \
/usr/lib/aarch64-linux-gnu/libX11* \
/usr/lib/aarch64-linux-gnu/libxcb* \
/usr/lib/aarch64-linux-gnu/libXau* \
/usr/lib/aarch64-linux-gnu/libXdmcp* \
/usr/lib/aarch64-linux-gnu/libXext* \
/usr/lib/aarch64-linux-gnu/libbsd*; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
tar -czvf /libs.tar.gz \
/usr/lib/x86_64-linux-gnu/libpq* \
/usr/lib/x86_64-linux-gnu/libldap_r* \
/usr/lib/x86_64-linux-gnu/libldap* \
/usr/lib/x86_64-linux-gnu/liblber* \
/usr/lib/x86_64-linux-gnu/libsasl* \
/usr/lib/x86_64-linux-gnu/libxml2* \
/usr/lib/x86_64-linux-gnu/libgcrypt* \
/usr/lib/x86_64-linux-gnu/libstdc++* \
/usr/lib/x86_64-linux-gnu/libjpeg* \
/usr/lib/x86_64-linux-gnu/libopenjp2* \
/usr/lib/x86_64-linux-gnu/libdeflate* \
/usr/lib/x86_64-linux-gnu/libjbig* \
/usr/lib/x86_64-linux-gnu/liblcms2* \
/usr/lib/x86_64-linux-gnu/libwebp* \
/usr/lib/x86_64-linux-gnu/libtiff* \
/usr/lib/x86_64-linux-gnu/libGL* \
/usr/lib/x86_64-linux-gnu/libgthread* \
/usr/lib/x86_64-linux-gnu/libglib-* \
/usr/lib/x86_64-linux-gnu/libX11* \
/usr/lib/x86_64-linux-gnu/libxcb* \
/usr/lib/x86_64-linux-gnu/libXau* \
/usr/lib/x86_64-linux-gnu/libXdmcp* \
/usr/lib/x86_64-linux-gnu/libXext* \
/usr/lib/x86_64-linux-gnu/libbsd*; \
fi


COPY pyproject.toml uv.lock ${FUNCTION_DIR}

RUN uv sync --no-dev

# Create GraphQL schema

FROM python:3.11-slim as schema-stage
FROM base AS build-stage

ARG FUNCTION_DIR

RUN apt-get update -y && apt-get install -y \
gcc git

RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}

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

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

COPY . ${FUNCTION_DIR}
RUN uv sync --no-dev

ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod
COPY . ./

RUN AWS_MEDIA_BUCKET=example \
AWS_REGION_NAME=eu-central-1 \
SECRET_KEY=DEMO \
STRIPE_SECRET_API_KEY=demo \
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
CELERY_BROKER_URL=demo \
CELERY_RESULT_BACKEND=demo \
HASHID_DEFAULT_SECRET_SALT=demo \
${FUNCTION_DIR}/.venv/bin/python manage.py graphql_schema
RUN .venv/bin/python manage.py graphql_schema

# Build custom admin components

FROM node:18.17.1 as js-stage
FROM node:23 AS js-stage

ARG FUNCTION_DIR

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

RUN pnpm install

COPY custom_admin/ .
COPY --from=build-stage ${FUNCTION_DIR}/schema.graphql schema.graphql

COPY --from=schema-stage ${FUNCTION_DIR}/schema.graphql schema.graphql
COPY custom_admin/ .

RUN ADMIN_GRAPHQL_URL=schema.graphql pnpm codegen
RUN pnpm build
RUN ADMIN_GRAPHQL_URL=schema.graphql pnpm codegen && pnpm build

# Final stage
# Runtime stage

FROM python:3.11-slim
FROM base AS runtime-stage

ARG FUNCTION_DIR

WORKDIR ${FUNCTION_DIR}

ENV LIBRARY_PATH=/lib:/usr/lib LD_LIBRARY_PATH=/lib:/usr/lib

RUN apt-get update -y && apt-get install -y curl

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

COPY --chown=app:app --from=js-stage ${FUNCTION_DIR}/dist/*.html ${FUNCTION_DIR}/custom_admin/templates/astro/
COPY --chown=app:app --from=js-stage ${FUNCTION_DIR}/dist/_astro ${FUNCTION_DIR}/custom_admin/static/_astro/

COPY --chown=app:app --from=build-stage ${FUNCTION_DIR}/.venv ${FUNCTION_DIR}/.venv
COPY --from=build-stage /usr/local/lib/*.so* /usr/local/lib/
COPY --from=build-stage /libs.tar.gz /libs.tar.gz

RUN tar -xvf /libs.tar.gz -C / && rm /libs.tar.gz && ldconfig

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

USER app

RUN mkdir -p ${FUNCTION_DIR}/assets

ENV DJANGO_SETTINGS_MODULE=pycon.settings.prod

RUN AWS_MEDIA_BUCKET=example \
AWS_REGION_NAME=eu-central-1 \
SECRET_KEY=DEMO \
STRIPE_SECRET_API_KEY=demo \
STRIPE_SUBSCRIPTION_PRICE_ID=demo \
STRIPE_WEBHOOK_SIGNATURE_SECRET=demo \
CELERY_BROKER_URL=demo \
CELERY_RESULT_BACKEND=demo \
HASHID_DEFAULT_SECRET_SALT=demo \
${FUNCTION_DIR}/.venv/bin/python manage.py collectstatic --noinput
RUN mkdir -p assets && .venv/bin/python manage.py collectstatic --noinput

ENTRYPOINT ["/home/app/.venv/bin/python", "-m", "awslambdaric"]
CMD [ "wsgi_handler.handler" ]
ENTRYPOINT ["/home/app/.venv/bin/gunicorn"]
CMD [ "pycon.wsgi" ]
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def _unassigned_schedule_items(client, **input):
id
}
}""",
variables={**input},
variables=input,
)


Expand Down
4 changes: 4 additions & 0 deletions backend/api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from .association_membership.mutation import AssociationMembershipMutation
from .cms.schema import CMSQuery
from .sponsors.schema import SponsorsMutation
from .visa.queries import VisaQuery
from .visa.mutations import VisaMutation


@strawberry.type
Expand All @@ -44,6 +46,7 @@ class Query(
BadgeScannerQuery,
UserQuery,
CMSQuery,
VisaQuery,
):
pass

Expand All @@ -64,6 +67,7 @@ class Mutation(
UsersMutations,
AssociationMembershipMutation,
SponsorsMutation,
VisaMutation,
):
pass

Expand Down
5 changes: 5 additions & 0 deletions backend/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,8 @@ def paginate_list(
),
items=items,
)


@strawberry.type
class NotFound:
message: str = "Not found"
73 changes: 73 additions & 0 deletions backend/api/visa/mutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Annotated
from api.context import Context
from api.types import NotFound
from custom_admin.audit import create_change_admin_log_entry
from visa.models import InvitationLetterDocument as InvitationLetterDocumentModel
from api.visa.permissions import CanEditInvitationLetterDocument
from strawberry.tools import create_type
from api.visa.types import InvitationLetterDocument
import strawberry


@strawberry.input
class UpdateInvitationLetterDocumentPageInput:
id: strawberry.ID
title: str
content: str


@strawberry.input
class UpdateInvitationLetterDocumentStructureInput:
header: str
footer: str
pages: list[UpdateInvitationLetterDocumentPageInput]


@strawberry.input
class UpdateInvitationLetterDocumentInput:
id: strawberry.ID
dynamic_document: UpdateInvitationLetterDocumentStructureInput


@strawberry.type
class InvitationLetterNotEditable:
message: str = "Invitation letter document is not editable"


UpdateInvitationLetterDocumentResult = Annotated[
InvitationLetterDocument | InvitationLetterNotEditable | NotFound,
strawberry.union(name="UpdateInvitationLetterDocumentResult"),
]


@strawberry.field(permission_classes=[CanEditInvitationLetterDocument])
def update_invitation_letter_document(
info: strawberry.Info[Context], input: UpdateInvitationLetterDocumentInput
) -> UpdateInvitationLetterDocumentResult:
invitation_letter_document = InvitationLetterDocumentModel.objects.filter(
id=input.id,
).first()

if not invitation_letter_document:
return NotFound()

if invitation_letter_document.document:
return InvitationLetterNotEditable()

invitation_letter_document.dynamic_document = strawberry.asdict(
input.dynamic_document
)
invitation_letter_document.save(update_fields=["dynamic_document"])

create_change_admin_log_entry(
info.context.request.user,
invitation_letter_document,
change_message="Invitation letter document updated",
)
return InvitationLetterDocument.from_model(invitation_letter_document)


VisaMutation = create_type(
"VisaMutation",
(update_invitation_letter_document,),
)
38 changes: 38 additions & 0 deletions backend/api/visa/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from api.permissions import IsStaffPermission
from visa.models import InvitationLetterDocument


class CanViewInvitationLetterDocument(IsStaffPermission):
message = "Cannot view invitation letter document"

def has_permission(self, source, info, **kwargs):
if not super().has_permission(source, info, **kwargs):
return False

self.invitation_letter_document = self.get_invitation_letter_document(kwargs)
user = info.context.request.user
return user.has_perm(
"visa.view_invitationletterdocument", self.invitation_letter_document
)

def get_invitation_letter_document(self, kwargs):
if input := kwargs.get("input", None):
id = input.id
else:
id = kwargs.get("id")

return InvitationLetterDocument.objects.filter(id=id).first()


class CanEditInvitationLetterDocument(CanViewInvitationLetterDocument):
message = "Cannot edit invitation letter document"

def has_permission(self, source, info, **kwargs):
if not super().has_permission(source, info, **kwargs):
return False

invitation_letter_document = self.invitation_letter_document
user = info.context.request.user
return user.has_perm(
"visa.change_invitationletterdocument", invitation_letter_document
)
Loading

0 comments on commit c4ed4ce

Please sign in to comment.