diff --git a/.isort.cfg b/.isort.cfg index 90a916d6291eb1d325ecd31c9258d23037dcc2f4..86199fdc497d3b3b5ce9adf3aee776a4d8ba8522 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -3,4 +3,4 @@ line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,gql,httplib2,icalendar,instaloader,markdown,modelcluster,nh3,pirates,pytest,pytz,requests,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,willow,yaml +known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,django_ratelimit,environ,faker,fastjsonschema,gql,httplib2,icalendar,instaloader,markdown,modelcluster,nh3,pirates,pytest,pytz,requests,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,willow,yaml diff --git a/README.md b/README.md index ba48d873d5adaf39f9cbbf65acb675ef6b9e2f2d..dfe3d8842122d1454b76bbe164dba7fe0e6dab65 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ V produkci musĂ bĂ˝t navĂc nastaveno: | `EMAIL_HOST_USER` | --||-- Username | | `EMAIL_HOST_PASSWORD` | --||-- Heslo | | `EMAIL_PORT` | --||-- Port | +| `CLAMD_TCP_ADDR` | ClamAV host (pro skenovánĂ virĹŻ v nahranĂ˝ch souborech) | +| `CLAMD_TCP_SOCKET` | ClamAV socket | RĹŻznĂ©: diff --git a/main/forms.py b/main/forms.py index 51f890009d852cb62d2d544039b2ac227c531ba3..f01f1d9ede56e8467622bd99e9390c82263bc7d6 100644 --- a/main/forms.py +++ b/main/forms.py @@ -2,6 +2,7 @@ import os import tempfile from django import forms +from django.core.exceptions import ValidationError from shared.forms import ArticlesPageForm as SharedArticlesPageForm from shared.forms import JekyllImportForm as SharedJekyllImportForm @@ -18,15 +19,53 @@ class MultipleFileField(forms.FileField): kwargs.setdefault("widget", MultipleFileInput()) super().__init__(*args, **kwargs) + TOTAL_MAX_FILE_SIZE = 25 * 1024 * 1024 # 25 MB + def clean(self, data, initial=None): single_file_clean = super().clean + if isinstance(data, (list, tuple)): + total_size = 0 + + for file in data: + total_size += file.size + + if total_size > self.TOTAL_MAX_FILE_SIZE: + raise ValidationError( + "Celková velikost nahranĂ˝ch souborĹŻ je pĹ™Ăliš velká." + ) + result = [single_file_clean(d, initial) for d in data] else: result = [single_file_clean(data, initial)] + return result +# Allowed MIME types +ALLOWED_FILE_TYPES = [ + "application/pdf", # PDF + "application/msword", # DOC + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # DOCX + "image/png", # PNG + "application/vnd.oasis.opendocument.text", # ODT +] + + +def validate_file_type(file): + if file.content_type not in ALLOWED_FILE_TYPES: + raise ValidationError( + f"ChybnĂ˝ formát souboru: {file.content_type}. PovolenĂ© jsou pouze PDF, DOC, DOCX, PNG, and ODT." + ) + + +def validate_file_size(file, max_size=10 * 1024 * 1024): # Default: 10 MB + if file.size > max_size: + raise ValidationError( + f"Soubory mohou bĂ˝t max. {max_size / (1024 * 1024)} MB velkĂ©." + ) + + class CareerSubmissionForm(forms.Form): name = forms.CharField( min_length=1, @@ -74,22 +113,35 @@ class CareerSubmissionForm(forms.Form): cv_file = forms.FileField( required=True, + validators=[validate_file_type, validate_file_size], widget=forms.FileInput( - attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"} + attrs={ + "class": "max-w-64 mr-auto overflow-hidden break-words", + "accept": ".pdf,.doc,.docx,.png,.odt", + } ), ) cover_letter_file = forms.FileField( required=True, + validators=[validate_file_type, validate_file_size], widget=forms.FileInput( - attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"} + attrs={ + "class": "max-w-64 mr-auto overflow-hidden break-words", + "accept": ".pdf,.doc,.docx,.png,.odt", + } ), ) other_files = MultipleFileField( + required=False, + validators=[validate_file_type, validate_file_size], widget=MultipleFileInput( - attrs={"class": "max-w-64 mr-auto overflow-hidden break-words"} - ) + attrs={ + "class": "max-w-64 mr-auto overflow-hidden break-words", + "accept": ".pdf,.doc,.docx,.png,.odt", + } + ), ) personal_data_agreement = forms.BooleanField(required=True) diff --git a/main/middlewares.py b/main/middlewares.py new file mode 100644 index 0000000000000000000000000000000000000000..b136e1ee3f8759dcec6aae948aae0f705ddcf7f1 --- /dev/null +++ b/main/middlewares.py @@ -0,0 +1,40 @@ +from io import BytesIO + +import clamd +from django.conf import settings +from django.http import HttpResponseForbidden + + +class ClamAVMiddleware: + def __init__(self, get_response): + self.get_response = get_response + # One-time configuration and initialization. + + def __call__(self, request): + # Code to be executed for each request before + # the view (and later middleware) are called. + + # If there is no Clamd connection set, don't check files as we are presumably + # in a development environment. + if not settings.CLAMD_TCP_SOCKET or not settings.CLAMD_TCP_ADDR: + return self.get_response(request) + + cd = clamd.ClamdNetworkSocket( + host=settings.CLAMD_TCP_ADDR, port=settings.CLAMD_TCP_SOCKET, timeout=120 + ) + + if request.method == "POST" and len(request.FILES) > 0: + for file_ in request.FILES.values(): + scan_result = cd.instream(BytesIO(file_.read())) + + if scan_result["stream"][0] == "FOUND": + return HttpResponseForbidden( + "NahranĂ˝ soubor obsahuje potenciálnÄ› škodlivĂ˝ obsah." + ) + + response = self.get_response(request) + + # Code to be executed for each request/response after + # the view is called. + + return response diff --git a/main/migrations/0139_maincareerpage_recipient_emails.py b/main/migrations/0139_maincareerpage_recipient_emails.py index 59783e9eb756353f9255fb193f4bb6b5e9994016..7cebf5cba304f6bd10112773b35645711b77f604 100644 --- a/main/migrations/0139_maincareerpage_recipient_emails.py +++ b/main/migrations/0139_maincareerpage_recipient_emails.py @@ -4,16 +4,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('main', '0138_maincareerpage_content'), + ("main", "0138_maincareerpage_content"), ] operations = [ migrations.AddField( - model_name='maincareerpage', - name='recipient_emails', - field=models.CharField(default='', help_text='Zadej buÄŹ jednu adresu, nebo vĂc, oddÄ›lenĂ˝ch čárkami.', verbose_name='PĹ™Ăjemci emailĹŻ o novĂ˝ch pĹ™ihláškách'), + model_name="maincareerpage", + name="recipient_emails", + field=models.CharField( + default="", + help_text="Zadej buÄŹ jednu adresu, nebo vĂc, oddÄ›lenĂ˝ch čárkami.", + verbose_name="PĹ™Ăjemci emailĹŻ o novĂ˝ch pĹ™ihláškách", + ), preserve_default=False, ), ] diff --git a/main/models.py b/main/models.py index e9c2f00c2a60701469284fcb7ca3e4fed6fda061..9150089492cbb03e8381ef3b5ec49dfddced6c77 100644 --- a/main/models.py +++ b/main/models.py @@ -1,9 +1,11 @@ from datetime import date, datetime from django.contrib import messages +from django.core.exceptions import PermissionDenied from django.core.mail import EmailMessage from django.db import models from django.shortcuts import render +from django_ratelimit.core import is_ratelimited from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey, ParentalManyToManyField from taggit.models import TaggedItemBase @@ -515,6 +517,11 @@ class MainCareerPage( parent_page_types = ["main.MainCareersPage"] def serve(self, request): + if is_ratelimited( + request, group="career_submissions", key="ip", rate="2/m", method="POST" + ): + raise PermissionDenied("Rate limit exceeded") + form = None current_time = datetime.now() @@ -573,7 +580,7 @@ PĹ™i otevĂránĂ souborĹŻ buÄŹte opatrnĂ, virovĂ˝ sken neprobÄ›hl! for file in form.cleaned_data["other_files"]: email.attach(file.name, file.read(), file.content_type) - sent_successfully = email.send(fail_silently=True) + sent_successfully = email.send() if sent_successfully: messages.add_message( @@ -583,13 +590,21 @@ PĹ™i otevĂránĂ souborĹŻ buÄŹte opatrnĂ, virovĂ˝ sken neprobÄ›hl! messages.add_message( request, messages.ERROR, - "Chyba serveru pĹ™i odesĂlánĂ pĹ™ihlášky.", + "OdeslánĂ pĹ™ihlášky selhalo. Zkuste to znovu.", ) else: + errors = "" + + for error_val in form.errors.values(): + errors += f"{error_val.as_text()}\n" + messages.add_message( request, messages.ERROR, - "Chyba pĹ™i odeslánĂ pĹ™ihlášky - prohlĂĹľeÄŤ odeslal chybná data.", + f""" +OdeslánĂ pĹ™ihlášky selhalo: +{errors} +""", ) else: form = CareerSubmissionForm() diff --git a/main/templates/main/main_career_page.html b/main/templates/main/main_career_page.html index 067190985335e24b2db8f5f97f0b583898411696..060e80d33951a1d6b42087395f70831d94bbcc9e 100644 --- a/main/templates/main/main_career_page.html +++ b/main/templates/main/main_career_page.html @@ -86,7 +86,7 @@ {{ form.cv_file }} - <small class="text-grey-300">(PovinnĂ©)</small> + <small class="text-grey-300">(PovinnĂ©, max. 10 MB)</small> </section> <section class="flex flex-col gap-3 lg:items-center lg:flex-row"> <label @@ -97,7 +97,7 @@ {{ form.cover_letter_file }} - <small class="text-grey-300">(PovinnĂ˝)</small> + <small class="text-grey-300">(PovinnĂ˝, max. 10 MB)</small> </section> <section class="flex flex-col gap-3 lg:items-center lg:flex-row"> <label @@ -106,7 +106,13 @@ for="id_other_files" >OstatnĂ soubory: </label> - {{ form.other_files }} + <div class="flex flex-col gap-2"> + {{ form.other_files }} + + <small class="text-grey-300"> + (Max. 10 MB na jeden soubor, 25 MB celkem) + </small> + </div> </section> <section class="flex flex-row gap-3 items-start leading-none"> diff --git a/majak/settings/base.py b/majak/settings/base.py index 3012a6e8bf6e8bdb93cd29df2a0a39978bcff5d1..9088da18173242cee83d68e006eaf594822f3cff 100644 --- a/majak/settings/base.py +++ b/majak/settings/base.py @@ -115,6 +115,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.security.SecurityMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware", + "main.middlewares.ClamAVMiddleware", ] # STATIC @@ -163,6 +164,12 @@ CSRF_COOKIE_HTTPONLY = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = "DENY" +# ClamAV + +CLAMD_USE_TCP = True +CLAMD_TCP_SOCKET = env.int("CLAMD_TCP_SOCKET", default=0) +CLAMD_TCP_ADDR = env.str("CLAMD_TCP_ADDR", default="") + # needed for editing large map collections DATA_UPLOAD_MAX_NUMBER_FIELDS = None diff --git a/requirements/base.in b/requirements/base.in index 57010e1715794d31b93849da83af86037d2839d5..3f2dd2a6f5d7f71d53301821e70b4c744ae242ca 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,3 +1,4 @@ +clamd wagtail wagtail-metadata wagtail-trash @@ -7,6 +8,7 @@ django-extensions django-redis django-settings-export django-widget-tweaks +django-ratelimit django-simple-captcha gql[all] numpy diff --git a/requirements/base.txt b/requirements/base.txt index 28152d4f9f8610c96502169602649ffa4a1e9344..56b9bb0308da28e4da9fef00456bc0123bbce634 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,29 +2,29 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile base.in +# pip-compile requirements/base.in # -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via aiohttp -aiohttp==3.10.10 +aiohttp==3.11.10 # via gql aiosignal==1.3.1 # via aiohttp -amqp==5.2.0 +amqp==5.3.1 # via kombu anyascii==0.3.2 # via wagtail -anyio==4.6.2.post1 +anyio==4.7.0 # via # gql # httpx arrow==1.3.0 # via - # -r base.in + # -r requirements/base.in # ics asgiref==3.8.1 # via django -asttokens==2.4.1 +asttokens==3.0.0 # via stack-data attrs==24.2.0 # via @@ -36,20 +36,20 @@ backoff==2.2.1 # via gql beautifulsoup4==4.12.3 # via - # -r base.in + # -r requirements/base.in # wagtail billiard==4.2.1 # via celery -bleach==6.1.0 - # via -r base.in -botocore==1.35.50 +bleach==6.2.0 + # via -r requirements/base.in +botocore==1.35.78 # via gql brotli==1.1.0 # via fonttools cattrs==24.1.2 # via requests-cache celery==5.4.0 - # via -r base.in + # via -r requirements/base.in certifi==2024.8.30 # via # httpcore @@ -62,6 +62,8 @@ cffi==1.17.1 # weasyprint charset-normalizer==3.4.0 # via requests +clamd==1.0.2 + # via -r requirements/base.in click==8.1.7 # via # celery @@ -74,7 +76,7 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -cryptography==43.0.3 +cryptography==44.0.0 # via # josepy # mozilla-django-oidc @@ -87,7 +89,7 @@ defusedxml==0.7.1 # via willow django==5.0.7 # via - # -r base.in + # -r requirements/base.in # django-extensions # django-filter # django-modelcluster @@ -103,9 +105,9 @@ django==5.0.7 # mozilla-django-oidc # wagtail django-environ==0.11.2 - # via -r base.in + # via -r requirements/base.in django-extensions==3.2.3 - # via -r base.in + # via -r requirements/base.in django-filter==24.3 # via wagtail django-modelcluster==6.3 @@ -114,18 +116,20 @@ django-permissionedforms==0.1 # via wagtail django-ranged-response==0.2.0 # via django-simple-captcha +django-ratelimit==4.1.0 + # via -r requirements/base.in django-redis==5.4.0 - # via -r base.in + # via -r requirements/base.in django-settings-export==1.2.1 - # via -r base.in + # via -r requirements/base.in django-simple-captcha==0.6.0 - # via -r base.in -django-taggit==5.0.1 + # via -r requirements/base.in +django-taggit==6.1.0 # via wagtail django-treebeard==4.7.1 # via wagtail django-widget-tweaks==1.5.0 - # via -r base.in + # via -r requirements/base.in djangorestframework==3.15.2 # via wagtail draftjs-exporter==5.0.0 @@ -134,43 +138,41 @@ et-xmlfile==2.0.0 # via openpyxl executing==2.1.0 # via stack-data -fastjsonschema==2.20.0 - # via -r base.in +fastjsonschema==2.21.1 + # via -r requirements/base.in filetype==1.2.0 # via willow -fonttools[woff]==4.54.1 +fonttools[woff]==4.55.3 # via weasyprint frozenlist==1.5.0 # via # aiohttp # aiosignal gql[all]==3.5.0 - # via -r base.in + # via -r requirements/base.in graphql-core==3.2.5 # via gql h11==0.14.0 # via httpcore -html5lib==1.1 - # via weasyprint -httpcore==1.0.6 +httpcore==1.0.7 # via httpx httplib2==0.22.0 - # via -r base.in -httpx==0.27.2 + # via -r requirements/base.in +httpx==0.28.1 # via gql -icalendar==6.0.1 - # via -r base.in +icalendar==6.1.0 + # via -r requirements/base.in ics==0.7.2 - # via -r base.in + # via -r requirements/base.in idna==3.10 # via # anyio # httpx # requests # yarl -ipython==8.29.0 - # via -r base.in -jedi==0.19.1 +ipython==8.30.0 + # via -r requirements/base.in +jedi==0.19.2 # via ipython jmespath==1.0.1 # via botocore @@ -183,7 +185,7 @@ l18n==2021.3 laces==0.1.1 # via wagtail markdown==3.7 - # via -r base.in + # via -r requirements/base.in matplotlib-inline==0.1.7 # via ipython mozilla-django-oidc==3.0.0 @@ -192,44 +194,46 @@ multidict==6.1.0 # via # aiohttp # yarl -nh3==0.2.18 - # via -r base.in -numpy==2.1.2 +nh3==0.2.19 + # via -r requirements/base.in +numpy==2.2.0 # via - # -r base.in + # -r requirements/base.in # opencv-python oauthlib==3.2.2 # via # requests-oauthlib # tweepy opencv-python==4.10.0.84 - # via -r base.in + # via -r requirements/base.in openpyxl==3.1.5 # via wagtail parso==0.8.4 # via jedi pexpect==4.9.0 # via ipython -pillow==10.4.0 +pillow==11.0.0 # via # django-simple-captcha # pillow-heif # wagtail # weasyprint -pillow-heif==0.20.0 +pillow-heif==0.21.0 # via willow pirates==0.7.0 - # via -r base.in + # via -r requirements/base.in platformdirs==4.3.6 # via requests-cache prompt-toolkit==3.0.48 # via # click-repl # ipython -propcache==0.2.0 - # via yarl +propcache==0.2.1 + # via + # aiohttp + # yarl psycopg2-binary==2.9.10 - # via -r base.in + # via -r requirements/base.in ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 @@ -240,12 +244,12 @@ pydyf==0.11.0 # via weasyprint pygments==2.18.0 # via ipython -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via josepy pyparsing==3.2.0 # via httplib2 pypdf2==3.0.1 - # via -r base.in + # via -r requirements/base.in pyphen==0.17.0 # via weasyprint python-dateutil==2.9.0.post0 @@ -257,16 +261,16 @@ python-dateutil==2.9.0.post0 # ics pytz==2024.2 # via - # -r base.in + # -r requirements/base.in # django-modelcluster # l18n pyyaml==6.0.2 - # via -r base.in -redis==5.2.0 + # via -r requirements/base.in +redis==5.2.1 # via django-redis requests==2.32.3 # via - # -r base.in + # -r requirements/base.in # gql # mozilla-django-oidc # requests-cache @@ -275,33 +279,28 @@ requests==2.32.3 # tweepy # wagtail requests-cache==1.2.1 - # via -r base.in + # via -r requirements/base.in requests-oauthlib==1.3.1 # via tweepy requests-toolbelt==1.0.0 # via gql -sentry-sdk==2.17.0 - # via -r base.in -six==1.16.0 +sentry-sdk==2.19.2 + # via -r requirements/base.in +six==1.17.0 # via - # asttokens - # bleach - # html5lib # ics # l18n # python-dateutil # url-normalize sniffio==1.3.1 - # via - # anyio - # httpx + # via anyio soupsieve==2.6 # via beautifulsoup4 -sqlparse==0.5.1 +sqlparse==0.5.3 # via django stack-data==0.6.3 # via ipython -tatsu==5.12.1 +tatsu==5.12.2 # via ics telepath==0.3.1 # via wagtail @@ -309,16 +308,20 @@ tinycss2==1.4.0 # via # cssselect2 # weasyprint +tinyhtml5==2.0.0 + # via weasyprint traitlets==5.14.3 # via # ipython # matplotlib-inline tweepy==4.14.0 - # via -r base.in -types-python-dateutil==2.9.0.20241003 + # via -r requirements/base.in +types-python-dateutil==2.9.0.20241206 # via arrow typing-extensions==4.12.2 - # via ipython + # via + # anyio + # ipython tzdata==2024.2 # via # celery @@ -337,39 +340,39 @@ vine==5.1.0 # amqp # celery # kombu -wagtail==6.2.2 +wagtail==6.3.1 # via - # -r base.in + # -r requirements/base.in # wagtail-metadata # wagtail-modeladmin # wagtail-trash wagtail-metadata==5.0.0 - # via -r base.in + # via -r requirements/base.in wagtail-modeladmin==2.1.0 # via wagtail-trash wagtail-trash==3.0.0 - # via -r base.in + # via -r requirements/base.in wand==0.6.13 - # via -r base.in + # via -r requirements/base.in wcwidth==0.2.13 # via prompt-toolkit -weasyprint==62.3 - # via -r base.in +weasyprint==63.1 + # via -r requirements/base.in webencodings==0.5.1 # via # bleach # cssselect2 - # html5lib # tinycss2 + # tinyhtml5 websockets==11.0.3 # via gql whitenoise==5.3.0 - # via -r base.in + # via -r requirements/base.in willow[heif]==1.9.0 # via # wagtail # willow -yarl==1.17.0 +yarl==1.18.3 # via # aiohttp # gql diff --git a/requirements/dev.txt b/requirements/dev.txt index e5bbcebf726f99f1a6ab8aa2f4fa5563ec64a612..031722c5366655366d60322a51ebadaf8d56787e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,21 +2,21 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile dev.in +# pip-compile requirements/dev.in # asgiref==3.8.1 # via django -coverage[toml]==7.6.4 +coverage[toml]==7.6.9 # via pytest-cov django==5.0.7 # via - # -r dev.in + # -r requirements/dev.in # django-debug-toolbar django-debug-toolbar==4.4.6 - # via -r dev.in + # via -r requirements/dev.in factory-boy==3.3.1 # via pytest-factoryboy -faker==30.8.1 +faker==33.1.0 # via factory-boy fastdiff==0.3.0 # via snapshottest @@ -26,45 +26,45 @@ inflection==0.5.1 # via pytest-factoryboy iniconfig==2.0.0 # via pytest -packaging==24.1 +packaging==24.2 # via # pytest # pytest-factoryboy # pytest-sugar pluggy==1.5.0 # via pytest -pytest==8.3.3 +pytest==8.3.4 # via - # -r dev.in + # -r requirements/dev.in # pytest-cov # pytest-django # pytest-factoryboy # pytest-freezegun # pytest-mock # pytest-sugar -pytest-cov==5.0.0 - # via -r dev.in +pytest-cov==6.0.0 + # via -r requirements/dev.in pytest-django==4.9.0 - # via -r dev.in + # via -r requirements/dev.in pytest-factoryboy==2.7.0 - # via -r dev.in + # via -r requirements/dev.in pytest-freezegun==0.4.2 - # via -r dev.in + # via -r requirements/dev.in pytest-mock==3.14.0 - # via -r dev.in + # via -r requirements/dev.in pytest-sugar==1.0.0 - # via -r dev.in + # via -r requirements/dev.in python-dateutil==2.9.0.post0 # via # faker # freezegun -six==1.16.0 +six==1.17.0 # via # python-dateutil # snapshottest snapshottest==0.6.0 - # via -r dev.in -sqlparse==0.5.1 + # via -r requirements/dev.in +sqlparse==0.5.3 # via # django # django-debug-toolbar diff --git a/requirements/production.txt b/requirements/production.txt index 8123fbd9faca598c73776b30b9fe8cca3befd123..8e4e8c16d3ea8acf3963ac8aa800d1a3c394f1a7 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -2,9 +2,9 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile production.in +# pip-compile requirements/production.in # gunicorn==23.0.0 - # via -r production.in -packaging==24.1 + # via -r requirements/production.in +packaging==24.2 # via gunicorn diff --git a/shared/templates/styleguide2/includes/organisms/layout/messages.html b/shared/templates/styleguide2/includes/organisms/layout/messages.html index b422fc81425bc906607c865a0c77c1d31332cf96..dbe8f862f5b4f461c030feb3d60e9ef5a500fa63 100644 --- a/shared/templates/styleguide2/includes/organisms/layout/messages.html +++ b/shared/templates/styleguide2/includes/organisms/layout/messages.html @@ -1,6 +1,6 @@ <ul class="flex flex-col w-full"> {% for message in messages %} - <script>alert("{{ message }}");</script> + <script>alert(`{{ message }}`);</script> {% comment %} <li>