Integrate Wagtail CMS and set up landing page structure (#2014)

This commit is contained in:
Greg Kaleka
2025-11-18 15:53:27 -05:00
committed by GitHub
parent f109203a47
commit 0f9bdc6f35
36 changed files with 1103 additions and 252 deletions

View File

@@ -89,6 +89,24 @@ INSTALLED_APPS += [
"widget_tweaks", "widget_tweaks",
] ]
# Wagtail Apps
INSTALLED_APPS += [
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
"wagtail.snippets",
"wagtail.documents",
"wagtail.images",
"wagtail.search",
"wagtail.admin",
"wagtail",
"wagtailmarkdown",
"modelcluster",
"taggit",
]
# Our Apps # Our Apps
INSTALLED_APPS += [ INSTALLED_APPS += [
"ak", "ak",
@@ -124,6 +142,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware", "allauth.account.middleware.AccountMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware", "oauth2_provider.middleware.OAuth2TokenMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
] ]
if DEBUG: if DEBUG:
@@ -592,3 +611,31 @@ ALGOLIA = {
"api_key": env("ALGOLIA_API_KEY", None), "api_key": env("ALGOLIA_API_KEY", None),
"region": env("ALGOLIA_APP_REGION", "us"), "region": env("ALGOLIA_APP_REGION", "us"),
} }
# Required by Wagtail
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
WAGTAIL_SITE_NAME = "Boost.org"
WAGTAILADMIN_BASE_URL = env("WAGTAILADMIN_BASE_URL", default="https://www.boost.org/")
WAGTAILDOCS_EXTENSIONS = [
"csv",
"docx",
"key",
"odt",
"pdf",
"pptx",
"rtf",
"txt",
"xlsx",
"zip",
]
WAGTAILMARKDOWN = {
"autodownload_fontawesome": True,
"allowed_tags": [], # optional. a list of HTML tags. e.g. ['div', 'p', 'a']
"allowed_styles": [], # optional. a list of styles
"allowed_attributes": {}, # optional. a dict with HTML tag as key and a list of attributes as value
"allowed_settings_mode": "extend", # optional. Possible values: "extend" or "override". Defaults to "extend".
"extensions": [], # optional. a list of python-markdown supported extensions
"extension_configs": {}, # optional. a dictionary with the extension name as key, and its configuration as value
"extensions_settings_mode": "extend", # optional. Possible values: "extend" or "override". Defaults to "extend".
"tab_length": 4, # optional. Sets the length of tabs used by python-markdown to render the output. This is the number of spaces used to replace with a tab character. Defaults to 4.
}

View File

