Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate ffc016a891 Update dependency prettier to v2.8.8 2023-12-28 22:01:14 +00:00
100 changed files with 567 additions and 1653 deletions

View File

@ -23,7 +23,7 @@ static:
expire_in: 2 hours
pip:
image: python:3.12-slim
image: python:3.11-slim
stage: build
variables:
PIP_CACHE_DIR: $CI_PROJECT_DIR/.pip-cache
@ -45,7 +45,7 @@ pip:
expire_in: 2 hours
.python_test_template:
image: python:3.12-slim
image: python:3.11-slim
stage: test
dependencies:
- pip

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

View File

@ -1,6 +1,3 @@
{
"extends": ["stylelint-config-standard-scss", "stylelint-config-prettier-scss"],
"rules": {
"scss/at-extend-no-missing-placeholder": null
}
"extends": ["stylelint-config-standard-scss", "stylelint-config-prettier-scss"]
}

View File

@ -11,13 +11,10 @@ COPY ./static/src ./static/src
RUN npm run build
# The actual container
FROM python:3.12-slim as production
FROM python:3.11-slim as production
ENV VIRTUAL_ENV=/venv
# renovate: datasource=github-tags depName=just-containers/s6-overlay
ENV S6_OVERLAY_VERSION=3.1.6.2
RUN useradd website --create-home -u 1000 && mkdir /app $VIRTUAL_ENV && chown -R website /app $VIRTUAL_ENV
WORKDIR /app
@ -34,16 +31,13 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
&& apt-get autoremove && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://github.com/aptible/supercronic/releases/download/v0.2.1/supercronic-linux-amd64 -o /usr/local/bin/supercronic && chmod +x /usr/local/bin/supercronic
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
ENV PATH=$VIRTUAL_ENV/bin:$PATH \
PYTHONUNBUFFERED=1
EXPOSE 8000
RUN ln -fs /app/etc/nginx.conf /etc/nginx/sites-available/default && chown -R website /var/log/nginx
RUN ln -fs /app/etc/nginx.conf /etc/nginx/sites-available/default
USER website
@ -62,17 +56,18 @@ RUN cat ./etc/bashrc.sh >> ~/.bashrc
RUN SECRET_KEY=none python manage.py collectstatic --noinput --clear
COPY ./etc/s6-rc.d /etc/s6-overlay/s6-rc.d
ENTRYPOINT [ "/init" ]
CMD ["/app/etc/entrypoints/web"]
# Just dev stuff
FROM production as dev
COPY --chown=website .nvmrc ./
RUN curl https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash \
&& bash --login -c "nvm install --no-progress && nvm alias default $(nvm run --silent --version)"
# Swap user, so the following tasks can be run as root
USER root
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
RUN apt-get update --yes --quiet && apt-get install -y postgresql-client inotify-tools
RUN curl -sSf https://just.systems/install.sh | bash -s -- --to /usr/bin
@ -82,5 +77,4 @@ USER website
COPY --chown=website dev-requirements.txt ./
RUN pip install --no-cache -r dev-requirements.txt
ENTRYPOINT []
CMD sleep infinity

View File

@ -1,14 +1,13 @@
-r requirements.txt
honcho==1.1.0
black==23.12.1
django-browser-reload==1.12.1
black==23.7.0
django-browser-reload==1.11.0
django-debug-toolbar
types-requests
mypy==1.8.0
wagtail-factories==4.1.0
coverage==7.4.0
djlint==1.34.1
types-pyyaml
ruff==0.1.11
setuptools # required for Honcho to work on Python 3.12+
types-requests==2.31.0.1
mypy==1.5.1
wagtail-factories==4.0.0
coverage==7.3.0
djlint==1.31.0
types-pyyaml==6.0.12.9
ruff==0.0.278

View File

@ -4,6 +4,7 @@ services:
build:
context: ../../
target: dev
init: true
environment:
- QUEUE_STORE_URL=redis://redis/0
- DEBUG=true
@ -11,8 +12,6 @@ services:
- DATABASE_URL=postgres://website:website@db/website
volumes:
- ../../:/app
tmpfs:
- /tmp
depends_on:
- redis
- db

2
etc/s6-rc.d/nginx/run → etc/entrypoints/nginx Normal file → Executable file
View File

@ -1,4 +1,4 @@
#!/command/with-contenv bash
#!/usr/bin/env bash
set -e

7
etc/entrypoints/web Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
python manage.py migrate --noinput
exec gunicorn -c etc/gunicorn.conf.py

4
etc/s6-rc.d/rq/run → etc/entrypoints/worker Normal file → Executable file
View File

@ -1,7 +1,5 @@
#!/command/with-contenv bash
#!/usr/bin/env bash
set -e
cd /app
exec python manage.py rqworker --with-scheduler

View File

@ -1,10 +1,11 @@
wsgi_app = "website.wsgi:application"
accesslog = "-"
disable_redirect_access_to_syslog = True
preload_app = True
bind = "127.0.0.1:8080"
bind = "0.0.0.0:8080"
max_requests = 1200
max_requests_jitter = 50
forwarded_allow_ips = "*"
# Run additional threads so the GIL isn't sitting completely idle
threads = 4
# Run an additional thread so the GIL isn't sitting completely idle
threads = 2

View File