@@ -7,6 +7,9 @@ from django.urls import include, path, re_path, register_converter, reverse_lazy
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from rest_framework import routers from rest_framework import routers
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from ak.views import ( from ak.views import (
ForbiddenView, ForbiddenView,
@@ -402,6 +405,10 @@ urlpatterns = (
ModernizedDocsView.as_view(), ModernizedDocsView.as_view(),
name="modernized_docs", name="modernized_docs",
), ),
# Wagtail stuff
path("cms/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
path("outreach/", include(wagtail_urls)),
] ]
+ [ + [
path( path(

View File

@@ -201,6 +201,8 @@ Env:
# postgres caching of s3 text file content # postgres caching of s3 text file content
- name: ENABLE_DB_CACHE - name: ENABLE_DB_CACHE
value: "true" value: "true"
- name: WAGTAILADMIN_BASE_URL
value: https://www.cppal-dev.boost.org/
# Volumes # Volumes
Volumes: Volumes:

View File

@@ -201,6 +201,8 @@ Env:
# postgres caching of s3 text file content # postgres caching of s3 text file content
- name: ENABLE_DB_CACHE - name: ENABLE_DB_CACHE
value: "true" value: "true"
- name: WAGTAILADMIN_BASE_URL
value: https://www.boost.org/
# Volumes # Volumes
Volumes: Volumes:

View File

@@ -201,6 +201,8 @@ Env:
# postgres caching of s3 text file content # postgres caching of s3 text file content
- name: ENABLE_DB_CACHE - name: ENABLE_DB_CACHE
value: "true" value: "true"
- name: WAGTAILADMIN_BASE_URL
value: https://www.stage.boost.org/
# Volumes # Volumes
Volumes: Volumes:

View File

@@ -6,4 +6,4 @@ from marketing.models import CapturedEmail
@admin.register(CapturedEmail) @admin.register(CapturedEmail)
class CapturedEmailAdmin(admin.ModelAdmin): class CapturedEmailAdmin(admin.ModelAdmin):
model = CapturedEmail model = CapturedEmail
list_display = ("email", "referrer", "page_slug") list_display = ("email", "referrer", "page")

View File

@@ -1,17 +0,0 @@
from django import forms
from .models import CapturedEmail
class CapturedEmailForm(forms.ModelForm):
class Meta:
model = CapturedEmail
fields = ["email"]
widgets = {
"email": forms.EmailInput(
attrs={
"placeholder": "your@email.com",
"autocomplete": "email",
}
)
}

View File

@@ -0,0 +1,231 @@
# Generated by Django 5.2.8 on 2025-11-17 21:27
import django.db.models.deletion
import django.utils.timezone
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("marketing", "0001_initial"),
("wagtailcore", "0095_groupsitepermission"),
]
operations = [
migrations.CreateModel(
name="DetailPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"email_capture_intro",
models.TextField(
default="Drop your email below to get engineering updates."
),
),
(
"privacy_blurb",
models.TextField(
default="Privacy: no spam, one step unsubscribe. We'll only send high-signal dev content re this and other Boost libraries."
),
),
(
"body",
wagtail.fields.StreamField(
[("rich", 0), ("md", 1)],
blank=True,
block_lookup={
0: (
"wagtail.blocks.RichTextBlock",
(),
{
"features": [
"h1",
"h2",
"h3",
"bold",
"italic",
"link",
"ol",
"ul",
"code",
"blockquote",
],
"label": "Rich text",
},
),
1: (
"wagtailmarkdown.blocks.MarkdownBlock",
(),
{"label": "Markdown"},
),
},
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="OutreachHomePage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="ProgramPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"email_capture_intro",
models.TextField(
default="Drop your email below to get engineering updates."
),
),
(
"privacy_blurb",
models.TextField(
default="Privacy: no spam, one step unsubscribe. We'll only send high-signal dev content re this and other Boost libraries."
),
),
(
"body",
wagtail.fields.StreamField(
[("rich", 0), ("md", 1)],
blank=True,
block_lookup={
0: (
"wagtail.blocks.RichTextBlock",
(),
{
"features": [
"h1",
"h2",
"h3",
"bold",
"italic",
"link",
"ol",
"ul",
"code",
"blockquote",
],
"label": "Rich text",
},
),
1: (
"wagtailmarkdown.blocks.MarkdownBlock",
(),
{"label": "Markdown"},
),
},
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="ProgramPageIndex",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name="TopicPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
],
options={
"verbose_name": "Topic",
},
bases=("wagtailcore.page",),
),
migrations.RemoveField(
model_name="capturedemail",
name="page_slug",
),
migrations.AddField(
model_name="capturedemail",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="capturedemail",
name="page",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="captured_emails",
to="wagtailcore.page",
),
),
]

View File

@@ -1,13 +1,210 @@
from django import forms
from django.contrib import messages
from django.db import models from django.db import models
from django.http import Http404
from django.shortcuts import render, redirect
from wagtail.admin.panels import FieldPanel
from wagtail.blocks import RichTextBlock
from wagtail.fields import StreamField
from wagtail.models import Page
from wagtail.url_routing import RouteResult
from wagtailmarkdown.blocks import MarkdownBlock
RICH_TEXT_FEATURES = [
"h1",
"h2",
"h3",
"bold",
"italic",
"link",
"ol",
"ul",
"code",
"blockquote",
]
class CapturedEmail(models.Model): class CapturedEmail(models.Model):
email = models.EmailField() email = models.EmailField()
referrer = models.CharField(blank=True, default="") referrer = models.CharField(blank=True, default="")
page_slug = models.CharField(blank=True, default="") page = models.ForeignKey(
Page,
related_name="captured_emails",
on_delete=models.SET_NULL,
null=True,
blank=True,
default=None,
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.email return self.email
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} ({self.pk}): {self}>" return f"<{self.__class__.__name__} ({self.pk}): {self}>"
class CapturedEmailForm(forms.ModelForm):
class Meta:
model = CapturedEmail
fields = ["email"]
widgets = {
"email": forms.EmailInput(
attrs={"placeholder": "your@email.com", "autocomplete": "email"}
)
}
class EmailCapturePage(Page):
"""Abstract page with reusable logic for pages that capture an email."""
email_capture_intro = models.TextField(
default="Drop your email below to get engineering updates."
)
privacy_blurb = models.TextField(
default="Privacy: no spam, one step unsubscribe. We'll only send high-signal dev content re this and other Boost libraries."
)
body = StreamField(
[
("rich", RichTextBlock(features=RICH_TEXT_FEATURES, label="Rich text")),
("md", MarkdownBlock(label="Markdown")),
],
use_json_field=True,
blank=True,
)
content_panels = Page.content_panels + [
FieldPanel("email_capture_intro"),
FieldPanel("privacy_blurb"),
FieldPanel("body"),
]
class Meta:
abstract = True
def get_referrer(self, request):
original = request.session.get("original_referrer", "")
return original or request.headers.get("referer", "")
def build_form(self, request) -> CapturedEmailForm:
"""Create a form instance appropriate to the request method."""
if request.method == "POST":
return CapturedEmailForm(data=request.POST)
return CapturedEmailForm()
def get_success_url(self, request):
"""Redirect back to the same page after a successful POST."""
return self.url
def handle_email_form(self, request, form):
captured = form.save(commit=False)
captured.referrer = self.get_referrer(request)
captured.page = self
captured.save()
messages.success(request, "Thanks! We'll be in touch.")
return redirect(self.get_success_url(request))
def serve(self, request, *args, **kwargs):
"""
Unified GET/POST handling:
- On GET: render template with empty form.
- On POST: validate, save CapturedEmail, redirect, or redisplay with errors.
"""
form = self.build_form(request)
if request.method == "POST" and form.is_valid():
return self.handle_email_form(request, form)
# Fall through: GET, or invalid POST
context = super().get_context(request, *args, **kwargs)
context["form"] = form
return render(request, self.get_template(request), context)
class ProgramPage(EmailCapturePage):
parent_page_types = ["marketing.ProgramPageIndex"]
subpage_types = []
class DetailPage(EmailCapturePage):
parent_page_types = ["marketing.TopicPage"]
subpage_types = []
# ===================
### Dummy pages ###
# ===================
class OutreachHomePage(Page):
"""A dummy homepage to just return a 404 at the `/outreach/` url"""
parent_page_types = ["wagtailcore.Page"]
subpage_types = ["marketing.ProgramPageIndex", "marketing.TopicPage"]
max_count = 1 # one container
def route(self, request, path_components):
"""
Custom router so public URLs don't include container slugs.
/outreach/program_page/<slug>/ => delegate to ProgramPageIndex -> ProgramPage
/outreach/<topic>/<detail>/ => delegate to TopicPage -> DetailPage
"""
if not path_components:
return RouteResult(self)
first, *rest = path_components
# Fixed segment for program pages
if first == "program_page":
try:
program_page_index = ProgramPageIndex.objects.child_of(self).get()
except ProgramPageIndex.DoesNotExist:
raise Http404("Program index not found")
# Delegate the remaining segments
return program_page_index.route(request, rest)
# Otherwise, first segment should be a TopicPage slug
try:
topic = TopicPage.objects.child_of(self).get(slug=first)
except TopicPage.DoesNotExist:
raise Http404("Topic not found")
return topic.route(request, rest)
# Hide this page publicly: /outreach/ -> 404
def serve(self, request, *args, **kwargs):
raise Http404
def get_sitemap_urls(self, request=None):
return []
class ProgramPageIndex(Page):
"""A dummy index page to facilitate our url scheme"""
parent_page_types = ["marketing.OutreachHomePage"]
subpage_types = ["marketing.ProgramPage"]
max_count = 1 # one container
# Hide index page: /outreach/program_page/ -> 404
def serve(self, request, *args, **kwargs):
raise Http404
def get_sitemap_urls(self, request=None):
return []
class TopicPage(Page):
"""A dummy topic page that represents a given topic (e.g. a library)"""
parent_page_types = ["marketing.OutreachHomePage"]
subpage_types = ["marketing.DetailPage"]
class Meta:
verbose_name = "Topic"
# Hide this page publicly: /outreach/ -> 404
def serve(self, request, *args, **kwargs):
raise Http404
def get_sitemap_urls(self, request=None):
return []

View File

@@ -8,8 +8,7 @@ from django.views.decorators.cache import never_cache
from django.views.generic import CreateView from django.views.generic import CreateView
from core.views import logger from core.views import logger
from marketing.forms import CapturedEmailForm from marketing.models import CapturedEmail, CapturedEmailForm
from marketing.models import CapturedEmail
@method_decorator(never_cache, name="dispatch") @method_decorator(never_cache, name="dispatch")

View File

@@ -107,7 +107,7 @@ def moderator_user(db, make_user):
image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0))
image.save(file, "png") image.save(file, "png")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
user.save() user.save()
return user return user
@@ -121,7 +121,7 @@ def regular_user(db, make_user):
image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0))
image.save(file, "png") image.save(file, "png")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
user.save() user.save()
return user return user

View File

@@ -388,9 +388,9 @@ def test_news_create_requirements(
image = Image.new("RGB", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGB", size=(100, 100), color=(155, 0, 0))
image.save(file, "jpeg") image.save(file, "jpeg")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
else: else:
user.image = None user.profile_image = None
user.display_name = "Test User" if has_display_name else "" user.display_name = "Test User" if has_display_name else ""
user.save() user.save()

View File

@@ -282,7 +282,7 @@ class AllTypesCreateView(LoginRequiredMixin, TemplateView):
if not request.user.display_name: if not request.user.display_name:
missing_data.append("your name") missing_data.append("your name")
if not request.user.image: if not request.user.profile_image:
missing_data.append("a profile photo") missing_data.append("a profile photo")
if missing_data: if missing_data:

182
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
"cssnano": "^5.1.14", "cssnano": "^5.1.14",
"htmx": "^0.0.2", "htmx": "^0.0.2",
"marked": "^17.0.0",
"tailwindcss": "3.2.1" "tailwindcss": "3.2.1"
} }
}, },
@@ -54,9 +55,9 @@
} }
}, },
"node_modules/@svgdotjs/svg.js": { "node_modules/@svgdotjs/svg.js": {
"version": "3.2.4", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz",
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -132,9 +133,9 @@
} }
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.14.1", "version": "3.15.1",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.1.tgz",
"integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==", "integrity": "sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
@@ -160,9 +161,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.20", "version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -179,11 +180,11 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"browserslist": "^4.23.3", "browserslist": "^4.27.0",
"caniuse-lite": "^1.0.30001646", "caniuse-lite": "^1.0.30001754",
"fraction.js": "^4.3.7", "fraction.js": "^5.3.4",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"picocolors": "^1.0.1", "picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0" "postcss-value-parser": "^4.2.0"
}, },
"bin": { "bin": {
@@ -196,6 +197,15 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/baseline-browser-mapping": {
"version": "2.8.27",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz",
"integrity": "sha512-2CXFpkjVnY2FT+B6GrSYxzYf65BJWEqz5tIRHCvNsZZ2F3CmsCB37h8SpYgKG7y9C4YAeTipIPWG7EmFmhAeXA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -227,9 +237,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.0", "version": "4.28.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -246,10 +256,11 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001663", "baseline-browser-mapping": "^2.8.25",
"electron-to-chromium": "^1.5.28", "caniuse-lite": "^1.0.30001754",
"node-releases": "^2.0.18", "electron-to-chromium": "^1.5.249",
"update-browserslist-db": "^1.1.0" "node-releases": "^2.0.27",
"update-browserslist-db": "^1.1.4"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -280,9 +291,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001667", "version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -398,9 +409,9 @@
} }
}, },
"node_modules/css-what": { "node_modules/css-what": {
"version": "6.1.0", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -603,9 +614,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.34", "version": "1.5.250",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.34.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz",
"integrity": "sha512-/TZAiChbAflBNjCg+VvstbcwAtIL/VdMFO3NgRFIzBjpvPzWOTIbbO8kNb6RwU4bt9TP7K+3KqBKw/lOU+Y+GA==", "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/entities": { "node_modules/entities": {
@@ -627,16 +638,16 @@
} }
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3", "@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2", "glob-parent": "^5.1.2",
"merge2": "^1.3.0", "merge2": "^1.3.0",
"micromatch": "^4.0.4" "micromatch": "^4.0.8"
}, },
"engines": { "engines": {
"node": ">=8.6.0" "node": ">=8.6.0"
@@ -655,9 +666,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.17.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -676,15 +687,15 @@
} }
}, },
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "*" "node": "*"
}, },
"funding": { "funding": {
"type": "patreon", "type": "github",
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
@@ -757,9 +768,9 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.15.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hasown": "^2.0.2" "hasown": "^2.0.2"
@@ -822,6 +833,18 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/marked": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz",
"integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/mdn-data": { "node_modules/mdn-data": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
@@ -869,9 +892,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -887,9 +910,9 @@
} }
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.18", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/normalize-path": { "node_modules/normalize-path": {
@@ -950,9 +973,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@@ -977,9 +1000,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.47", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -996,8 +1019,8 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.11",
"picocolors": "^1.1.0", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
@@ -1117,9 +1140,19 @@
} }
}, },
"node_modules/postcss-js": { "node_modules/postcss-js": {
"version": "4.0.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"camelcase-css": "^2.0.1" "camelcase-css": "^2.0.1"
@@ -1127,10 +1160,6 @@
"engines": { "engines": {
"node": "^12 || ^14 || >= 16" "node": "^12 || ^14 || >= 16"
}, },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": { "peerDependencies": {
"postcss": "^8.4.21" "postcss": "^8.4.21"
} }
@@ -1566,26 +1595,29 @@
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.16.1",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
"bin": { "bin": {
"resolve": "bin/resolve" "resolve": "bin/resolve"
}, },
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.0.4", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"iojs": ">=1.0.0", "iojs": ">=1.0.0",
@@ -1743,9 +1775,9 @@
} }
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.1", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1763,7 +1795,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"escalade": "^3.2.0", "escalade": "^3.2.0",
"picocolors": "^1.1.0" "picocolors": "^1.1.1"
}, },
"bin": { "bin": {
"update-browserslist-db": "cli.js" "update-browserslist-db": "cli.js"

View File

@@ -19,6 +19,7 @@
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
"cssnano": "^5.1.14", "cssnano": "^5.1.14",
"htmx": "^0.0.2", "htmx": "^0.0.2",
"marked": "^17.0.0",
"tailwindcss": "3.2.1" "tailwindcss": "3.2.1"
} }
} }

View File

@@ -4,11 +4,11 @@ asgiref==3.10.0
# via # via
# -c ./requirements.txt # -c ./requirements.txt
# django # django
django==5.2.7 django==5.2.8
# via # via
# -c ./requirements.txt # -c ./requirements.txt
# django-debug-toolbar # django-debug-toolbar
django-debug-toolbar==6.0.0 django-debug-toolbar==6.1.0
# via -r ./requirements-dev.in # via -r ./requirements-dev.in
pydevd-pycharm==252.27397.106 pydevd-pycharm==252.27397.106
# via -r ./requirements-dev.in # via -r ./requirements-dev.in

View File

@@ -38,6 +38,8 @@ lxml
algoliasearch algoliasearch
openai openai
playwright playwright
wagtail
wagtail-markdown
# Logging # Logging
django-tracer django-tracer

View File