@ -1,22 +1,10 @@
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginxcache:10m max_size=150m inactive=24h;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
server {
listen 8000;
access_log /dev/stdout;
error_log /dev/stderr;
gzip_static on;
gzip on;
gzip_vary on;
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
@ -24,36 +12,30 @@ server {
more_set_headers "Server: Wouldn't you like to know";
server_tokens off;
proxy_buffers 32 4k;
proxy_connect_timeout 240;
proxy_headers_hash_bucket_size 128;
proxy_headers_hash_max_size 1024;
proxy_http_version 1.1;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Proxy "";
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_cache_valid 404 1m;
location / {
proxy_pass http://localhost:8080;
gzip_types *;
proxy_buffers 32 4k;
proxy_connect_timeout 240;
proxy_headers_hash_bucket_size 128;
proxy_headers_hash_max_size 1024;
proxy_http_version 1.1;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Proxy "";
proxy_pass http://django:8080;
}
location /static {
proxy_cache nginxcache;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://localhost:8080;
add_header Cache-Control "public, immutable, max-age=31536000";
alias /app/collected-static;
}
location /media {
proxy_cache nginxcache;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://localhost:8080;
add_header Cache-Control "public, immutable, max-age=3600";
alias /app/media;
}
}

View File

@ -1,7 +0,0 @@
#!/command/with-contenv bash
set -e
cd /app
exec supercronic etc/crontab

View File

@ -1 +0,0 @@
longrun

View File

@ -1,7 +0,0 @@
#!/command/with-contenv bash
set -e
cd /app
exec gunicorn -c etc/gunicorn.conf.py

View File

@ -1 +0,0 @@
longrun

View File

@ -1 +0,0 @@
oneshot

View File

@ -1 +0,0 @@
with-contenv bash -c "cd /app && python manage.py migrate --noinput"

View File

@ -1 +0,0 @@
longrun

View File

@ -1 +0,0 @@
longrun

View File

@ -9,7 +9,7 @@ DEV_COMPOSE := justfile_directory() + "/docker/dev/docker-compose.yml"
build:
docker-compose -f {{ DEV_COMPOSE }} pull
docker-compose -f {{ DEV_COMPOSE }} build
docker-compose -f {{ DEV_COMPOSE }} run --entrypoint=bash --rm --no-deps web -c "npm ci"
docker-compose -f {{ DEV_COMPOSE }} run --rm --no-deps web bash -lc "npm ci"
@compose +ARGS:
docker-compose -f {{ DEV_COMPOSE }} {{ ARGS }}
@ -52,9 +52,5 @@ lint_python:
docker-compose -f {{ DEV_COMPOSE }} up -d
docker-compose -f {{ DEV_COMPOSE }} exec web bash
@sh-root:
docker-compose -f {{ DEV_COMPOSE }} up -d
docker-compose -f {{ DEV_COMPOSE }} exec --user=root web bash
@down:
docker-compose -f {{ DEV_COMPOSE }} down

445
package-lock.json generated
View File

@ -9,25 +9,25 @@
"version": "0.0.0",
"dependencies": {
"@fontsource/fira-code": "5.0.2",
"@fortawesome/fontawesome-free": "6.5.2",
"@fortawesome/fontawesome-free": "6.4.0",
"@ledge/is-ie-11": "7.0.0",
"bulma": "0.9.4",
"elevator.js": "1.0.1",
"esbuild": "0.20.2",
"glightbox": "3.3.0",
"esbuild": "0.19.2",
"glightbox": "3.2.0",
"htmx.org": "1.9.2",
"lite-youtube-embed": "0.3.0",
"lodash.clamp": "4.0.3",
"lodash.debounce": "4.0.8",
"lodash.throttle": "4.1.1",
"npm-run-all": "4.1.5",
"sass": "1.75.0",
"sass": "1.67.0",
"shareon": "2.4.0"
},
"devDependencies": {
"eslint": "8.55.0",
"eslint-plugin-unicorn": "49.0.0",
"prettier": "2.7.1",
"prettier": "2.8.8",
"stylelint": "14.16.1",
"stylelint-config-prettier-scss": "0.0.1",
"stylelint-config-standard-scss": "6.1.0"
@ -94,25 +94,10 @@
"postcss-selector-parser": "^6.0.10"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
"cpu": [
"arm"
],
@ -125,9 +110,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
"cpu": [
"arm64"
],
@ -140,9 +125,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
"cpu": [
"x64"
],
@ -155,9 +140,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
"cpu": [
"arm64"
],
@ -170,9 +155,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
"cpu": [
"x64"
],
@ -185,9 +170,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
"cpu": [
"arm64"
],
@ -200,9 +185,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
"cpu": [
"x64"
],
@ -215,9 +200,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
"cpu": [
"arm"
],
@ -230,9 +215,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
"cpu": [
"arm64"
],
@ -245,9 +230,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
"cpu": [
"ia32"
],
@ -260,9 +245,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
"cpu": [
"loong64"
],
@ -275,9 +260,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
"cpu": [
"mips64el"
],
@ -290,9 +275,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
"cpu": [
"ppc64"
],
@ -305,9 +290,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
"cpu": [
"riscv64"
],
@ -320,9 +305,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
"cpu": [
"s390x"
],
@ -335,9 +320,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
"cpu": [
"x64"
],
@ -350,9 +335,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
"cpu": [
"x64"
],
@ -365,9 +350,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
"cpu": [
"x64"
],
@ -380,9 +365,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
"cpu": [
"x64"
],
@ -395,9 +380,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
"cpu": [
"arm64"
],
@ -410,9 +395,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
"cpu": [
"ia32"
],
@ -425,9 +410,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
"cpu": [
"x64"
],
@ -501,9 +486,9 @@
"integrity": "sha512-T3yzwKP/JFRYdBUHjDXQfRGp9EOI7+V0uCf9ky1rZXDzMUECxuUqTfBcj60CE3oRLzzSm9fgiEGYLSvzo/S/Fw=="
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
"integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
@ -1148,9 +1133,9 @@
}
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@ -1159,29 +1144,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
"@esbuild/android-arm": "0.19.2",
"@esbuild/android-arm64": "0.19.2",
"@esbuild/android-x64": "0.19.2",
"@esbuild/darwin-arm64": "0.19.2",
"@esbuild/darwin-x64": "0.19.2",
"@esbuild/freebsd-arm64": "0.19.2",
"@esbuild/freebsd-x64": "0.19.2",
"@esbuild/linux-arm": "0.19.2",
"@esbuild/linux-arm64": "0.19.2",
"@esbuild/linux-ia32": "0.19.2",
"@esbuild/linux-loong64": "0.19.2",
"@esbuild/linux-mips64el": "0.19.2",
"@esbuild/linux-ppc64": "0.19.2",
"@esbuild/linux-riscv64": "0.19.2",
"@esbuild/linux-s390x": "0.19.2",
"@esbuild/linux-x64": "0.19.2",
"@esbuild/netbsd-x64": "0.19.2",
"@esbuild/openbsd-x64": "0.19.2",
"@esbuild/sunos-x64": "0.19.2",
"@esbuild/win32-arm64": "0.19.2",
"@esbuild/win32-ia32": "0.19.2",
"@esbuild/win32-x64": "0.19.2"
}
},
"node_modules/escape-string-regexp": {
@ -1721,9 +1705,9 @@
}
},
"node_modules/glightbox": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.3.0.tgz",
"integrity": "sha512-SJukatHBZZ/POMOpLUQ6/dhXf/wJTDx1wZ/FwApjseXw2WrRj3Ze9DzNCFYzca0oU7RjXQhi9o02aIZ9SuCz1A=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.2.0.tgz",
"integrity": "sha512-iit1xYixqL4YVL+I2YJLfMeyJwvLi6FE6kY3qNKeZHEJgRIz80QU8Rm7YCyw1wOTgXvmNDnXGVhHOHRCwnDltQ=="
},
"node_modules/glob": {
"version": "7.2.3",
@ -2981,9 +2965,9 @@
}
},
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
@ -3319,9 +3303,9 @@
}
},
"node_modules/sass": {
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
"integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -4094,142 +4078,136 @@
"dev": true,
"requires": {}
},
"@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"optional": true
},
"@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
"optional": true
},
"@eslint-community/eslint-utils": {
@ -4276,9 +4254,9 @@
"integrity": "sha512-T3yzwKP/JFRYdBUHjDXQfRGp9EOI7+V0uCf9ky1rZXDzMUECxuUqTfBcj60CE3oRLzzSm9fgiEGYLSvzo/S/Fw=="
},
"@fortawesome/fontawesome-free": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
"integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
},
"@humanwhocodes/config-array": {
"version": "0.11.13",
@ -4755,33 +4733,32 @@
}
},
"esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
"requires": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
"@esbuild/android-arm": "0.19.2",
"@esbuild/android-arm64": "0.19.2",
"@esbuild/android-x64": "0.19.2",
"@esbuild/darwin-arm64": "0.19.2",
"@esbuild/darwin-x64": "0.19.2",
"@esbuild/freebsd-arm64": "0.19.2",
"@esbuild/freebsd-x64": "0.19.2",
"@esbuild/linux-arm": "0.19.2",
"@esbuild/linux-arm64": "0.19.2",
"@esbuild/linux-ia32": "0.19.2",
"@esbuild/linux-loong64": "0.19.2",
"@esbuild/linux-mips64el": "0.19.2",
"@esbuild/linux-ppc64": "0.19.2",
"@esbuild/linux-riscv64": "0.19.2",
"@esbuild/linux-s390x": "0.19.2",
"@esbuild/linux-x64": "0.19.2",
"@esbuild/netbsd-x64": "0.19.2",
"@esbuild/openbsd-x64": "0.19.2",
"@esbuild/sunos-x64": "0.19.2",
"@esbuild/win32-arm64": "0.19.2",
"@esbuild/win32-ia32": "0.19.2",
"@esbuild/win32-x64": "0.19.2"
}
},
"escape-string-regexp": {
@ -5174,9 +5151,9 @@
}
},
"glightbox": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.3.0.tgz",
"integrity": "sha512-SJukatHBZZ/POMOpLUQ6/dhXf/wJTDx1wZ/FwApjseXw2WrRj3Ze9DzNCFYzca0oU7RjXQhi9o02aIZ9SuCz1A=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.2.0.tgz",
"integrity": "sha512-iit1xYixqL4YVL+I2YJLfMeyJwvLi6FE6kY3qNKeZHEJgRIz80QU8Rm7YCyw1wOTgXvmNDnXGVhHOHRCwnDltQ=="
},
"glob": {
"version": "7.2.3",
@ -6066,9 +6043,9 @@
"dev": true
},
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true
},
"punycode": {
@ -6285,9 +6262,9 @@
}
},
"sass": {
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"version": "1.67.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz",
"integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",

View File

@ -22,26 +22,26 @@
"devDependencies": {
"eslint": "8.55.0",
"eslint-plugin-unicorn": "49.0.0",
"prettier": "2.7.1",
"prettier": "2.8.8",
"stylelint": "14.16.1",
"stylelint-config-prettier-scss": "0.0.1",
"stylelint-config-standard-scss": "6.1.0"
},
"dependencies": {
"@fontsource/fira-code": "5.0.2",
"@fortawesome/fontawesome-free": "6.5.2",
"@fortawesome/fontawesome-free": "6.4.0",
"@ledge/is-ie-11": "7.0.0",
"bulma": "0.9.4",
"elevator.js": "1.0.1",
"esbuild": "0.20.2",
"glightbox": "3.3.0",
"esbuild": "0.19.2",
"glightbox": "3.2.0",
"htmx.org": "1.9.2",
"lite-youtube-embed": "0.3.0",
"lodash.clamp": "4.0.3",
"lodash.debounce": "4.0.8",
"lodash.throttle": "4.1.1",
"npm-run-all": "4.1.5",
"sass": "1.75.0",
"sass": "1.67.0",
"shareon": "2.4.0"
}
}

View File

@ -10,13 +10,5 @@
"schedule": ["every weekend"],
"enabled": false
}
],
"regexManagers": [
{
"fileMatch": ["^Dockerfile$"],
"matchStrings": ["ENV S6_OVERLAY_VERSION=(?<currentValue>.*?)\\n"],
"depNameTemplate": "just-containers/s6-overlay",
"datasourceTemplate": "github-releases"
}
]
}

View File

@ -1,42 +1,40 @@
Django==5.0.4
wagtail==5.2.5
Django==3.2.22
wagtail==4.1.8
django-environ==0.11.2
whitenoise[brotli]==6.6.0
Pygments==2.17.2
beautifulsoup4
lxml==5.2.1
requests
wagtail-generic-chooser==0.6
django-rq==2.10.1
django-redis==5.4.0
gunicorn==22.0.0
psycopg==3.1.18
whitenoise[brotli]==6.5.0
Pygments==2.16.1
beautifulsoup4==4.11.2
lxml==4.9.1
requests==2.31.0
wagtail-generic-chooser==0.5.1
django-rq==2.8.0
django-redis==5.3.0
gunicorn==21.2.0
psycopg2==2.9.6
djangorestframework
django-htmx==1.17.2
wagtail-metadata==5.0.0
django-htmx==1.16.0
wagtail-metadata==4.0.3
django-plausible==0.5.0
sentry-sdk
sentry-sdk==1.29.2
django-sri==0.7.0
wagtail-2fa==1.6.9
wagtail-2fa==1.6.5
django-health-check==3.17.0
wagtail-autocomplete==0.11.0
Wand==0.6.13
wagtail-autocomplete==0.10.0
Wand==0.6.11
django3-cache-decorator==0.5.2
django-cors-headers==4.3.1
django-cors-headers==4.2.0
django-csp==3.7
django-permissions-policy==4.18.0
django-permissions-policy==4.17.0
django-enforce-host==1.1.0
django-proxy==1.2.2
wagtail-lite-youtube-embed==0.1.0
django-minify-html==1.7.1
# DRF OpenAPI dependencies
uritemplate
PyYAML
inflection
# Use custom `wagtail-favicon` with performance improvements
git+https://github.com/RealOrangeOne/wagtail-favicon@b892165e047b35c46d7244109b9ad9226d32a213
git+https://github.com/RealOrangeOne/wagtail-favicon@4586efaac746085338fc7d61713006d9adc62d2e
# Use custom `wagtail-draftail-snippet` with support for Wagtail 5.x
git+https://github.com/RealOrangeOne/wagtail-draftail-snippet@74ed858bd958a066d5aee295c9848257107b1546
# Use custom `wagtail-draftail-snippet` with support for Wagtail 4.1
git+https://github.com/RealOrangeOne/wagtail-draftail-snippet@0924ab12b1ca205b94ccd9a34ecc446d7ac422e5

View File

@ -23,13 +23,11 @@ function handleScrollIndicator() {
}
window.addEventListener("load", () => {
if (CONTENT && SCROLL_INDICATOR) {
window.addEventListener("resize", handleScrollIndicator);
window.addEventListener("scroll", handleScrollIndicator);
// Initialize the indicator
handleScrollIndicator();
}
window.addEventListener("resize", handleScrollIndicator);
window.addEventListener("scroll", handleScrollIndicator);
GLightbox({});
// Initialize the indicator
handleScrollIndicator();
});

View File