@@ -2,16 +2,18 @@
# uv pip compile ./requirements.in --no-strip-extras --output-file ./requirements.txt # uv pip compile ./requirements.in --no-strip-extras --output-file ./requirements.txt
aiohappyeyeballs==2.6.1 aiohappyeyeballs==2.6.1
# via aiohttp # via aiohttp
aiohttp==3.13.1 aiohttp==3.13.2
# via algoliasearch # via algoliasearch
aiosignal==1.4.0 aiosignal==1.4.0
# via aiohttp # via aiohttp
algoliasearch==4.30.0 algoliasearch==4.33.0
# via -r ./requirements.in # via -r ./requirements.in
amqp==5.3.1 amqp==5.3.1
# via kombu # via kombu
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
anyascii==0.3.3
# via wagtail
anyio==4.11.0 anyio==4.11.0
# via # via
# httpx # httpx
@@ -37,16 +39,20 @@ attrs==25.4.0
# aiohttp # aiohttp
# interrogate # interrogate
beautifulsoup4==4.14.2 beautifulsoup4==4.14.2
# via -r ./requirements.in # via
# -r ./requirements.in
# wagtail
billiard==4.2.2 billiard==4.2.2
# via celery # via celery
black==25.9.0 black==25.11.0
# via -r ./requirements.in # via -r ./requirements.in
boto3==1.40.56 bleach==4.1.0
# via wagtail-markdown
boto3==1.40.74
# via # via
# -r ./requirements.in # -r ./requirements.in
# django-bakery # django-bakery
botocore==1.40.56 botocore==1.40.74
# via # via
# boto3 # boto3
# s3transfer # s3transfer
@@ -56,7 +62,7 @@ bumpversion==0.6.0
# via -r ./requirements.in # via -r ./requirements.in
celery==5.5.3 celery==5.5.3
# via -r ./requirements.in # via -r ./requirements.in
certifi==2025.10.5 certifi==2025.11.12
# via # via
# elasticsearch # elasticsearch
# httpcore # httpcore
@@ -92,7 +98,7 @@ colorama==0.4.6
# via interrogate # via interrogate
contourpy==1.3.3 contourpy==1.3.3
# via matplotlib # via matplotlib
coverage[toml]==7.11.0 coverage[toml]==7.11.3
# via pytest-cov # via pytest-cov
cryptography==46.0.3 cryptography==46.0.3
# via # via
@@ -103,6 +109,8 @@ cycler==0.12.1
# via matplotlib # via matplotlib
decorator==5.2.1 decorator==5.2.1
# via ipython # via ipython
defusedxml==0.7.1
# via willow
distlib==0.4.0 distlib==0.4.0
# via virtualenv # via virtualenv
distro==1.9.0 distro==1.9.0
@@ -111,7 +119,7 @@ dj-database-url==3.0.1
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.2.7 django==5.2.8
# via # via
# -r ./requirements.in # -r ./requirements.in
# dj-database-url # dj-database-url
@@ -121,21 +129,31 @@ django==5.2.7
# django-cors-headers # django-cors-headers
# django-db-geventpool # django-db-geventpool
# django-extensions # django-extensions
# django-filter
# django-haystack # django-haystack
# django-health-check # django-health-check
# django-js-asset # django-js-asset
# django-modelcluster
# django-oauth-toolkit # django-oauth-toolkit
# django-permissionedforms
# django-redis # django-redis
# django-storages # django-storages
# django-stubs-ext
# django-taggit
# django-tasks
# django-treebeard
# djangorestframework # djangorestframework
# laces
# model-bakery # model-bakery
# modelsearch
# wagtail
django-admin-env-notice==1.0.1 django-admin-env-notice==1.0.1
# via -r ./requirements.in # via -r ./requirements.in
django-allauth[socialaccount]==65.12.1 django-allauth[socialaccount]==65.13.0
# via -r ./requirements.in # via -r ./requirements.in
django-anymail[mailgun]==13.1 django-anymail[mailgun]==13.1
# via -r ./requirements.in # via -r ./requirements.in
django-appconf==1.1.0 django-appconf==1.2.0
# via django-imagekit # via django-imagekit
django-bakery==0.13.5 django-bakery==0.13.5
# via -r ./requirements.in # via -r ./requirements.in
@@ -145,12 +163,14 @@ django-click==2.4.1
# via -r ./requirements.in # via -r ./requirements.in
django-cors-headers==4.9.0 django-cors-headers==4.9.0
# via -r ./requirements.in # via -r ./requirements.in
django-countries==7.6.1 django-countries==8.1.0
# via -r ./requirements.in # via -r ./requirements.in
django-db-geventpool==4.0.8 django-db-geventpool==4.0.8
# via -r ./requirements.in # via -r ./requirements.in
django-extensions==4.1 django-extensions==4.1
# via -r ./requirements.in # via -r ./requirements.in
django-filter==25.2
# via wagtail
django-haystack==3.3.0 django-haystack==3.3.0
# via -r ./requirements.in # via -r ./requirements.in
django-health-check==3.20.0 django-health-check==3.20.0
@@ -159,36 +179,58 @@ django-imagekit==6.0.0
# via -r ./requirements.in # via -r ./requirements.in
django-js-asset==3.1.2 django-js-asset==3.1.2
# via django-mptt # via django-mptt
django-modelcluster==6.4
# via wagtail
django-mptt==0.14.0 django-mptt==0.14.0
# via -r ./requirements.in # via -r ./requirements.in
django-oauth-toolkit==3.1.0 django-oauth-toolkit==3.1.0
# via -r ./requirements.in # via -r ./requirements.in
django-permissionedforms==0.1
# via wagtail
django-redis==6.0.0 django-redis==6.0.0
# via -r ./requirements.in # via -r ./requirements.in
django-storages==1.14.6 django-storages==1.14.6
# via -r ./requirements.in # via -r ./requirements.in
django-stubs-ext==5.2.7
# via django-tasks
django-taggit==6.1.0
# via wagtail
django-tasks==0.9.0
# via
# modelsearch
# wagtail
django-test-plus==2.3.0 django-test-plus==2.3.0
# via -r ./requirements.in # via -r ./requirements.in
django-tracer==0.9.3 django-tracer==0.9.3
# via -r ./requirements.in # via -r ./requirements.in
django-upgrade==1.29.0 django-treebeard==4.7.1
# via wagtail
django-upgrade==1.29.1
# via -r ./requirements.in # via -r ./requirements.in
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
# via -r ./requirements.in # via -r ./requirements.in
djangorestframework==3.16.1 djangorestframework==3.16.1
# via -r ./requirements.in # via
# -r ./requirements.in
# wagtail
draftjs-exporter==5.1.0
# via wagtail
elasticsearch==7.9.1 elasticsearch==7.9.1
# via -r ./requirements.in # via -r ./requirements.in
environs[django]==14.3.0 environs[django]==14.5.0
# via -r ./requirements.in # via -r ./requirements.in
et-xmlfile==2.0.0
# via openpyxl
executing==2.2.1 executing==2.2.1
# via stack-data # via stack-data
faker==37.11.0 faker==38.0.0
# via -r ./requirements.in # via -r ./requirements.in
fastcore==1.8.13 fastcore==1.8.16
# via ghapi # via ghapi
filelock==3.20.0 filelock==3.20.0
# via virtualenv # via virtualenv
filetype==1.2.0
# via willow
fonttools==4.60.1 fonttools==4.60.1
# via matplotlib # via matplotlib
frozenlist==1.8.0 frozenlist==1.8.0
@@ -226,7 +268,7 @@ iniconfig==2.3.0
# via pytest # via pytest
interrogate==1.7.0 interrogate==1.7.0
# via -r ./requirements.in # via -r ./requirements.in
ipython==9.6.0 ipython==9.7.0
# via -r ./requirements.in # via -r ./requirements.in
ipython-pygments-lexers==1.1.1 ipython-pygments-lexers==1.1.1
# via ipython # via ipython
@@ -234,7 +276,7 @@ itsdangerous==2.2.0
# via -r ./requirements.in # via -r ./requirements.in
jedi==0.19.2 jedi==0.19.2
# via ipython # via ipython
jiter==0.11.1 jiter==0.12.0
# via openai # via openai
jmespath==1.0.1 jmespath==1.0.1
# via # via
@@ -250,13 +292,17 @@ kiwisolver==1.4.9
# via matplotlib # via matplotlib
kombu==5.5.4 kombu==5.5.4
# via celery # via celery
laces==0.1.2
# via wagtail
lxml==6.0.2 lxml==6.0.2
# via -r ./requirements.in # via -r ./requirements.in
marshmallow==4.0.1 markdown==3.10
# via wagtail-markdown
marshmallow==4.1.0
# via environs # via environs
matplotlib==3.10.7 matplotlib==3.10.7
# via wordcloud # via wordcloud
matplotlib-inline==0.1.7 matplotlib-inline==0.2.1
# via ipython # via ipython
minio==7.2.18 minio==7.2.18
# via -r ./requirements.in # via -r ./requirements.in
@@ -264,6 +310,8 @@ mistletoe==1.5.0
# via -r ./requirements.in # via -r ./requirements.in
model-bakery==1.20.5 model-bakery==1.20.5
# via -r ./requirements.in # via -r ./requirements.in
modelsearch==1.1.1
# via wagtail
multidict==6.7.0 multidict==6.7.0
# via # via
# aiohttp # aiohttp
@@ -281,11 +329,14 @@ oauthlib==3.3.1
# via # via
# django-allauth # django-allauth
# django-oauth-toolkit # django-oauth-toolkit
openai==2.6.0 openai==2.8.0
# via -r ./requirements.in # via -r ./requirements.in
openpyxl==3.1.5
# via wagtail
packaging==25.0 packaging==25.0
# via # via
# black # black
# bleach
# django-haystack # django-haystack
# fastcore # fastcore
# ghapi # ghapi
@@ -306,18 +357,22 @@ pillow==12.0.0
# -r ./requirements.in # -r ./requirements.in
# matplotlib # matplotlib
# pilkit # pilkit
# pillow-heif
# wagtail
# wordcloud # wordcloud
pillow-heif==1.1.1
# via willow
platformdirs==4.5.0 platformdirs==4.5.0
# via # via
# black # black
# virtualenv # virtualenv
playwright==1.55.0 playwright==1.56.0
# via -r ./requirements.in # via -r ./requirements.in
pluggy==1.6.0 pluggy==1.6.0
# via # via
# pytest # pytest
# pytest-cov # pytest-cov
pre-commit==4.3.0 pre-commit==4.4.0
# via -r ./requirements.in # via -r ./requirements.in
prompt-toolkit==3.0.52 prompt-toolkit==3.0.52
# via # via
@@ -341,11 +396,11 @@ pycparser==2.23
# via cffi # via cffi
pycryptodome==3.23.0 pycryptodome==3.23.0
# via minio # via minio
pydantic==2.12.3 pydantic==2.12.4
# via # via
# algoliasearch # algoliasearch
# openai # openai
pydantic-core==2.41.4 pydantic-core==2.41.5
# via pydantic # via pydantic
pyee==13.0.0 pyee==13.0.0
# via playwright # via playwright
@@ -360,7 +415,7 @@ pyjwt[crypto]==2.10.1
# redis # redis
pyparsing==3.2.5 pyparsing==3.2.5
# via matplotlib # via matplotlib
pytest==8.4.2 pytest==9.0.1
# via # via
# -r ./requirements.in # -r ./requirements.in
# pytest-cov # pytest-cov
@@ -376,13 +431,13 @@ python-dateutil==2.9.0.post0
# botocore # botocore
# celery # celery
# matplotlib # matplotlib
python-dotenv==1.1.1 python-dotenv==1.2.1
# via environs # via environs
python-frontmatter==1.1.0 python-frontmatter==1.1.0
# via -r ./requirements.in # via -r ./requirements.in
python-json-logger==4.0.0 python-json-logger==4.0.0
# via -r ./requirements.in # via -r ./requirements.in
pytokens==0.2.0 pytokens==0.3.0
# via black # via black
pyyaml==6.0.3 pyyaml==6.0.3
# via # via
@@ -401,20 +456,20 @@ requests==2.32.5
# django-anymail # django-anymail
# django-oauth-toolkit # django-oauth-toolkit
# responses # responses
# wagtail
responses==0.25.8 responses==0.25.8
# via -r ./requirements.in # via -r ./requirements.in
s3transfer==0.14.0 s3transfer==0.14.0
# via boto3 # via boto3
setuptools==80.9.0 setuptools==80.9.0
# via # via fs
# fs
# zope-event
six==1.17.0 six==1.17.0
# via # via
# bleach
# django-bakery # django-bakery
# fs # fs
# python-dateutil # python-dateutil
slack-sdk==3.37.0 slack-sdk==3.38.0
# via -r ./requirements.in # via -r ./requirements.in
sniffio==1.3.1 sniffio==1.3.1
# via # via
@@ -426,10 +481,12 @@ sqlparse==0.5.3
# via django # via django
stack-data==0.6.3 stack-data==0.6.3
# via ipython # via ipython
structlog==25.4.0 structlog==25.5.0
# via -r ./requirements.in # via -r ./requirements.in
tabulate==0.9.0 tabulate==0.9.0
# via interrogate # via interrogate
telepath==0.3.1
# via wagtail
tokenize-rt==6.2.0 tokenize-rt==6.2.0
# via django-upgrade # via django-upgrade
tqdm==4.67.1 tqdm==4.67.1
@@ -442,6 +499,8 @@ typing-extensions==4.15.0
# via # via
# beautifulsoup4 # beautifulsoup4
# django-countries # django-countries
# django-stubs-ext
# django-tasks
# jwcrypto # jwcrypto
# minio # minio
# openai # openai
@@ -466,26 +525,36 @@ urllib3==2.5.0
# minio # minio
# requests # requests
# responses # responses
uv==0.9.5 uv==0.9.9
# via -r ./requirements.in # via -r ./requirements.in
vine==5.1.0 vine==5.1.0
# via # via
# amqp # amqp
# celery # celery
# kombu # kombu
virtualenv==20.35.3 virtualenv==20.35.4
# via pre-commit # via pre-commit
wagtail==7.2
# via
# -r ./requirements.in
# wagtail-markdown
wagtail-markdown==0.13.0
# via -r ./requirements.in
wcwidth==0.2.14 wcwidth==0.2.14
# via prompt-toolkit # via prompt-toolkit
webencodings==0.5.1
# via bleach
wheel==0.45.1 wheel==0.45.1
# via -r ./requirements.in # via -r ./requirements.in
whitenoise==6.11.0 whitenoise==6.11.0
# via -r ./requirements.in # via -r ./requirements.in
willow[heif]==1.12.0
# via wagtail
wordcloud==1.9.4 wordcloud==1.9.4
# via -r ./requirements.in # via -r ./requirements.in
yarl==1.22.0 yarl==1.22.0
# via aiohttp # via aiohttp
zope-event==6.0 zope-event==6.1
# via gevent # via gevent
zope-interface==8.0.1 zope-interface==8.1
# via gevent # via gevent