@ -8,7 +8,7 @@ section.content {
h5,
h6 {
.heading-anchor {
scroll-margin-top: var(--hero-height); // hero height (ish)
scroll-margin-top: var(--hero-height); // navbar height (ish)
margin-right: 0.5rem;
font-weight: 100;
}
@ -56,16 +56,9 @@ section.content {
.gslide-image img {
object-fit: contain !important;
// Manually set sizes, as mermaid images are very small
width: 80vw !important;
&[src*="mermaid.ink"] {
@include dark-mode {
filter: invert(100%);
}
}
}
#comments {
scroll-margin-top: var(--hero-height); // hero height (ish)
}

View File

@ -1,27 +1,17 @@
body.page-homepage {
min-height: 100vh;
height: 100vh;
main {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: $white;
margin-bottom: 0;
padding-bottom: 2rem;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.top-section {
min-width: 90%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 2rem;
margin-bottom: 1rem;
}
h1 {
@ -33,7 +23,7 @@ body.page-homepage {
min-width: 45%;
}
.box {
.latest {
padding: 0.25rem 0.5rem;
margin-top: 2rem;
background-color: color.adjust($dark, $alpha: -0.2);
@ -69,60 +59,4 @@ body.page-homepage {
#to-top {
display: none;
}
.content-list {
width: 90%;
.card-image {
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
transition: filter 0.25s;
filter: brightness(0.85);
}
p {
position: absolute;
bottom: 0;
color: $white;
padding: 0.5rem;
transition: background-color 0.25s;
width: 100%;
background-color: rgb(0 0 0 / 55%);
}
figure {
margin: 0;
text-align: initial;
}
a:hover {
img {
filter: unset;
}
p {
background-color: rgb(0 0 0 / 75%);
}
}
}
.recent-posts {
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
flex-grow: unset;
margin-top: 2rem;
.box {
margin: 0 auto;
display: inline-block;
}
}
}

View File

@ -48,7 +48,7 @@
}
}
.container.listing {
.page-blogpostlistpage {
.date-header {
font-size: $size-2;
font-weight: $weight-bold;

View File

@ -14,6 +14,10 @@ body.page-searchpage {
max-width: 80%;
}
.htmx-request i {
animation: search-loading 1.5s linear infinite;
}
#search-results > p {
font-size: 1.5rem;
text-align: center;
@ -43,19 +47,9 @@ body.page-searchpage {
#search-page-indicator {
text-align: center;
margin-top: 1.5rem;
font-size: $size-3;
&:not(.htmx-request) {
display: none;
}
}
// The search icon is hidden during requests
#search-icon {
opacity: 1 !important;
&.htmx-request {
opacity: 0 !important;
}
}
}

View File

@ -1,5 +1,4 @@
.shareon-container {
scroll-margin-top: var(--hero-height); // hero height (ish)
margin-top: 1rem;
.shareon {

View File

@ -1,4 +1,4 @@
$support-pill-size: 60px;
$support-pill-size: 50px;
$support-pill-position: 1.5rem;
.tag.support-pill {
@ -13,11 +13,6 @@ $support-pill-position: 1.5rem;
height: $support-pill-size;
font-size: 100%;
z-index: $dropdown-content-z;
padding: none;
&:hover {
background-color: $primary-dark;
}
@include mobile {
display: none;

View File

@ -2,6 +2,5 @@ from rest_framework.pagination import PageNumberPagination
class CustomPageNumberPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 25
page_size = 15
max_page_size = 40

View File

@ -36,14 +36,3 @@ class LMOTFYSerializer(serializers.ModelSerializer):
return self.context["request"].build_absolute_uri(image_url)
return image_url
class LatestPostSerializer(serializers.ModelSerializer):
full_url = serializers.SerializerMethodField()
class Meta:
model = BlogPostPage
fields = read_only_fields = ["full_url", "title", "date"]
def get_full_url(self, page: Page) -> str:
return page.get_full_url(request=self.context["request"])

View File

@ -86,20 +86,3 @@ class SchemaTestCase(APISimpleTestCase):
def test_schema(self) -> None:
response = self.client.get(reverse("api:schema"))
self.assertEqual(response.status_code, 200)
class LatestPostsAPIViewTestCase(APITestCase):
url = reverse("api:latest-posts")
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
for i in range(4):
BlogPostPageFactory(parent=cls.home_page, title=f"Post {i}")
def test_accessible(self) -> None:
with self.assertNumQueries(5):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 4)

View File

@ -10,7 +10,6 @@ api_urlpatterns = [
path("ping/", views.PingAPIView.as_view(), name="ping"),
path("page-links/", views.PageLinksAPIView.as_view(), name="page-links"),
path("lmotfy/", views.LMOTFYAPIView.as_view(), name="lmotfy"),
path("latest-posts/", views.LatestPostsAPIView.as_view(), name="latest-posts"),
]
schema_view = get_schema_view(

View File

@ -60,20 +60,3 @@ class SwaggerRedirectView(RedirectView):
return HttpResponseRedirect(
self.SWAGGER_EDITOR_URL + request.build_absolute_uri(reverse("api:schema"))
)
class LatestPostsAPIView(ListAPIView):
"""
List my latest blog posts
"""
serializer_class = serializers.LatestPostSerializer
pagination_class = CustomPageNumberPagination
def get_queryset(self) -> PageQuerySet:
return (
BlogPostPage.objects.live()
.public()
.only("id", "url_path", "title", "date")
.order_by("-date")
)

View File

@ -7,7 +7,6 @@ from django.utils import timezone
from django.utils.functional import cached_property
from modelcluster.fields import ParentalManyToManyField
from wagtail.admin.panels import FieldPanel
from wagtail.models import PageQuerySet
from wagtail.search import index
from wagtailautocomplete.edit_handlers import AutocompletePanel
@ -62,19 +61,6 @@ class BlogPostPage(BaseContentPage):
def tag_list_page_url(self) -> Optional[str]:
return SingletonPageCache.get_url(BlogPostTagListPage)
@cached_property
def tags_list(self) -> models.QuerySet:
"""
Use this to get a page's tags.
"""
tags = self.tags.order_by("slug")
# In drafts, `django-modelcluster` doesn't support these filters
if isinstance(tags, PageQuerySet):
return tags.public().live()
return tags
@cached_property
def blog_post_list_page_url(self) -> Optional[str]:
return SingletonPageCache.get_url(BlogPostListPage)
@ -93,7 +79,7 @@ class BlogPostPage(BaseContentPage):
else models.Value(1),
)
page_tags = list(self.tags.public().live().values_list("id", flat=True))
page_tags = list(self.tags.values_list("id", flat=True))
similar_posts = similar_posts.alias(
# If this page has no tags, ignore it as part of similarity
# NB: Cast to a float, because `COUNT` returns a `bigint`.

View File

@ -1,19 +1,21 @@
{% extends "common/listing_page.html" %}
{% load wagtailroutablepage_tags %}
{% block hero_buttons %}
<a class="button is-radiusless" href="{{ page.tag_list_page_url }}" title="View tags"><i class="fas fa-tags" aria-hidden="true"></i></a>
{{ block.super }}
{% endblock %}
{% block post_content %}
<section class="container listing">
<section class="container">
{% for page in listing_pages %}
{% ifchanged %}
<h2 id="date-{{ page.date|date:'Y-m' }}" class="date-header">
<h3 id="date-{{ page.date|date:'Y-m' }}" class="date-header">
<time datetime="{{ page.date|date:'Y-m' }}" title="{{ page.date|date:'F Y' }}">
{{ page.date|date:"Y-m" }}
</time>
</h2>
</h3>
{% endifchanged %}
{% include "common/listing-item.html" %}

View File

@ -1,32 +1,26 @@
{% extends "common/content_page.html" %}
{% load wagtail_cache navbar_tags %}
{% load cache util_tags %}
{% block post_content %}
{{ block.super }}
{% if not request.is_preview %}
{% include "common/shareon.html" %}
{% cache FRAGMENT_CACHE_TTL|jitter:FRAGMENT_CACHE_TTL_JITTER "similar-content" page.id request.is_preview %}
<section class="container similar-content" id="similar-content">
<h2 class="subtitle is-size-2">Similar content</h2>
{% wagtailpagecache FRAGMENT_CACHE_TTL "similar-content" %}
<section class="container similar-content" id="similar-content">
<h2 class="subtitle is-size-2">Similar content</h2>
<p class="view-all">
<a href="{{ page.blog_post_list_page_url }}">View all &rarr;</a>
</p>
<p class="view-all">
<a href="{{ page.blog_post_list_page_url }}">View all &rarr;</a>
</p>
{% for page in page.get_similar_posts %}
{% block listing_item %}
{% include "common/listing-item.html" %}
{% endblock %}
{% endfor %}
{% for page in page.get_similar_posts %}
{% block listing_item %}
{% include "common/listing-item.html" with show_listing_images=True %}
{% endblock %}
{% endfor %}
</section>
{% endwagtailpagecache %}
{% include "common/comments.html" %}
{% if not request.user.is_authenticated %}
{% support_pill %}
</section>
{% endcache %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -7,9 +7,9 @@
<div class="card-content">
<div class="media">
<div class="media-content">
<h2 class="title is-4">
<p class="title is-4">
<a href="{% pageurl page %}">{{ page.title }}</a>
</h2>
</p>
<p class="subtitle is-6">{{ page.summary }}</p>
</div>
<div class="media-right">

View File

@ -18,7 +18,7 @@ class BlogPostPageTestCase(TestCase):
self.assertEqual(response.status_code, 200)
def test_queries(self) -> None:
with self.assertNumQueries(43):
with self.assertNumQueries(48):
self.client.get(self.page.url)
@ -76,7 +76,7 @@ class BlogPostListPageTestCase(TestCase):
self.assertEqual(len(response.context["listing_pages"]), 2)
def test_queries(self) -> None:
with self.assertNumQueries(39):
with self.assertNumQueries(44):
self.client.get(self.page.url)
def test_feed_accessible(self) -> None:

View File

@ -1,9 +0,0 @@
from django_minify_html.middleware import MinifyHtmlMiddleware
class CustomMinifyHtmlMiddleware(MinifyHtmlMiddleware):
minify_args = {
"do_not_minify_doctype": True,
"ensure_spec_compliant_unquoted_attribute_values": True,
"keep_spaces_between_attributes": True,
}

View File

@ -11,7 +11,7 @@ from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect
from django.template.defaultfilters import pluralize
from django.utils.functional import cached_property, classproperty
from django.utils.text import Truncator, slugify
from django.utils.text import slugify
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.contrib.settings.models import BaseGenericSetting, register_setting
@ -31,10 +31,12 @@ from .serializers import PaginationSerializer
from .streamfield import add_heading_anchors, get_blocks, get_content_html
from .utils import (
TocEntry,
count_words,
extract_text,
get_site_title,
get_table_of_contents,
get_url_mime_type,
truncate_string,
)
@ -139,11 +141,16 @@ class BaseContentPage(BasePage, MetadataMixin):
@cached_property
def word_count(self) -> int:
return len(self.plain_text.split())
return count_words(self.plain_text)
@cached_property
def summary(self) -> str:
return Truncator(self.plain_text).words(50)
summary = truncate_string(self.plain_text, 50)
if summary and summary != self.plain_text and not summary.endswith("."):
summary += ""
return summary
@cached_property
def body_html(self) -> str:
@ -188,13 +195,6 @@ class BaseContentPage(BasePage, MetadataMixin):
def list_image_url(self) -> Optional[str]:
return self.hero_url("small")
@cached_property
def hero_image_alt(self) -> str:
if self.hero_unsplash_photo_id is None:
return ""
return self.hero_unsplash_photo.data.get("description", "")
def get_meta_url(self) -> str:
return self.full_url
@ -256,12 +256,7 @@ class BaseListingPage(RoutablePageMixin, BaseContentPage):
def get_context(self, request: HttpRequest) -> dict:
context = super().get_context(request)
listing_pages = self.get_paginator_page()
context["listing_pages"] = listing_pages
# Show listing images if at least 1 page has an image
context["show_listing_images"] = any(p.list_image_url for p in listing_pages)
context["listing_pages"] = self.get_paginator_page()
return context
@cached_property
@ -295,13 +290,6 @@ class BaseListingPage(RoutablePageMixin, BaseContentPage):
def feed(self, request: HttpRequest) -> HttpResponse:
return redirect("feed", permanent=True)
@route(r"^random/$")
def random(self, request: HttpRequest) -> HttpResponse:
page = self.get_listing_pages().order_by("?").first()
if page is None:
return redirect(self.get_url(request=request), permanent=False)
return redirect(page.get_url(request=request), permanent=False)
class ListingPage(BaseListingPage):
pass

View File

@ -1,4 +1,4 @@
{% load static wagtailcore_tags wagtailuserbar navbar_tags footer_tags plausible_wagtail favicon_tags sri wagtail_cache %}
{% load static wagtailcore_tags wagtailuserbar navbar_tags footer_tags plausible_wagtail favicon_tags sri cache %}
<!DOCTYPE html>
<html lang="en-GB">
@ -14,8 +14,6 @@
{% block extra_head %}{% endblock %}
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="Orange search" />
<link rel="alternate" type="application/rss+xml" href="{% url 'feed' %}" />
<link rel="me" href="https://{{ ACTIVITYPUB_HOST }}/@jake" />
@ -32,31 +30,31 @@
<body class="{% block body_class %}{% endblock %}">
{% wagtailuserbar %}
{% wagtailcache 1800 "navbar" %}
{% navbar %}
{% endwagtailcache %}
{% cache 1800 "navbar" request.is_preview %}
{% navbar %}
{% endcache %}
{% block main %}
<main>
{% block main_content %}{% endblock %}
</main>
{% endblock %}
{% block main %}
<main>
{% block main_content %}{% endblock %}
</main>
{% endblock %}
{% wagtailcache 1800 "footer" %}
{% footer %}
{% endwagtailcache %}
{% cache 1800 "footer" request.is_preview %}
{% footer %}
{% endcache %}
{# Not async to avoid bright flashes #}
{% sri_static "js/dark-mode.js" %}
{# Not async to avoid bright flashes #}
{% sri_static "js/dark-mode.js" %}
<script async defer type="text/javascript" src="{% static 'js/base.js' %}" integrity="{% sri_integrity_static 'js/base.js' %}"></script>
<script async defer type="text/javascript" src="{% static 'js/base.js' %}" integrity="{% sri_integrity_static 'js/base.js' %}"></script>
{% block extra_js %}{% endblock %}
{% block extra_js %}{% endblock %}
{% block plausible %}
{% if not request.user.is_authenticated or not request.is_preview %}
{% plausible %}
{% endif %}
{% endblock %}
</body>
{% block plausible %}
{% if not request.user.is_authenticated or not request.is_preview %}
{% plausible %}
{% endif %}
{% endblock %}
</body>
</html>

View File

@ -2,7 +2,7 @@
<figure>
<div class="image">
<a href="{% image_url value.image 'original' %}" class="glightbox" data-gallery="content" data-height="70vh" data-alt="{{ value.caption|richtext|extract_text }}" data-title="{{ value.caption|richtext|extract_text }}">
<a href="{% image_url value.image 'original' %}" class="glightbox" data-gallery="content" data-height="70vh" data-width="95vw" data-alt="{{ value.caption|richtext|extract_text }}">
<img src="{% image_url value.image 'width-1500' %}" alt="{{ value.caption|richtext|extract_text }}" loading="lazy" decoding="async" />
</a>
</div>

View File

@ -1,4 +1,4 @@
<section class="container" id="comments">
<section class="container">
<div id="commento"></div>
</section>

View File

@ -1,69 +1,36 @@
{% load wagtailcore_tags wagtail_cache %}
{% load wagtailcore_tags cache util_tags %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "content-details" %}
<div class="content-details field is-grouped">
{% if page.date %}
<span class="icon-text">
<span class="icon">
<i class="far fa-lg fa-calendar-alt"></i>
</span>
<span>{{ page.date|date:"Y-m-d" }}</span>
</span>
{% endif %}
{% if page.show_reading_time %}
<div class="icon-text" {% if page.word_count %}title="{{ page.word_count }} words"{% endif %}>
<span class="icon">
<i class="far fa-lg fa-clock"></i>
</span>
<span>{{ page.reading_time_display }}</span>
</div>
{% endif %}
{% if page.tags_list %}
<div class="icon-text is-family-code">
<span class="icon">
<a href="{{ page.tag_list_page_url }}" title="View all tags">
<i class="fas fa-lg fa-tags"></i>
</a>
</span>
{% for tag in page.tags_list %}
<span><a title="{{ tag.name }}" href="{% pageurl tag %}">#{{ tag.slug }}</a></span>
{% endfor %}
</div>
{% endif %}
{% if page.slides_url %}
<span class="icon-text">
<a href="{{ page.slides_url }}">
{% cache FRAGMENT_CACHE_TTL|jitter:FRAGMENT_CACHE_TTL_JITTER "content-details" page.id request.is_preview %}
<div class="content-details field is-grouped">
{% if page.date %}
<span class="icon-text">
<span class="icon">
<i class="fas fa-lg fa-images"></i>
<i class="far fa-lg fa-calendar-alt"></i>
</span>
<span>Slides</span>
</a>
</span>
{% endif %}
<span>{{ page.date|date:"Y-m-d" }}</span>
</span>
{% endif %}
{% if page.video_url %}
<span class="icon-text">
<a href="{{ page.video_url }}">
{% if page.show_reading_time %}
<div class="icon-text" {% if page.word_count %}title="{{ page.word_count }} words"{% endif %}>
<span class="icon">
<i class="fas fa-lg fa-film"></i>
<i class="far fa-lg fa-clock"></i>
</span>
<span>Video</span>
</a>
</span>
{% endif %}
<span>{{ page.reading_time_display }}</span>
</div>
{% endif %}
{% if page.location_name and page.location_url %}
<span class="icon-text">
<a href="{{ page.location_url }}">
{% if page.tags.all %}
<div class="icon-text is-family-code">
<span class="icon">
<i class="fas fa-lg fa-map-marker-alt"></i>
<a href="{{ page.tag_list_page_url }}" title="View all tags">
<i class="fas fa-lg fa-tags"></i>
</a>
</span>
<span>{{ page.location_name }}</span>
</a>
</span>
{% endif %}
</div>
{% endwagtailpagecache %}
{% for tag in page.tags.all|dictsort:"slug" %}
<span><a title="{{ tag.name }}" href="{% pageurl tag %}">#{{ tag.slug }}</a></span>
{% endfor %}
</div>
{% endif %}
</div>
{% endcache %}

View File

@ -1,27 +1,27 @@
{% load wagtailcore_tags wagtail_cache util_tags %}
{% load wagtailcore_tags cache util_tags %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "listing-item" breadcrumbs show_listing_images %}
<article class="media listing-item">
<div class="columns">
<figure class="media-left column is-{{ show_listing_images|yesno:'3,1' }} image-column">
{% if page.list_image_url %}
<a href="{% pageurl page %}" class="image" title="{{ page.title }}">
<img src="{{ page.list_image_url }}" alt="{{ page.hero_image_alt }}" loading="lazy" decoding="async" referrerpolicy="no-referrer" />
</a>
{% endif %}
</figure>
<div class="media-content column">
<div>
{% if breadcrumbs %}
{% include "common/breadcrumbs.html" with parents=page.get_parent_pages %}
{% cache FRAGMENT_CACHE_TTL|jitter:FRAGMENT_CACHE_TTL_JITTER "listing-item" page.id request.is_preview breadcrumbs %}
<article class="media listing-item">
<div class="columns">
<figure class="media-left column is-3 image-column">
{% if page.list_image_url %}
<a href="{% pageurl page %}" class="image" title="{{ page.title }}">
<img src="{{ page.list_image_url }}" alt="" loading="lazy" decoding="async" />
</a>
{% endif %}
<h2 class="title is-3">
<a href="{% pageurl page %}">{{ page.title }}</a>
</h2>
{% include "common/content-details.html" %}
<p>{{ page.summary }}</p>
</figure>
<div class="media-content column">
<div>
{% if breadcrumbs %}
{% include "common/breadcrumbs.html" with parents=page.get_parent_pages %}
{% endif %}
<h2 class="title is-3">
<a href="{% pageurl page %}">{{ page.title }}</a>
</h2>
{% include "common/content-details.html" %}
<p>{{ page.summary }}</p>
</div>
</div>
</div>
</div>
</article>
{% endwagtailpagecache %}
</article>
{% endcache %}

View File

@ -1,10 +1,8 @@
{% extends "common/content_page.html" %}
{% load wagtailadmin_tags wagtailroutablepage_tags %}
{% load wagtailadmin_tags %}
{% block hero_buttons %}
<a class="button is-radiusless" href="{% routablepageurl page 'random' %}" title="View random"><i class="fas fa-dice" aria-hidden="true"></i></a>
{% if listing_pages.has_previous %}
<a class="button is-radiusless" href="{% querystring page=listing_pages.previous_page_number %}" title="Previous page"><i class="fas fa-arrow-left" aria-hidden="true"></i></a>
{% endif %}

View File

@ -1,14 +1,14 @@
{% load wagtailadmin_tags %}
<nav class="pagination is-centered" role="navigation" title="pagination">
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if page.has_previous %}
<a class="pagination-previous" title="Go to page {{ page.previous_page_number }}" href="{% querystring page=page.previous_page_number %}"><i class="fas fa-arrow-left" aria-hidden="true"></i></a>
<a class="pagination-previous" href="{% querystring page=page.previous_page_number %}"><i class="fas fa-arrow-left" aria-hidden="true"></i></a>
{% else %}
<span class="pagination-previous is-disabled"><i class="fas fa-arrow-left" aria-hidden="true"></i></span>
{% endif %}
{% if page.has_next %}
<a class="pagination-next" title="Go to page {{ page.next_page_number }}" href="{% querystring page=page.next_page_number %}"><i class="fas fa-arrow-right" aria-hidden="true"></i></a>
<a class="pagination-next" href="{% querystring page=page.next_page_number %}"><i class="fas fa-arrow-right" aria-hidden="true"></i></a>
{% else %}
<span class="pagination-next is-disabled"><i class="fas fa-arrow-right" aria-hidden="true"></i></span>
{% endif %}
@ -16,7 +16,7 @@
<ul class="pagination-list">
{% if page.has_previous and page.previous_page_number != 1 %}
<li>
<a class="pagination-link" aria-label="Go to page 1" href="{% querystring page=1 %}">1</a>
<a class="pagination-link" aria-label="Goto page 1" href="{% querystring page=1 %}">1</a>
</li>
<li>
<span class="pagination-ellipsis">&hellip;</span>
@ -25,17 +25,17 @@
{% if page.has_previous %}
<li>
<a class="pagination-link" title="Go to page {{ page.previous_page_number }}" href="{% querystring page=page.previous_page_number %}">{{ page.previous_page_number }}</a>
<a class="pagination-link" aria-label="Goto page {{ page.previous_page_number }}" href="{% querystring page=page.previous_page_number %}">{{ page.previous_page_number }}</a>
</li>
{% endif %}
<li>
<a class="pagination-link is-current" title="Page {{ page.number }}" aria-current="page" href="{% querystring page=page.number %}">{{ page.number }}</a>
<a class="pagination-link is-current" aria-label="Page {{ page.number }}" aria-current="page" href="{% querystring page=page.number %}">{{ page.number }}</a>
</li>
{% if page.has_next %}
<li>
<a class="pagination-link" title="Go to page {{ page.next_page_number }}" href="{% querystring page=page.next_page_number %}">{{ page.next_page_number }}</a>
<a class="pagination-link" aria-label="Goto page {{ page.next_page_number }}" href="{% querystring page=page.next_page_number %}">{{ page.next_page_number }}</a>
</li>
{% endif %}
@ -44,7 +44,7 @@
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li>
<a class="pagination-link" title="Go to page {{ page.paginator.num_pages }}" href="{% querystring page=page.paginator.num_pages %}">{{ page.paginator.num_pages }}</a>
<a class="pagination-link" aria-label="Goto page {{ page.paginator.num_pages }}" href="{% querystring page=page.paginator.num_pages %}">{{ page.paginator.num_pages }}</a>
</li>
{% endif %}
</ul>

View File

@ -1,8 +1,8 @@
{% load wagtailcore_tags %}
{% load util_tags %}
<section class="container has-text-centered shareon-container" id="shareon">
<section class="container has-text-centered shareon-container">
<p>Share this page</p>
<div class="shareon" data-title="{{ page.title }}" data-url="{% fullpageurl page %}">
<div class="shareon" data-title="{{ page.title }}" data-url="{% pagefullurl page %}">
<a class="facebook" title="Share on Facebook"></a>
<a class="linkedin" title="Share on LinkedIn"></a>
<a class="mastodon" title="Share on Mastodon"></a>

View File

@ -1,7 +1,7 @@
{% load wagtailcore_tags %}
{% if support_page and page.id != support_page.id %}
<a href="{% pageurl support_page %}" class="tag is-primary support-pill" title="If you like what I do, please consider supporting my work!">
<a href="{% pageurl support_page %}" class="tag is-primary support-pill" title="Support me">
<i class="fas fa-praying-hands"></i>
</a>
{% endif %}

View File

@ -1,9 +0,0 @@
{% load wagtailcore_tags %}
{% spaceless %}
{{ obj.content_html | truncatewords_html:100 | safe }}
<p>
<a href="{% fullpageurl obj %}">Continue Reading&hellip;</a>
</p>
{% endspaceless %}

View File

@ -15,7 +15,7 @@
<picture>
{% for width, image_url in page.hero_image_urls.items reversed %}<source srcset="{{ image_url }}" media="(max-width: {{ width }}px)" />{% endfor %}
<img class="hero" src="{{ page.hero_image_url }}" referrerpolicy="no-referrer" decoding="async" alt="{{ page.hero_image_alt }}" />
<img class="hero" src="{{ page.hero_image_url }}" decoding="async" alt="" />
</picture>
{% endif %}

View File

@ -1,5 +1,8 @@
import random
from django.template import Library
from django.utils.encoding import force_str
from wagtail.models import Page
from wagtail.rich_text import RichText
from website.common import utils
@ -12,6 +15,16 @@ def do_range(stop: int) -> range:
return range(stop)
@register.simple_tag(takes_context=True)
def pagefullurl(context: dict, page: Page) -> str:
return page.get_full_url(context["request"])
@register.filter()
def jitter(original: float, jitter: float) -> float:
return random.uniform(original + jitter, original - jitter)
@register.filter()
def extract_text(html: str | RichText) -> str:
return utils.extract_text(force_str(html))

View File

@ -36,7 +36,7 @@ class ContentPageTestCase(TestCase):
self.assertEqual(response.status_code, 200)
def test_queries(self) -> None:
with self.assertNumQueries(32):
with self.assertNumQueries(39):
self.client.get(self.page.url)
@ -53,7 +53,7 @@ class ListingPageTestCase(TestCase):
ContentPageFactory(parent=cls.page)
def test_accessible(self) -> None:
with self.assertNumQueries(35):
with self.assertNumQueries(42):
response = self.client.get(self.page.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["listing_pages"]), 2)
@ -77,12 +77,3 @@ class ListingPageTestCase(TestCase):
self.assertEqual(
response.context["page"].get_meta_url(), self.page.full_url + "?page=2"
)
def test_random(self) -> None:
url = self.page.url + self.page.reverse_subpage("random")
with self.assertNumQueries(10):
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertIn(
response.url, [page.get_url() for page in self.page.get_listing_pages()]
)

View File

@ -3,6 +3,7 @@ from django.test import SimpleTestCase
from wagtail.rich_text import features as richtext_feature_registry
from website.common.utils import (
count_words,
extract_text,
get_table_of_contents,
heading_id,
@ -96,6 +97,13 @@ class ExtractTextTestCase(SimpleTestCase):
self.assertEqual(extract_text("Hello there!"), "Hello there!")
class CountWordsTestCase(SimpleTestCase):
def test_counts_words(self) -> None:
self.assertEqual(count_words("a b c"), 3)
self.assertEqual(count_words("Correct Horse Battery Staple"), 4)
self.assertEqual(count_words("Hello there! How are you?"), 5)
class RichTextFeaturesTestCase(SimpleTestCase):
def test_features_exist(self) -> None:
for editor, editor_config in settings.WAGTAILADMIN_RICH_TEXT_EDITORS.items():

View File

@ -22,7 +22,7 @@ class Error404PageTestCase(TestCase):
)
def test_queries(self) -> None:
with self.assertNumQueries(16):
with self.assertNumQueries(22):
self.client.get(self.url)

View File

@ -1,13 +1,12 @@
from dataclasses import dataclass
from itertools import pairwise
from typing import Optional, Type
from itertools import islice, pairwise
from typing import Iterable, Optional, Type
import requests
from bs4 import BeautifulSoup, SoupStrainer
from django.conf import settings
from django.db import models
from django.http.request import HttpRequest
from django.utils.text import slugify
from django.utils.text import re_words, slugify
from django_cache_decorator import django_cache_decorator
from wagtail.models import Page, Site
from wagtail.models import get_page_models as get_wagtail_page_models
@ -69,6 +68,19 @@ def show_toolbar_callback(request: HttpRequest) -> bool:
return settings.DEBUG
def split_words(text: str) -> Iterable[str]:
for word in re_words.split(text):
if word and word.strip():
yield word.strip()
def count_words(text: str) -> int:
"""
Count the number of words in the text, without duplicating the item in memory
"""
return len(list(split_words(text)))
def extract_text(html: str) -> str:
"""
Get the plain text of some HTML.
@ -78,6 +90,10 @@ def extract_text(html: str) -> str:
)
def truncate_string(text: str, words: int) -> str:
return " ".join(islice(split_words(text), words))
def heading_id(heading: str) -> str:
"""
Convert a heading into an identifier which is valid for a HTML id attribute
@ -102,13 +118,3 @@ def get_url_mime_type(url: str) -> Optional[str]:
return requests_session.head(url).headers.get("Content-Type")
except requests.exceptions.RequestException:
return None
def get_or_none(queryset: models.QuerySet) -> models.Model:
"""
Helper method to get a single instance, or None if there is not exactly 1 matches
"""
try:
return queryset.get()
except (queryset.model.DoesNotExist, queryset.model.MultipleObjectsReturned):
return None

View File

@ -15,7 +15,6 @@ from wagtail.query import PageQuerySet
from wagtail_favicon.models import FaviconSettings
from wagtail_favicon.utils import get_rendition_url
from website.blog.models import BlogPostPage
from website.common.utils import get_site_title
from website.contrib.singleton_page.utils import SingletonPageCache
from website.home.models import HomePage
@ -64,7 +63,6 @@ class KeybaseView(TemplateView):
class AllPagesFeed(Feed):
feed_type = CustomFeed
link = "/"
description_template = "feed-description.html"
def __init__(self) -> None:
self.style_tag = f'<?xml-stylesheet href="{static("contrib/pretty-feed-v3.xsl")}" type="text/xsl"?>'.encode()
@ -124,9 +122,12 @@ class AllPagesFeed(Feed):
def item_updateddate(self, item: BasePage) -> datetime:
return item.last_published_at
def item_description(self, item: BasePage) -> str:
return getattr(item, "summary", None) or item.title
def item_categories(self, item: BasePage) -> Optional[list[str]]:
if isinstance(item, BlogPostPage):
return item.tags_list.values_list("slug", flat=True)
if tags := getattr(item, "tags", None):
return tags.order_by("slug").values_list("slug", flat=True)
return None
def item_enclosure_url(self, item: BasePage) -> Optional[str]:

View File

@ -7,12 +7,12 @@
<div class="card-content">
<div class="media">
<div class="media-content">
<h3 class="title is-4">
<p class="title is-4">
<a href="{{ account.url }}">
{% if account.icon %}<i class="{{ account.icon }}"></i>{% endif %}
{{ account.name }}
</a>
</h3>
</p>
<p class="subtitle is-6">{{ account.username }}</p>
</div>
</div>

View File

@ -9,7 +9,6 @@ class GitHubLinguistHealthCheckBackend(BaseHealthCheckBackend):
colours = _get_linguist_colours()
except Exception as e:
self.add_error(str(e))
return
if colours is None:
self.add_error("No colours provided")

View File

@ -2,7 +2,7 @@
<figure>
<div class="image">
<a href="https://mermaid.ink/svg/{{ value.pako }}" data-gallery="content" class="glightbox" data-type="image" data-height="70vh" data-alt="{{ value.caption|richtext|extract_text }}" data-title="{{ value.caption|richtext|extract_text }}">
<a href="https://mermaid.ink/svg/{{ value.pako }}" data-gallery="content" class="glightbox" data-type="image" data-height="60vh" data-width="95vw" data-alt="{{ value.caption|richtext|extract_text }}">
<img src="https://mermaid.ink/svg/{{ value.pako }}" referrerpolicy="no-referrer" alt="{{ value.caption|richtext|extract_text }}" loading="lazy" decoding="async" />
</a>
</div>

View File

@ -1,6 +1,5 @@
from django.core.cache import cache
from wagtail import hooks
from wagtail.models import Page
from website.common.utils import get_page_models
@ -8,7 +7,7 @@ from .utils import SingletonPageCache
@hooks.register("after_move_page")
def clear_singleton_url_cache(page_to_move: Page) -> None:
def clear_singleton_url_cache(**kwargs: dict) -> None:
"""
Clear all page caches, in case a parent has moved
"""

View File

@ -3,11 +3,11 @@ from typing import Type
from django.core.exceptions import ValidationError
from django.http.response import Http404
from django.utils.html import format_html
from wagtail import hooks
from wagtail.admin.forms.models import WagtailAdminModelForm
from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.contrib.modeladmin.views import CreateView, EditView, IndexView
from wagtail.core import hooks
from .models import UnsplashPhoto
from .utils import get_unsplash_photo

View File

@ -14,6 +14,6 @@ class UnsplashPhotoChooser(AdminChooser):
def get_title(self, instance: UnsplashPhoto) -> str:
return format_html(
"<img src='{}' width=165 loading='lazy' decoding='async' referrerpolicy='no-referrer'>",
"<img src='{}' width=165 loading='lazy' decoding='async'>",
instance.get_thumbnail_url(),
)

View File

@ -1,5 +1,8 @@
from typing import Optional, Tuple
from django.db import models
from django.http.request import HttpRequest
from django_cache_decorator import django_cache_decorator
from wagtail.admin.panels import FieldPanel
from wagtail.images import get_image_model_string
from wagtail.images.models import Image
@ -9,6 +12,20 @@ from website.common.models import BasePage
from website.contrib.singleton_page.utils import SingletonPageCache
@django_cache_decorator(time=600)
def get_latest_blog_post() -> Optional[Tuple[str, str]]:
from website.blog.models import BlogPostPage
try:
latest_blog_post = (
BlogPostPage.objects.live().public().defer_streamfields().latest("date")
)
except BlogPostPage.DoesNotExist:
return None
return latest_blog_post.title, latest_blog_post.get_url()
class HomePage(BasePage, WagtailImageMetadataMixin):
max_count = 1
@ -38,22 +55,9 @@ class HomePage(BasePage, WagtailImageMetadataMixin):
return self.html_title
def get_context(self, request: HttpRequest) -> dict:
from website.blog.models import BlogPostListPage, BlogPostPage
from website.search.models import SearchPage
context = super().get_context(request)
context["recent_posts"] = list(
BlogPostPage.objects.live()
.public()
.defer_streamfields()
.order_by("-date")[:7]
)
context["latest_blog_post"] = (
context["recent_posts"].pop(0) if context["recent_posts"] else None
)
context["latest_blog_post"] = get_latest_blog_post()
context["search_page_url"] = SingletonPageCache.get_url(SearchPage, request)
context["blog_post_list_url"] = SingletonPageCache.get_url(
BlogPostListPage, request
)
return context

View File

@ -4,38 +4,21 @@
{% block main %}
<main {% if page.image %}style="background-image: url({% image_url page.image 'width-1200' %})"{% endif %}>
<div class="top-section">
<div class="heading-wrapper">
<h1>{{ page.heading }}</h1>
{% if search_page_url %}
<form action="{{ search_page_url }}">
<input id="search-input" class="input" type="text" placeholder="Search" name="q" />
</form>
{% endif %}
</div>
{% if latest_blog_post %}
<div class="box latest is-size-5">
<strong>Latest Post</strong>:
<a href="{% pageurl latest_blog_post %}">{{ latest_blog_post.title }}</a>
&rarr;
</div>
<div class="heading-wrapper">
<h1>{{ page.heading }}</h1>
{% if search_page_url %}
<form action="{{ search_page_url }}">
<input id="search-input" class="input" type="text" placeholder="Search" name="q" />
</form>
{% endif %}
</div>
<section class="container content recent-posts">
<h2 class="has-text-centered has-text-white is-size-3">Recent Posts</h2>
<div class="columns content-list is-multiline">
{% for page in recent_posts %}
{% include "home/home_page_card.html" %}
{% endfor %}
{% if latest_blog_post %}
<div class="box latest">
<strong>Latest Post</strong>:
<a href="{{ latest_blog_post.1 }}">{{ latest_blog_post.0 }}</a>
&rarr;
</div>
{% if blog_post_list_url %}
<div class="box">
<a href="{{ blog_post_list_url }}">View more &rarr;</a>
</div>
{% endif %}
</section>
{% endif %}
</main>
{% endblock %}

View File

@ -1,16 +0,0 @@
{% load wagtail_cache wagtailcore_tags %}
{% wagtailpagecache FRAGMENT_CACHE_TTL "homepage-card" %}
<div class="column is-one-third-widescreen is-half">
<div class="card">
<div class="card-image">
<a href="{% pageurl page %}">
<figure class="image is-16by9">
<img src="{{ page.list_image_url }}" alt="{{ page.hero_image_alt }}" loading="lazy" decoding="async" referrerpolicy="no-referrer" />
<p>{{ page.title }}</p>
</figure>
</a>
</div>
</div>
</div>
{% endwagtailpagecache %}

View File

@ -1,33 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-05 17:28
import wagtail.images.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("images", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="customimage",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
upload_to=wagtail.images.models.get_upload_to,
verbose_name="file",
width_field="width",
),
),
migrations.AlterField(
model_name="customrendition",
name="file",
field=wagtail.images.models.WagtailImageField(
height_field="height",
storage=wagtail.images.models.get_rendition_storage,
upload_to=wagtail.images.models.get_rendition_upload_to,
width_field="width",
),
),
]

View File

@ -10,5 +10,4 @@ urlpatterns = [
path("tags/<slug:slug>/", views.TagView.as_view()),
path("tags/", views.TagsView.as_view()),
path("categories/", views.TagsView.as_view()),
path("index.json", views.PageLinksView.as_view()),
]

View File

@ -12,18 +12,12 @@ class AllPagesFeedView(RedirectView):
permanent = True
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class PageLinksView(RedirectView):
pattern_name = "api:page-links"
permanent = True
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class TagView(RedirectView):
permanent = True
def get_redirect_url(self, slug: str) -> str:
tag = get_object_or_404(BlogPostTagPage.objects.public().live(), slug=slug)
tag = get_object_or_404(BlogPostTagPage, slug=slug)
return tag.get_url(request=self.request)

View File

@ -1,5 +1,4 @@
from django.core.paginator import EmptyPage, Paginator
from django.db import models
from django.http.request import HttpRequest
from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
from django.template.response import TemplateResponse
@ -13,7 +12,7 @@ from wagtail.search.utils import parse_query_string
from website.common.models import BaseContentPage, BaseListingPage
from website.common.utils import get_page_models
from .serializers import MIN_SEARCH_LENGTH, SearchPageParamsSerializer
from .serializers import MIN_SEARCH_LENGTH, SearchParamsSerializer
class SearchPage(RoutablePageMixin, BaseContentPage):
@ -44,21 +43,13 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
context["SEO_INDEX"] = False
return context
@classmethod
def get_listing_pages(cls) -> models.QuerySet:
return (
Page.objects.live()
.public()
.not_type(cls.__class__, *cls.EXCLUDED_PAGE_TYPES)
)
@route(r"^results/$")
@method_decorator(require_GET)
def results(self, request: HttpRequest) -> HttpResponse:
if not request.htmx:
return HttpResponseBadRequest()
serializer = SearchPageParamsSerializer(data=request.GET)
serializer = SearchParamsSerializer(data=request.GET)
if not serializer.is_valid():
return TemplateResponse(
@ -77,7 +68,12 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
}
filters, query = parse_query_string(search_query)
pages = self.get_listing_pages().search(query, order_by_relevance=True)
pages = (
Page.objects.live()
.public()
.not_type(self.__class__, *self.EXCLUDED_PAGE_TYPES)
.search(query, order_by_relevance=True)
)
paginator = Paginator(pages, self.PAGE_SIZE)
context["paginator"] = paginator

View File

@ -5,9 +5,5 @@ from website.common.serializers import PaginationSerializer
MIN_SEARCH_LENGTH = 3
class SearchParamSerializer(serializers.Serializer):
class SearchParamsSerializer(PaginationSerializer):
q = serializers.CharField(min_length=MIN_SEARCH_LENGTH)
class SearchPageParamsSerializer(SearchParamSerializer, PaginationSerializer):
pass

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>{{ site_title }}</ShortName>
<Description>{{ site_title }}</Description>
<InputEncoding>UTF-8</InputEncoding>
{% if favicon_url %}
<Image type="image/png">{{ favicon_url }}</Image>
{% endif %}
<Url type="text/html" template="{{ search_page_url }}?q={searchTerms}"/>
<Url type="application/x-suggestions+json" template="{{ search_suggestions_url }}?q={searchTerms}"/>
</OpenSearchDescription>

View File

@ -6,12 +6,12 @@
<section class="container search-controls">
<div class="field">
<p class="control has-icons-left has-icons-right">
<input type="search" class="input" name="q" placeholder="Search" hx-get="{{ search_url }}" hx-trigger="keyup changed delay:200ms, search{% if search_query %}, load{% endif %}" hx-target="#search-results" autocomplete="off" value="{{ search_query }}" hx-indicator=".search-indicator" />
<span class="icon is-small is-left htmx-indicator search-indicator" id="search-icon">
<input type="search" class="input" name="q" placeholder="Search" hx-get="{{ search_url }}" hx-trigger="keyup changed delay:200ms, search{% if search_query %}, load{% endif %}" hx-target="#search-results" autocomplete="off" value="{{ search_query }}" hx-indicator="#search-indicator" />
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
<span class="icon is-small is-left htmx-indicator search-indicator">
<i class="fas fa-spinner fa-pulse"></i>
<span class="icon is-small is-right htmx-indicator" id="search-indicator">
<i class="fas fa-circle-notch"></i>
</span>
</p>
</div>
@ -27,7 +27,7 @@
</div>
<div class="htmx-indicator" id="search-page-indicator">
<i class="fas fa-spinner fa-pulse"></i>
<i class="fas fa-circle-notch"></i>
</div>
</section>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% load wagtailadmin_tags %}
{% for page in results %}
{% include "common/listing-item.html" with breadcrumbs=True show_listing_images=True %}
{% include "common/listing-item.html" with breadcrumbs=True %}
{% endfor %}
{% if not results and page_num == 1 %}

View File

@ -1,6 +1,5 @@
from bs4 import BeautifulSoup
from django.test import TestCase
from django.urls import reverse
from website.common.factories import ContentPageFactory
from website.home.models import HomePage
@ -38,10 +37,10 @@ class SearchPageTestCase(TestCase):
self.assertEqual(search_input.attrs["name"], "q")
self.assertEqual(search_input.attrs["hx-get"], "results/")
self.assertNotIn("value", search_input.attrs) # Because of minify-html
self.assertEqual(search_input.attrs["value"], "")
self.assertEqual(len(soup.select(search_input.attrs["hx-target"])), 1)
self.assertEqual(len(soup.select(search_input.attrs["hx-indicator"])), 2)
self.assertEqual(len(soup.select(search_input.attrs["hx-indicator"])), 1)
class SearchPageResultsTestCase(TestCase):
@ -56,7 +55,7 @@ class SearchPageResultsTestCase(TestCase):
cls.url = cls.page.url + cls.page.reverse_subpage("results")
def test_returns_results(self) -> None:
with self.assertNumQueries(23):
with self.assertNumQueries(24):
response = self.client.get(self.url, {"q": "post"}, HTTP_HX_REQUEST="true")
self.assertEqual(response.status_code, 200)
@ -90,7 +89,7 @@ class SearchPageResultsTestCase(TestCase):
)
def test_too_high_page(self) -> None:
with self.assertNumQueries(42):
with self.assertNumQueries(49):
response = self.client.get(
self.url, {"q": "post", "page": 30}, HTTP_HX_REQUEST="true"
)
@ -111,114 +110,20 @@ class SearchPageResultsTestCase(TestCase):
self.assertContains(response, "No results found")
def test_no_query(self) -> None:
with self.assertNumQueries(6):
with self.assertNumQueries(7):
response = self.client.get(self.url, HTTP_HX_REQUEST="true")
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "search/enter-search-term.html")
def test_empty_query(self) -> None:
with self.assertNumQueries(6):
with self.assertNumQueries(7):
response = self.client.get(self.url, {"q": ""}, HTTP_HX_REQUEST="true")
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "search/enter-search-term.html")
def test_not_htmx(self) -> None:
with self.assertNumQueries(6):
with self.assertNumQueries(7):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
class OpenSearchTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.page = SearchPageFactory(parent=cls.home_page)
for i in range(6):
ContentPageFactory(parent=cls.home_page, title=f"Post {i}")
def test_opensearch_description(self) -> None:
with self.assertNumQueries(6):
response = self.client.get(reverse("opensearch"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, reverse("go"))
self.assertContains(response, reverse("opensearch-suggestions"))
def test_opensearch_suggestions(self) -> None:
with self.assertNumQueries(3):
response = self.client.get(reverse("opensearch-suggestions"), {"q": "post"})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data[0], "post")
self.assertEqual(data[1], [f"Post {i}" for i in range(5)])
class GoViewTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.search_page = SearchPageFactory(parent=cls.home_page)
cls.post_1 = ContentPageFactory(
parent=cls.home_page, title="Post Title 1", slug="post-slug-1"
)
cls.post_2 = ContentPageFactory(
parent=cls.home_page, title="Post Title 2", slug="post-slug-2"
)
def test_by_title(self) -> None:
with self.assertNumQueries(5):
response = self.client.get(reverse("go"), {"q": self.post_1.title})
self.assertRedirects(
response, self.post_1.get_url(), fetch_redirect_response=True
)
def test_by_slug(self) -> None:
with self.assertNumQueries(6):
response = self.client.get(reverse("go"), {"q": self.post_2.slug})
self.assertRedirects(
response, self.post_2.get_url(), fetch_redirect_response=True
)
def test_no_match(self) -> None:
with self.assertNumQueries(6):
response = self.client.get(reverse("go"), {"q": "Something else"})
self.assertRedirects(
response,
self.search_page.get_url() + "?q=Something+else",
fetch_redirect_response=True,
)
def test_no_query(self) -> None:
with self.assertNumQueries(3):
response = self.client.get(reverse("go"))
self.assertRedirects(
response, self.search_page.get_url(), fetch_redirect_response=True
)
def test_multiple_matches(self) -> None:
ContentPageFactory(parent=self.home_page, title=self.post_1.title)
with self.assertNumQueries(6):
response = self.client.get(reverse("go"), {"q": self.post_1.title})
self.assertRedirects(
response,
self.search_page.get_url() + f"?q={self.post_1.title}",
fetch_redirect_response=True,
)
def test_no_search_page(self) -> None:
self.search_page.delete()
response = self.client.get(reverse("go"))
self.assertEqual(response.status_code, 404)

View File

@ -1,13 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path("opensearch.xml", views.OpenSearchView.as_view(), name="opensearch"),
path(
"opensearch-suggestions/",
views.OpenSearchSuggestionsView.as_view(),
name="opensearch-suggestions",
),
path("go/", views.GoView.as_view(), name="go"),
]

View File

@ -1,90 +0,0 @@
from django.http import Http404, HttpRequest, JsonResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control, cache_page
from django.views.generic import RedirectView, TemplateView, View
from wagtail.search.utils import parse_query_string
from wagtail_favicon.models import FaviconSettings
from wagtail_favicon.utils import get_rendition_url
from website.common.utils import get_or_none, get_site_title
from website.contrib.singleton_page.utils import SingletonPageCache
from .models import SearchPage
from .serializers import SearchParamSerializer
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class OpenSearchView(TemplateView):
template_name = "search/opensearch.xml"
content_type = "application/xml"
def get_context_data(self, **kwargs: dict) -> dict:
context = super().get_context_data(**kwargs)
favicon_settings = FaviconSettings.for_request(self.request)
if favicon_settings.base_favicon_image_id:
context["favicon_url"] = self.request.build_absolute_uri(
get_rendition_url(
favicon_settings.base_favicon_image, "fill-100|format-png"
)
)
context["search_page_url"] = self.request.build_absolute_uri(reverse("go"))
context["search_suggestions_url"] = self.request.build_absolute_uri(
reverse("opensearch-suggestions")
)
context["site_title"] = get_site_title()
return context
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class OpenSearchSuggestionsView(View):
def get(self, request: HttpRequest) -> JsonResponse:
serializer = SearchParamSerializer(data=request.GET)
if not serializer.is_valid():
return JsonResponse(serializer.errors, status=400)
filters, query = parse_query_string(serializer.validated_data["q"])
results = (
SearchPage.get_listing_pages()
.search(query, order_by_relevance=True)[:5]
.get_queryset()
)
return JsonResponse(
[
serializer.validated_data["q"],
list(results.values_list("title", flat=True)),
],
safe=False,
)
@method_decorator(cache_page(60 * 60), name="dispatch")
class GoView(RedirectView):
def get_redirect_url(self) -> str:
serializer = SearchParamSerializer(data=self.request.GET)
search_page_url = SingletonPageCache.get_url(SearchPage, self.request)
if search_page_url is None:
raise Http404
if not serializer.is_valid():
return search_page_url
query = serializer.validated_data["q"]
pages = SearchPage.get_listing_pages()
if title_match := get_or_none(pages.filter(title__iexact=query)):
return title_match.get_url(request=self.request)
if slug_match := get_or_none(pages.filter(slug__iexact=query)):
return slug_match.get_url(request=self.request)
return f"{search_page_url}?{self.request.GET.urlencode()}"

View File

@ -42,7 +42,6 @@ INSTALLED_APPS = [
"website.utils",
"website.well_known",
"website.legacy",
"website.talks",
"website.contrib.code_block",
"website.contrib.mermaid_block",
"website.contrib.unsplash",
@ -79,7 +78,6 @@ INSTALLED_APPS = [
"wagtail_2fa",
"django_otp",
"django_otp.plugins.otp_totp",
"django_minify_html",
"health_check",
"health_check.db",
"health_check.cache",
@ -94,12 +92,12 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
"django.middleware.gzip.GZipMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
"enforce_host.EnforceHostMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"website.common.middleware.CustomMinifyHtmlMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -376,11 +374,6 @@ LOGGING = {
"level": "WARNING",
"propagate": False,
},
"wagtail.images": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "ERROR",
@ -405,6 +398,9 @@ SESSION_COOKIE_AGE = 2419200 # About a month
CSRF_COOKIE_SECURE = not DEBUG
SESSION_COOKIE_HTTPONLY = True
# https://github.com/wagtail/wagtail-autocomplete/issues/149
CSRF_COOKIE_HTTPONLY = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
PERMISSIONS_POLICY: dict[str, list] = {

View File

@ -1,17 +0,0 @@
from datetime import timedelta
from website.common.factories import BaseContentFactory, BaseListingFactory
from . import models
class TalksListPageFactory(BaseListingFactory):
class Meta:
model = models.TalksListPage
class TalkPageFactory(BaseContentFactory):
duration = timedelta(minutes=30)
class Meta:
model = models.TalkPage

View File

@ -1,354 +0,0 @@
# Generated by Django 5.0.1 on 2024-03-01 17:44
import django.db.models.deletion
import django.utils.timezone
import wagtail.blocks
import wagtail.contrib.routable_page.models
import wagtail.contrib.typed_table_block.blocks
import wagtail.embeds.blocks
import wagtail.fields
import wagtail.images.blocks
import wagtailmetadata.models
from django.db import migrations, models
import website.contrib.code_block.blocks
class Migration(migrations.Migration):
initial = True
dependencies = [
("images", "0002_alter_customimage_file_alter_customrendition_file"),
("unsplash", "0001_initial"),
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
]
operations = [
migrations.CreateModel(
name="TalkPage",
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",
),
),
("subtitle", wagtail.fields.RichTextField(blank=True)),
(
"body",
wagtail.fields.StreamField(
[
("embed", wagtail.embeds.blocks.EmbedBlock()),
("rich_text", wagtail.blocks.RichTextBlock()),
(
"lorem",
wagtail.blocks.StructBlock(
[
(
"paragraphs",
wagtail.blocks.IntegerBlock(min_value=1),
)
]
),
),
("html", wagtail.blocks.RawHTMLBlock()),
(
"image",
wagtail.blocks.StructBlock(
[
(
"image",
wagtail.images.blocks.ImageChooserBlock(),
),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"code",
wagtail.blocks.StructBlock(
[
(
"filename",
wagtail.blocks.CharBlock(
max_length=128, required=False
),
),
(
"language",
wagtail.blocks.ChoiceBlock(
choices=website.contrib.code_block.blocks.get_language_choices
),
),
("source", wagtail.blocks.TextBlock()),
]
),
),
(
"tangent",
wagtail.blocks.StructBlock(
[
(
"name",
wagtail.blocks.CharBlock(max_length=64),
),
(
"content",
wagtail.blocks.RichTextBlock(
editor="simple"
),
),
]
),
),
(
"mermaid",
wagtail.blocks.StructBlock(
[
("source", wagtail.blocks.TextBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"table",
wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
[
(
"rich_text",
wagtail.blocks.RichTextBlock(
editor="plain"
),
),
("numeric", wagtail.blocks.FloatBlock()),
("text", wagtail.blocks.CharBlock()),
]
),
),
(
"iframe",
wagtail.blocks.StructBlock(
[
("url", wagtail.blocks.URLBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
],
blank=True,
use_json_field=True,
),
),
("date", models.DateField(default=django.utils.timezone.now)),
("duration", models.DurationField()),
("slides_url", models.URLField(blank=True)),
("video_url", models.URLField(blank=True)),
("location_name", models.CharField(blank=True, max_length=64)),
("location_url", models.URLField(blank=True)),
(
"hero_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="images.customimage",
),
),
(
"hero_unsplash_photo",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="unsplash.unsplashphoto",
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page", wagtailmetadata.models.MetadataMixin),
),
migrations.CreateModel(
name="TalksListPage",
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",
),
),
(
"body",
wagtail.fields.StreamField(
[
("embed", wagtail.embeds.blocks.EmbedBlock()),
("rich_text", wagtail.blocks.RichTextBlock()),
(
"lorem",
wagtail.blocks.StructBlock(
[
(
"paragraphs",
wagtail.blocks.IntegerBlock(min_value=1),
)
]
),
),
("html", wagtail.blocks.RawHTMLBlock()),
(
"image",
wagtail.blocks.StructBlock(
[
(
"image",
wagtail.images.blocks.ImageChooserBlock(),
),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"code",
wagtail.blocks.StructBlock(
[
(
"filename",
wagtail.blocks.CharBlock(
max_length=128, required=False
),
),
(
"language",
wagtail.blocks.ChoiceBlock(
choices=website.contrib.code_block.blocks.get_language_choices
),
),
("source", wagtail.blocks.TextBlock()),
]
),
),
(
"tangent",
wagtail.blocks.StructBlock(
[
(
"name",
wagtail.blocks.CharBlock(max_length=64),
),
(
"content",
wagtail.blocks.RichTextBlock(
editor="simple"
),
),
]
),
),
(
"mermaid",
wagtail.blocks.StructBlock(
[
("source", wagtail.blocks.TextBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
(
"table",
wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
[
(
"rich_text",
wagtail.blocks.RichTextBlock(
editor="plain"
),
),
("numeric", wagtail.blocks.FloatBlock()),
("text", wagtail.blocks.CharBlock()),
]
),
),
(
"iframe",
wagtail.blocks.StructBlock(
[
("url", wagtail.blocks.URLBlock()),
(
"caption",
wagtail.blocks.RichTextBlock(
editor="plain", required=False
),
),
]
),
),
],
blank=True,
use_json_field=True,
),
),
(
"hero_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="images.customimage",
),
),
(
"hero_unsplash_photo",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="unsplash.unsplashphoto",
),
),
],
options={
"abstract": False,
},
bases=(
wagtail.contrib.routable_page.models.RoutablePageMixin,
"wagtailcore.page",
wagtailmetadata.models.MetadataMixin,
),
),
]

View File

@ -1,62 +0,0 @@
from datetime import timedelta
from typing import Any
from django.db import models
from django.utils import timezone
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from website.common.models import BaseContentPage, BaseListingPage
class TalksListPage(BaseListingPage):
max_count = 1
subpage_types = ["talks.TalkPage"]
class TalkPage(BaseContentPage):
subpage_types: list[Any] = []
parent_page_types = [TalksListPage]
date = models.DateField(default=timezone.now)
duration = models.DurationField()
slides_url = models.URLField(blank=True)
video_url = models.URLField(blank=True)
location_name = models.CharField(max_length=64, blank=True)
location_url = models.URLField(blank=True)
content_panels = BaseContentPage.content_panels + [
MultiFieldPanel(
[
FieldPanel("slides_url"),
FieldPanel("video_url"),
],
heading="Media",
),
MultiFieldPanel(
[
FieldPanel("location_name"),
FieldPanel("location_url"),
],
heading="Location",
),
FieldPanel("duration"),
]
promote_panels = BaseContentPage.promote_panels + [
FieldPanel("date"),
]
@property
def show_table_of_contents(self) -> bool:
return False
@property
def reading_time(self) -> timedelta:
return self.duration
@property
def word_count(self) -> int:
return 0

View File

@ -1,11 +0,0 @@
{% extends "common/content_page.html" %}
{% load wagtailembeds_tags %}
{% block pre_content %}
{% if page.video_url %}
<section class="container mb-5 content">
<div class="block-embed">{% embed page.video_url %}</div>
</section>
{% endif %}
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "common/listing_page.html" %}
{% load wagtailroutablepage_tags %}
{% block post_content %}
<section class="container listing">
{% for page in listing_pages %}
{% ifchanged %}
<h2 id="date-{{ page.date.year }}" class="date-header">
<time datetime="{{ page.date.year }}" title="{{ page.date.year }}">
{{ page.date.year }}
</time>
</h2>
{% endifchanged %}
{% include "common/listing-item.html" %}
{% endfor %}
</section>
{% if listing_pages.has_other_pages %}
<section class="container">
<hr class="my-5" />
{% include "common/pagination.html" with page=listing_pages %}
</section>
{% endif %}
{% endblock %}

View File

@ -1,47 +0,0 @@
from django.test import TestCase
from django.urls import reverse
from website.home.models import HomePage
from .factories import TalkPageFactory, TalksListPageFactory
class TalkPageTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.list_page = TalksListPageFactory(parent=cls.home_page)
cls.page = TalkPageFactory(parent=cls.list_page)
def test_accessible(self) -> None:
response = self.client.get(self.page.url)
self.assertEqual(response.status_code, 200)
def test_queries(self) -> None:
with self.assertNumQueries(34):
self.client.get(self.page.url)
class TalksListPageTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.page = TalksListPageFactory(parent=cls.home_page)
TalkPageFactory(parent=cls.page)
TalkPageFactory(parent=cls.page)
def test_accessible(self) -> None:
response = self.client.get(self.page.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["listing_pages"]), 2)
def test_queries(self) -> None:
with self.assertNumQueries(35):
self.client.get(self.page.url)
def test_feed_accessible(self) -> None:
response = self.client.get(self.page.url + self.page.reverse_subpage("feed"))
self.assertRedirects(
response, reverse("feed"), status_code=301, fetch_redirect_response=True
)

View File

@ -50,7 +50,6 @@ urlpatterns = [
path("feed/", AllPagesFeed(), name="feed"),
path(".health/", include("health_check.urls")),
path("", include("website.legacy.urls")),
path("", include("website.search.urls")),
path("api/", include("website.api.urls", namespace="api")),
path(
"@jake",
@ -58,11 +57,6 @@ urlpatterns = [
),
path("favicon.ico", FaviconView.as_view()),
path("", include(favicon_urls)),
re_path(
r"^%s(?P<path>.*)$" % re.escape(settings.MEDIA_URL.lstrip("/")),
cache_control(max_age=60 * 60)(serve),
{"document_root": settings.MEDIA_ROOT},
),
]
@ -77,6 +71,15 @@ if settings.DEBUG:
# Add django-debug-toolbar
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))
urlpatterns.append(
# Media is served by nginx in production
re_path(
r"^%s(?P<path>.*)$" % re.escape(settings.MEDIA_URL.lstrip("/")),
cache_control(max_age=60 * 60)(serve),
{"document_root": settings.MEDIA_ROOT},
)
)
if settings.DEBUG or settings.TEST:
urlpatterns.extend(

View File

@ -3,9 +3,12 @@ from django.http.request import HttpRequest
def global_vars(request: HttpRequest) -> dict:
# noop caching in preview
fragment_cache_ttl = 0 if getattr(request, "is_preview", False) else 3600
return {
"SEO_INDEX": settings.SEO_INDEX,
"DEBUG": settings.DEBUG,
"FRAGMENT_CACHE_TTL": 3600,
"FRAGMENT_CACHE_TTL": fragment_cache_ttl,
"FRAGMENT_CACHE_TTL_JITTER": fragment_cache_ttl * 0.1,
"ACTIVITYPUB_HOST": settings.ACTIVITYPUB_HOST,
}

View File

@ -1,5 +1,4 @@
from datetime import timedelta
from urllib.parse import urljoin
from django.conf import settings
from django.http.request import HttpRequest
@ -57,13 +56,10 @@ def activitypub_proxy(request: HttpRequest) -> HttpResponse:
if not settings.ACTIVITYPUB_HOST:
raise Http404
activitypub_url = urljoin(
"https://" + settings.ACTIVITYPUB_HOST,
request.path,
allow_fragments=True,
)
try:
return proxy_view(request, activitypub_url)
return proxy_view(
request,
f"https://{settings.ACTIVITYPUB_HOST}{request.path}",
)
except RequestException:
return HttpResponse(status=502)
return HttpResponse(status_code=502)