View File

@@ -46,3 +46,80 @@ code, pre {
.email-block {display:flex; align-items:center;margin:auto; justify-content:center;} .email-block {display:flex; align-items:center;margin:auto; justify-content:center;}
.email-button {margin-left:1rem; background-color:#0284c7; color:white; font-weight:bold; padding:5px 10px; border:0;} .email-button {margin-left:1rem; background-color:#0284c7; color:white; font-weight:bold; padding:5px 10px; border:0;}
.email-button:hover {background-color:#006394} .email-button:hover {background-color:#006394}
/* Pygments output */
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #f8f8f8; }
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
.codehilite .err { border: 1px solid #F00 } /* Error */
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.codehilite .o { color: #666 } /* Operator */
.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #9C6500 } /* Comment.Preproc */
.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.codehilite .gr { color: #E40000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #008400 } /* Generic.Inserted */
.codehilite .go { color: #717171 } /* Generic.Output */
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #04D } /* Generic.Traceback */
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #B00040 } /* Keyword.Type */
.codehilite .m { color: #666 } /* Literal.Number */
.codehilite .s { color: #BA2121 } /* Literal.String */
.codehilite .na { color: #687822 } /* Name.Attribute */
.codehilite .nb { color: #008000 } /* Name.Builtin */
.codehilite .nc { color: #00F; font-weight: bold } /* Name.Class */
.codehilite .no { color: #800 } /* Name.Constant */
.codehilite .nd { color: #A2F } /* Name.Decorator */
.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */
.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.codehilite .nf { color: #00F } /* Name.Function */
.codehilite .nl { color: #767600 } /* Name.Label */
.codehilite .nn { color: #00F; font-weight: bold } /* Name.Namespace */
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #19177C } /* Name.Variable */
.codehilite .ow { color: #A2F; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #BBB } /* Text.Whitespace */
.codehilite .mb { color: #666 } /* Literal.Number.Bin */
.codehilite .mf { color: #666 } /* Literal.Number.Float */
.codehilite .mh { color: #666 } /* Literal.Number.Hex */
.codehilite .mi { color: #666 } /* Literal.Number.Integer */
.codehilite .mo { color: #666 } /* Literal.Number.Oct */
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.codehilite .sx { color: #008000 } /* Literal.String.Other */
.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #00F } /* Name.Function.Magic */
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
.codehilite .il { color: #666 } /* Literal.Number.Integer.Long */

74
static/js/marked.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
(function () {
// Runs on the page editor; attaches a paste handler to Draftail editors.
function isMarkdown(text) {
// Very light heuristic: backticks or fenced code or headings/bullets
return /`[^`]+`|(^|\n)```|(^|\n)#{1,6}\s|(^|\n)[*-]\s/.test(text);
}
function convertAndInsertMarkdown(evt, editorEl) {
try {
const md = evt.clipboardData.getData("text/markdown") || evt.clipboardData.getData("text/plain");
if (!md || !isMarkdown(md)) return false;
// Convert markdown -> HTML
const html = window.marked.parse(md, { mangle: false, headerIds: false });
// Stop the default paste; inject HTML instead
evt.preventDefault();
// Use the browser to paste HTML so Draftail can apply its from_html rules.
// Create a temp contenteditable to execCommand('insertHTML')
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return false;
// Some Draftail builds support 'insertHTML' directly via execCommand:
document.execCommand("insertHTML", false, html);
return true;
} catch (_) {
return false;
}
}
function attach() {
// Draftail editor root elements have [data-draftail-input]
document.querySelectorAll("[data-draftail-input]").forEach((wrapper) => {
const editorEl = wrapper.querySelector("[contenteditable='true']");
if (!editorEl || editorEl.__md_paste_bound) return;
editorEl.__md_paste_bound = true;
editorEl.addEventListener("paste", (evt) => {
// Prefer text/markdown if provided by the clipboard
const hasMarkdownMime = evt.clipboardData && Array.from(evt.clipboardData.types || []).includes("text/markdown");
if (hasMarkdownMime || isMarkdown(evt.clipboardData.getData("text/plain") || "")) {
convertAndInsertMarkdown(evt, editorEl);
}
});
});
}
// Attach when the editor loads and also after Wagtail re-initializes editors
document.addEventListener("DOMContentLoaded", attach);
document.addEventListener("wagtail:document-loaded", attach);
})();

View File

@@ -0,0 +1 @@
{% include 'marketing/program_page.html' %}

View File

@@ -0,0 +1,37 @@
{# Generic landing page; don't use directly #}
{% load static wagtailcore_tags %}
<html>
<head>
<link rel="stylesheet" href="{% static 'css/landing-style.css' %}" />
</head>
<body>
<h1>{{ page.email_capture_intro }}</h1>
{% if messages %}
{# Expect a success message from submitting the form #}
<div class="email-block">
<div class="messages">
{% for message in messages %}
<p{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
{% endfor %}
</div>
</div>
{% else %}
<div class="email-block">
<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{{ form.email.errors }}
{{ form.email }}
<button class="email-button" type="submit">Get C++ updates</button>
</form>
</div>
<p>{{ page.privacy_blurb }}</p>
{% endif %}
<hr/>
{{ page.body }}
</body>
</html>

View File

@@ -0,0 +1 @@
{% include 'marketing/landing_page.html' %}

View File

@@ -39,7 +39,7 @@ class EmailUserAdmin(UserAdmin):
{ {
"fields": ( "fields": (
"can_update_image", "can_update_image",
"image", "profile_image",
) )
}, },
), ),

View File

@@ -119,11 +119,11 @@ class CustomClearableFileInput(forms.ClearableFileInput):
class UserProfilePhotoForm(forms.ModelForm): class UserProfilePhotoForm(forms.ModelForm):
image = forms.FileField(widget=CustomClearableFileInput, required=False) profile_image = forms.FileField(widget=CustomClearableFileInput, required=False)
class Meta: class Meta:
model = User model = User
fields = ["image"] fields = ["profile_image"]
def clean(self): def clean(self):
"""Ensure a user can't update their photo if they """Ensure a user can't update their photo if they
@@ -137,25 +137,24 @@ class UserProfilePhotoForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
# Temporarily store the old image # Temporarily store the old image
old_image = self.instance.image old_image = self.instance.profile_image
# Save the new image # Save the new image
user = super().save(commit=False) user = super().save(commit=False)
if not old_image: if not old_image:
# reset image on image delete checked # reset image on image delete checked
user.image_uploaded = False user.image_uploaded = False
elif self.cleaned_data["image"] != old_image: elif self.cleaned_data["profile_image"] != old_image:
# Delete the old image file if there's a new image being uploaded # Delete the old image file if there's a new image being uploaded
old_image.delete(save=False) old_image.delete(save=False)
if self.cleaned_data.get("image"): if new_image := self.cleaned_data.get("profile_image"):
new_image = self.cleaned_data["image"]
_, file_extension = os.path.splitext(new_image.name) _, file_extension = os.path.splitext(new_image.name)
# Strip the leading period from the file extension. # Strip the leading period from the file extension.
file_extension = file_extension.lstrip(".") file_extension = file_extension.lstrip(".")
new_image.name = f"{user.profile_image_filename_root}.{file_extension}" new_image.name = f"{user.profile_image_filename_root}.{file_extension}"
user.image = new_image user.profile_image = new_image
user.image_uploaded = True user.image_uploaded = True
if commit: if commit:

View File

@@ -18,11 +18,11 @@ class Command(BaseCommand):
see your photo in the admin for whoever your first user is. see your photo in the admin for whoever your first user is.
""" """
user = User.objects.first() user = User.objects.first()
user.image = None user.profile_image = None
user.github_username = "testing" user.github_username = "testing"
user.save() user.save()
user.refresh_from_db() user.refresh_from_db()
assert bool(user.image) is False assert bool(user.profile_image) is False
user.save_image_from_github() user.save_image_from_github()
user.refresh_from_db() user.refresh_from_db()
assert bool(user.image) is True assert bool(user.profile_image) is True

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-10 17:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("users", "0019_user_image_uploaded"),
]
operations = [
migrations.RenameField(
model_name="user",
old_name="image",
new_name="profile_image",
),
]

View File

@@ -204,14 +204,14 @@ class User(BaseUser):
is_commit_author_name_overridden = models.BooleanField( is_commit_author_name_overridden = models.BooleanField(
default=False, help_text="Select to override the commit author with Username" default=False, help_text="Select to override the commit author with Username"
) )
image = models.FileField( profile_image = models.FileField(
upload_to="profile-images", upload_to="profile-images",
null=True, null=True,
blank=True, blank=True,
validators=[image_validator, max_file_size_validator], validators=[image_validator, max_file_size_validator],
) )
image_thumbnail = ImageSpecField( image_thumbnail = ImageSpecField(
source="image", source="profile_image",
processors=[ResizeToFill(100, 100)], processors=[ResizeToFill(100, 100)],
format="JPEG", format="JPEG",
options={"quality": 90}, options={"quality": 90},
@@ -269,7 +269,7 @@ class User(BaseUser):
response = requests.get(avatar_url) response = requests.get(avatar_url)
filename = f"{self.profile_image_filename_root}.png" filename = f"{self.profile_image_filename_root}.png"
self.image.save(filename, ContentFile(response.content), save=True) self.profile_image.save(filename, ContentFile(response.content), save=True)
@cached_property @cached_property
def profile_image_filename_root(self): def profile_image_filename_root(self):
@@ -285,7 +285,7 @@ class User(BaseUser):
def get_thumbnail_url(self): def get_thumbnail_url(self):
# convenience method for templates # convenience method for templates
if self.image and self.image_thumbnail: if self.profile_image and self.image_thumbnail:
with suppress(AttributeError, MissingSource): with suppress(AttributeError, MissingSource):
return getattr(self.image_thumbnail, "url", None) return getattr(self.image_thumbnail, "url", None)
@@ -324,9 +324,9 @@ class User(BaseUser):
self.last_name = "Doe" self.last_name = "Doe"
self.display_name = "John Doe" self.display_name = "John Doe"
self.email = "deleted-{}@example.com".format(uuid.uuid4()) self.email = "deleted-{}@example.com".format(uuid.uuid4())
image = self.image image = self.profile_image
transaction.on_commit(lambda: image.delete()) transaction.on_commit(lambda: image.delete())
self.image = None self.profile_image = None
self.image_thumbnail = None self.image_thumbnail = None
self.delete_permanently_at = None self.delete_permanently_at = None
self.save() self.save()

View File

@@ -32,14 +32,14 @@ class CurrentUserSerializer(serializers.ModelSerializer):
"id", "id",
"email", "email",
"display_name", "display_name",
"image", "profile_image",
"date_joined", "date_joined",
"data", "data",
) )
read_only_fields = ( read_only_fields = (
"id", "id",
"email", # Users shouldn't change their email this way "email", # Users shouldn't change their email this way
"image", "profile_image",
"date_joined", "date_joined",
) )

View File

@@ -25,7 +25,7 @@ def user(db):
image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0))
image.save(file, "png") image.save(file, "png")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
user.set_password("password") user.set_password("password")
user.save() user.save()
@@ -50,7 +50,7 @@ def staff_user(db):
image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0))
image.save(file, "png") image.save(file, "png")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
user.set_password("password") user.set_password("password")
user.save() user.save()
@@ -76,7 +76,7 @@ def super_user(db):
image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0)) image = Image.new("RGBA", size=(100, 100), color=(155, 0, 0))
image.save(file, "png") image.save(file, "png")
file.seek(0) file.seek(0)
user.image.save(filename, file) user.profile_image.save(filename, file)
user.set_password("password") user.set_password("password")
user.save() user.save()

View File

@@ -189,10 +189,10 @@ def test_user_profile_photo_form_save(user):
return file return file
old_image = ImageFile(create_test_image_file(filename="initial_image.png")) old_image = ImageFile(create_test_image_file(filename="initial_image.png"))
user.image.save("initial_image.png", old_image) user.profile_image.save("initial_image.png", old_image)
# Make sure the initial image was saved # Make sure the initial image was saved
initial_path = user.image.path initial_path = user.profile_image.path
assert initial_path is not None assert initial_path is not None
# Create new image for upload # Create new image for upload
@@ -202,8 +202,8 @@ def test_user_profile_photo_form_save(user):
content_type="image/jpeg", content_type="image/jpeg",
) )
form = UserProfilePhotoForm({"image": new_image}, instance=user) form = UserProfilePhotoForm({"profile_image": new_image}, instance=user)
assert form.is_valid() assert form.is_valid()
updated_user = form.save() updated_user = form.save()
updated_user.refresh_from_db() updated_user.refresh_from_db()
assert str(user.pk) in updated_user.image.path assert str(user.pk) in updated_user.profile_image.path

View File

@@ -40,7 +40,7 @@ def test_user_model_image_validator(user):
valid_image = SimpleUploadedFile( valid_image = SimpleUploadedFile(
"test.jpg", b"file_content", content_type="image/jpeg" "test.jpg", b"file_content", content_type="image/jpeg"
) )
user.image = valid_image user.profile_image = valid_image
# This should not raise any errors # This should not raise any errors
user.full_clean() user.full_clean()
@@ -48,7 +48,7 @@ def test_user_model_image_validator(user):
invalid_image = SimpleUploadedFile( invalid_image = SimpleUploadedFile(
"test.pdf", b"file_content", content_type="application/pdf" "test.pdf", b"file_content", content_type="application/pdf"
) )
user.image = invalid_image user.profile_image = invalid_image
# This should raise a ValidationError # This should raise a ValidationError
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
user.full_clean() user.full_clean()
@@ -61,7 +61,7 @@ def test_user_model_image_file_size(user):
valid_image = SimpleUploadedFile( valid_image = SimpleUploadedFile(
"test.jpg", b"a" * (1 * 1024 * 1024 - 1), content_type="image/jpeg" "test.jpg", b"a" * (1 * 1024 * 1024 - 1), content_type="image/jpeg"
) )
user.image = valid_image user.profile_image = valid_image
# This should not raise any errors # This should not raise any errors
user.full_clean() user.full_clean()
@@ -69,7 +69,7 @@ def test_user_model_image_file_size(user):
invalid_image = SimpleUploadedFile( invalid_image = SimpleUploadedFile(
"too_large.jpg", b"a" * (1 * 1024 * 1024 + 1), content_type="image/jpeg" "too_large.jpg", b"a" * (1 * 1024 * 1024 + 1), content_type="image/jpeg"
) )
user.image = invalid_image user.profile_image = invalid_image
# This should raise a ValidationError for file size # This should raise a ValidationError for file size
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
user.full_clean() user.full_clean()

View File

@@ -130,7 +130,7 @@ def test_current_user_profile_view_post_valid_photo(user, tp):
) )
assert response.status_code == 200 assert response.status_code == 200
user.refresh_from_db() user.refresh_from_db()
assert user.image assert user.profile_image
# confirm that password was not changed, as these are on the same screen # confirm that password was not changed, as these are on the same screen
assert user.check_password("password") assert user.check_password("password")

171
yarn.lock
View File

@@ -24,9 +24,9 @@
fastq "^1.6.0" fastq "^1.6.0"
"@svgdotjs/svg.js@^3.2.4": "@svgdotjs/svg.js@^3.2.4":
version "3.2.4" version "3.2.5"
resolved "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz" resolved "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz"
integrity sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg== integrity sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==
"@tailwindcss/forms@0.5.3": "@tailwindcss/forms@0.5.3":
version "0.5.3" version "0.5.3"
@@ -72,9 +72,9 @@ acorn@^7.0.0:
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
alpinejs@^3.10.2: alpinejs@^3.10.2:
version "3.14.1" version "3.15.1"
resolved "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz" resolved "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.1.tgz"
integrity sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ== integrity sha512-HLO1TtiE92VajFHtLLPK8BWaK1YepV/uj31UrfoGnQ00lyFOJZ+oVY3F0DghPAwvg8sLU79pmjGQSytERa2gEg==
dependencies: dependencies:
"@vue/reactivity" "~3.1.1" "@vue/reactivity" "~3.1.1"
@@ -92,17 +92,22 @@ arg@^5.0.2:
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
autoprefixer@^10.4.12: autoprefixer@^10.4.12:
version "10.4.20" version "10.4.22"
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz"
integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== integrity sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==
dependencies: dependencies:
browserslist "^4.23.3" browserslist "^4.27.0"
caniuse-lite "^1.0.30001646" caniuse-lite "^1.0.30001754"
fraction.js "^4.3.7" fraction.js "^5.3.4"
normalize-range "^0.1.2" normalize-range "^0.1.2"
picocolors "^1.0.1" picocolors "^1.1.1"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
baseline-browser-mapping@^2.8.25:
version "2.8.27"
resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz"
integrity sha512-2CXFpkjVnY2FT+B6GrSYxzYf65BJWEqz5tIRHCvNsZZ2F3CmsCB37h8SpYgKG7y9C4YAeTipIPWG7EmFmhAeXA==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz"
@@ -120,15 +125,16 @@ braces@^3.0.3, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.1.1" fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.21.4, browserslist@^4.23.3, "browserslist@>= 4.21.0": browserslist@^4.0.0, browserslist@^4.21.4, browserslist@^4.27.0, "browserslist@>= 4.21.0":
version "4.24.0" version "4.28.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz"
integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
dependencies: dependencies:
caniuse-lite "^1.0.30001663" baseline-browser-mapping "^2.8.25"
electron-to-chromium "^1.5.28" caniuse-lite "^1.0.30001754"
node-releases "^2.0.18" electron-to-chromium "^1.5.249"
update-browserslist-db "^1.1.0" node-releases "^2.0.27"
update-browserslist-db "^1.1.4"
camelcase-css@^2.0.1: camelcase-css@^2.0.1:
version "2.0.1" version "2.0.1"
@@ -145,10 +151,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2" lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001754:
version "1.0.30001667" version "1.0.30001754"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz"
integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==
chokidar@^3.5.3: chokidar@^3.5.3:
version "3.6.0" version "3.6.0"
@@ -205,9 +211,9 @@ css-tree@^1.1.2, css-tree@^1.1.3:
source-map "^0.6.1" source-map "^0.6.1"
css-what@^6.0.1: css-what@^6.0.1:
version "6.1.0" version "6.2.2"
resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" resolved "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==
cssesc@^3.0.0: cssesc@^3.0.0:
version "3.0.0" version "3.0.0"
@@ -324,10 +330,10 @@ domutils@^2.8.0:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domhandler "^4.2.0" domhandler "^4.2.0"
electron-to-chromium@^1.5.28: electron-to-chromium@^1.5.249:
version "1.5.34" version "1.5.250"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.34.tgz" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz"
integrity sha512-/TZAiChbAflBNjCg+VvstbcwAtIL/VdMFO3NgRFIzBjpvPzWOTIbbO8kNb6RwU4bt9TP7K+3KqBKw/lOU+Y+GA== integrity sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==
entities@^2.0.0: entities@^2.0.0:
version "2.2.0" version "2.2.0"
@@ -340,20 +346,20 @@ escalade@^3.2.0:
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
fast-glob@^3.2.12: fast-glob@^3.2.12:
version "3.3.2" version "3.3.3"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
dependencies: dependencies:
"@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3" "@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2" glob-parent "^5.1.2"
merge2 "^1.3.0" merge2 "^1.3.0"
micromatch "^4.0.4" micromatch "^4.0.8"
fastq@^1.6.0: fastq@^1.6.0:
version "1.17.1" version "1.19.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz"
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
dependencies: dependencies:
reusify "^1.0.4" reusify "^1.0.4"
@@ -364,10 +370,10 @@ fill-range@^7.1.1:
dependencies: dependencies:
to-regex-range "^5.0.1" to-regex-range "^5.0.1"
fraction.js@^4.3.7: fraction.js@^5.3.4:
version "4.3.7" version "5.3.4"
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
fsevents@~2.3.2: fsevents@~2.3.2:
version "2.3.3" version "2.3.3"
@@ -419,10 +425,10 @@ is-binary-path@~2.1.0:
dependencies: dependencies:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-core-module@^2.13.0: is-core-module@^2.16.1:
version "2.15.1" version "2.16.1"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz"
integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
dependencies: dependencies:
hasown "^2.0.2" hasown "^2.0.2"
@@ -458,6 +464,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
marked@^17.0.0:
version "17.0.0"
resolved "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz"
integrity sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==
mdn-data@2.0.14: mdn-data@2.0.14:
version "2.0.14" version "2.0.14"
resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz"
@@ -468,7 +479,7 @@ merge2@^1.3.0:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4, micromatch@^4.0.5: micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -486,15 +497,15 @@ minimist@^1.2.6:
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
nanoid@^3.3.7: nanoid@^3.3.11:
version "3.3.7" version "3.3.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
node-releases@^2.0.18: node-releases@^2.0.27:
version "2.0.18" version "2.0.27"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
@@ -528,10 +539,10 @@ path-parse@^1.0.7:
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.0" version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
@@ -599,9 +610,9 @@ postcss-import@^14.1.0:
resolve "^1.1.7" resolve "^1.1.7"
postcss-js@^4.0.0: postcss-js@^4.0.0:
version "4.0.1" version "4.1.0"
resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz" resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== integrity sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==
dependencies: dependencies:
camelcase-css "^2.0.1" camelcase-css "^2.0.1"
@@ -785,12 +796,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.0.0, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.4.17, postcss@^8.4.21, postcss@>=8.0.9: postcss@^8.0.0, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.4.17, postcss@^8.4.21, postcss@>=8.0.9:
version "8.4.47" version "8.5.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz" resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies: dependencies:
nanoid "^3.3.7" nanoid "^3.3.11"
picocolors "^1.1.0" picocolors "^1.1.1"
source-map-js "^1.2.1" source-map-js "^1.2.1"
queue-microtask@^1.2.2: queue-microtask@^1.2.2:
@@ -818,18 +829,18 @@ readdirp@~3.6.0:
picomatch "^2.2.1" picomatch "^2.2.1"
resolve@^1.1.7, resolve@^1.22.1: resolve@^1.1.7, resolve@^1.22.1:
version "1.22.8" version "1.22.11"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
dependencies: dependencies:
is-core-module "^2.13.0" is-core-module "^2.16.1"
path-parse "^1.0.7" path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0" supports-preserve-symlinks-flag "^1.0.0"
reusify@^1.0.4: reusify@^1.0.4:
version "1.0.4" version "1.1.0"
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
run-parallel@^1.1.9: run-parallel@^1.1.9:
version "1.2.0" version "1.2.0"
@@ -915,13 +926,13 @@ to-regex-range@^5.0.1:
dependencies: dependencies:
is-number "^7.0.0" is-number "^7.0.0"
update-browserslist-db@^1.1.0: update-browserslist-db@^1.1.4:
version "1.1.1" version "1.1.4"
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz"
integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==
dependencies: dependencies:
escalade "^3.2.0" escalade "^3.2.0"
picocolors "^1.1.0" picocolors "^1.1.1"
util-deprecate@^1.0.2: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"