78 Commits

Author SHA1 Message Date
1fdd316d0d feat(models.py): add users to gitea teams on purchase/refund 2026-05-05 12:46:07 +02:00
da5662ceee feat(base.py): add GITEA_ROOT_URL and configure logger 2026-05-05 12:45:35 +02:00
9acb4c8d7c feat(module_lesson_page.html): auto login to gitea when viewing repo 2026-05-05 12:45:08 +02:00
36c14ab939 feat(pages.py): add gitea login redirect url property for handling links to repo 2026-05-05 12:44:37 +02:00
a9d7aef6dd feat(signals.py): add gitea repo to team 2026-05-05 12:44:01 +02:00
fa942809d9 chore(signals.py): remove unused commented code 2026-05-05 12:43:28 +02:00
29f8475b88 feat(module_lesson_page.html): add link to Gitea code 2026-04-23 19:56:09 +02:00
1875c6fd97 feat(blog_page.html): add BlogPage template 2026-04-23 19:54:51 +02:00
92aa1fc024 feat(blog_index_page.html): add BlogIndexPage template 2026-04-23 19:54:20 +02:00
3551d2b654 feat(header.html): add blog link to header 2026-04-23 19:54:02 +02:00
4c21e324f6 feat(pages.py): add posts ctx variable to BlogIndexPage 2026-04-23 19:53:41 +02:00
80a80553fe chore(home/views.py): remove debug print 2026-04-23 19:13:30 +02:00
15e74660f5 feat(admin_chat.html): add back button to admin chat 2026-04-23 19:13:10 +02:00
2b34cadf24 refactor(admin_chat.html): style chat 2026-04-23 19:09:46 +02:00
6dd3355087 refactor(user_chat.html): style chat 2026-04-23 18:58:31 +02:00
bfdc953e0f feat(header.html): add chat link to header 2026-04-23 18:57:39 +02:00
27567caf78 fix(forms.py): fix error when gitea account already exists 2026-04-23 18:57:18 +02:00
97df6349ab feat(pages.py): delete event's future occurrences if unpublished 2026-04-23 18:17:24 +02:00
6d355b603d fix(pages.py): fix event's other occurrences not being deleted when changing event from recurrent to single
Closes #1
2026-04-23 18:10:05 +02:00
f9f812caf0 chore(.gitignore): ignore gitea data 2026-04-22 20:39:59 +02:00
299a600b09 fix(docker-compose.yml): run gitea on host network 2026-04-22 20:39:47 +02:00
0cb2794eff chore(gitea): add gitea config 2026-04-22 20:39:26 +02:00
3d51c6f043 build(uv.lock): update depedencies 2026-04-22 20:38:27 +02:00
2a15556513 refactor(base.py): use relative DB path 2026-04-22 20:38:14 +02:00
ffa0b03661 fix(signals.py): fix repo creation even if create_gitea_repo False 2026-04-22 20:37:42 +02:00
3ec8963030 chore(0023): add EventIndexPage 2026-04-22 20:36:39 +02:00
d70bf79107 feat(pages.py): add EventIndexPage 2026-04-22 20:36:22 +02:00
daf0b05bb8 feat: add gitea docker 2026-04-02 14:59:29 +02:00
35d6bb5f2e docs(README.md): add README 2026-04-02 10:40:09 +02:00
dd936473d8 feat(signals.py): create per-lesson repositories on ModuleLessonPage save 2026-03-30 10:58:01 +02:00
787440d56f chore(migrations/0022): create BlogIndexPage and BlogPage 2026-03-30 10:13:12 +02:00
37b0a6a95b feat(pages.py): add blog pages 2026-03-30 10:13:12 +02:00
345914a519 chore(migrations/0021): add create_gitea_repo and gitea_repo_url to ModuleLessonPage 2026-03-30 10:13:12 +02:00
7e24ded8ee fix(pages.py): generate event occurrences only if live 2026-03-30 10:13:12 +02:00
64edf6656e feat(pages.py): add per-lesson repo fields 2026-03-30 10:13:03 +02:00
a2ad8e7ac9 feat(module_lesson_page.html): add link to course library 2026-03-23 14:03:38 +01:00
a0b4697c61 feat(course_module_page.html): add link to course library 2026-03-23 14:03:29 +01:00
983384f62b feat(course_page.html): add link to course library in CoursePage 2026-03-23 14:03:10 +01:00
668ddccea5 feat(settings/base.py): add LOGGING config 2026-03-23 14:02:24 +01:00
6dd826c3bd feat(home/signals.py): create gitea team and repo for course on CoursePage save 2026-03-23 14:02:07 +01:00
e74c1fb28d chore(migrations/0020): add repository_url field to CoursePage 2026-03-23 13:46:24 +01:00
cb19bc6262 feat(models/pages.py): add repository_url field to CoursePage 2026-03-23 13:45:19 +01:00
a918ee73c4 fix(models/pages.py): ensure course has ID before creating group 2026-03-23 13:44:33 +01:00
5913e847bc refactor(forms.py): move gitea account creation login to separate function 2026-03-20 14:47:12 +01:00
18b21b0892 feat(forms.py): create gitea account on signup 2026-03-20 14:37:40 +01:00
efb3799e12 feat(forms.py): capitalize first and last name 2026-03-20 14:37:22 +01:00
306d39bd22 feat(oauth_validators.py): use user ID for gitea username 2026-03-20 14:36:27 +01:00
e503d69235 feat(locale/pl): update polish locale 2026-03-20 13:22:39 +01:00
57ec3162d0 feat(locale/en): update english locale 2026-03-20 13:22:39 +01:00
c4e9ec5484 feat(kursy/urls.py): include purchase urls 2026-03-20 13:18:39 +01:00
c8732a05cb chore(settings/base.py): add purchase to INSTALLED_APPS 2026-03-20 13:18:07 +01:00
6810e540e5 feat(purchase/views.py): add purchase and refund views 2026-03-20 13:17:47 +01:00
21500e0f10 feat(purchase/urls.py): add purchase/urls.py 2026-03-20 13:17:18 +01:00
be42d71bb8 feat(course_page.html): add refund button and make purchase button work 2026-03-20 13:16:36 +01:00
b5e9e1ec66 feat(home/models/pages.py): add mock purchase login and auto group creation 2026-03-20 13:16:00 +01:00
d575c836e9 feat(purchase/models.py): handle mock refunds 2026-03-20 13:14:03 +01:00
84a6c4cf5e feat(purchase/): add purchase app 2026-03-20 12:03:57 +01:00
e46f034d9e refactor(header.html): use 'Calendar' instead of 'Course Calendar' as it's shorter 2026-03-19 18:18:04 +01:00
dc7e34f5b6 feat(locale/en): update english translations 2026-03-19 18:16:20 +01:00
f002651e2a feat(locale/pl): update polish translations 2026-03-19 18:16:09 +01:00
c789eeb4ff feat(header.html): make header sticky on desktop 2026-03-19 18:11:14 +01:00
acb6ea58ce chore(templates/welcome_page.html): remove unused template 2026-03-19 17:58:56 +01:00
72fca4228c feat(home/course_page.html): add description to course page template 2026-03-19 17:56:53 +01:00
9f779407af feat(header.html): add link to course index page and move course calendar to left side 2026-03-19 17:56:27 +01:00
f2f594afb6 feat(templates/course_index_page.html): add CourseIndexPage template 2026-03-19 17:55:31 +01:00
95ab896e5f chore(migrations/0019): add description to CoursePage 2026-03-19 17:54:57 +01:00
4f58cb0320 feat(models/pages.py): add description field to CoursePage 2026-03-19 17:54:35 +01:00
294ea9a28b chore(migrations/0018): add CourseIndexPage 2026-03-19 17:54:15 +01:00
e56aff1a5c feat(models/pages.py): add CourseIndexPage 2026-03-19 17:53:53 +01:00
71d4580a82 feat(settings/base.py): add GITEA_URL config 2026-03-19 15:26:58 +01:00
0356374870 feat(settings/base.py): add OAUTH2_PROVIDER config 2026-03-19 15:26:42 +01:00
ffc33d3be4 feat(oauth_validators.py): add CustomOAuth2Validator to supply OIDC scopes 2026-03-19 15:23:07 +01:00
730e041794 feat(kursy/urls.py): add oauth2/ url 2026-03-19 15:22:11 +01:00
e0f3f094ff feat(home/apps.py): register signals 2026-03-19 15:21:26 +01:00
157ee875e1 feat(signals.py): add signals 2026-03-19 15:20:36 +01:00
12de01c2dc chore(settings/base.py): add oauth to INSTALLED_APPS and MIDDLEWARE 2026-03-18 11:47:58 +01:00
718aeb9cf5 build(uv.lock): add depedency on django-oauth-toolkit 2026-03-18 11:47:13 +01:00
9761daf820 build(pyproject.toml): add depedency on django-oauth-toolkit 2026-03-18 11:47:04 +01:00
45 changed files with 2612 additions and 407 deletions

View File

@@ -1,7 +1,7 @@
# Django project
/media/
/static/
*.sqlite3
# *.sqlite3
# Python and others
__pycache__
@@ -37,3 +37,6 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
.venv/
gitea/

3
.gitignore vendored
View File

@@ -55,3 +55,6 @@ docs/ref/modules/
# Media (managed by wagtail)
media/
# Gitea data
gitea/data/

View File

@@ -1,60 +1,57 @@
# Use an official Python runtime based on Debian 12 "bookworm" as a parent image.
FROM python:3.12-slim-bookworm
FROM python:3.14-slim-bookworm AS builder
# Add user that will be used in the container.
RUN useradd wagtail
RUN useradd -m kursy
# Port used by this container to serve HTTP.
EXPOSE 8000
ENV NODE_VERSION=24.14.1
# Set environment variables.
# 1. Force Python stdout and stderr streams to be unbuffered.
# 2. Set PORT variable that is used by Gunicorn. This should match "EXPOSE"
# command.
ENV PYTHONUNBUFFERED=1 \
PORT=8000
# install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Install system packages required by Wagtail and Django.
RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \
build-essential \
libpq-dev \
libmariadb-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
# install nodejs
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs
# Install the application server.
RUN pip install "gunicorn==20.0.4"
# copy project files
WORKDIR /app
COPY pyproject.toml uv.lock ./
# Install the project requirements.
COPY requirements.txt /
RUN pip install -r /requirements.txt
RUN chown kursy:kursy /app -R
# Use /app folder as a directory where the source code is stored.
USER kursy
# install project dependencies
RUN uv sync
COPY --chown=kursy:kursy . .
# install tailwind
WORKDIR /app/theme/static_src
RUN npm install
# collect static files
WORKDIR /app
RUN uv run python manage.py collectstatic --noinput --clear
# --- RUNTIME IMAGE ---
FROM python:3.14-slim-bookworm
RUN useradd -m kursy
WORKDIR /app
# Set this directory to be owned by the "wagtail" user. This Wagtail project
# uses SQLite, the folder needs to be owned by the user that
# will be writing to the database file.
RUN chown wagtail:wagtail /app
RUN mkdir -p /app/data && chown kursy:kursy /app/data
# Copy the source code of the project into the container.
COPY --chown=wagtail:wagtail . .
USER kursy
# Use user "wagtail" to run the build commands below and the server itself.
USER wagtail
COPY --from=builder --chown=kursy:kursy /app/.venv /app/.venv
COPY --from=builder --chown=kursy:kursy /app /app
# Collect static files.
RUN python manage.py collectstatic --noinput --clear
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PORT=8000
EXPOSE 8000
CMD set -xe; python manage.py migrate --noinput; python manage.py runserver 0.0.0.0:8000
# Runtime command that executes when "docker run" is called, it does the
# following:
# 1. Migrate the database.
# 2. Start the application server.
# WARNING:
# Migrating database at the same time as starting the server IS NOT THE BEST
# PRACTICE. The database should be migrated manually or using the release
# phase facilities of your hosting platform. This is used only so the
# Wagtail instance can be started with a simple "docker run" command.
CMD set -xe; python manage.py migrate --noinput; gunicorn kursy.wsgi:application

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# Studio77 (kursy)
## Instalacja
### Wymagania
- `uv` >= 0.11.0
- `python` >= 3.14 (instalowany automatycznie przez `uv`)
- `node` >= 24.14.1 (LTS)
- `npm` >= 9.2.0 (LTS)
### Instalacja wymaganych narzędzi
1. `uv` - można zainstalować za pomocą skryptu instalacyjnego dostępnego na [stronie projektu](https://docs.astral.sh/uv/getting-started/installation):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. `node` i `npm` - można zainstalować za pomocą [Node Version Manager (nvm)](https://nodejs.org/en/download):
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
\. "$HOME/.nvm/nvm.sh"
nvm install 24
```
### Instalacja projektu
1. Sklonuj repozytorium:
```bash
git clone http://192.168.190:3000/StudioCodeLab/kursy.git
cd kursy
```
2. Zainstaluj zależności:
```bash
uv sync
```
3. Zainstaluj zależności tailwind:
```bash
cd theme/static_src
npm install
cd ../..
```
## Pierwsze uruchomienie
1. Wykonaj migracje bazy danych:
```bash
uv run python manage.py migrate
```
2. Utwórz superużytkownika (admina):
```bash
uv run python manage.py createsuperuser
```
## Użycie
### Uruchom serwery
- Django:
```bash
uv run python manage.py runserver
```
- Tailwind:
```bash
cd theme/static_src
npm run dev
```
> [!IMPORTANT]
> Oba serwery muszą być uruchomione równolegle, aby aplikacja działała poprawnie.
## Autorzy
- [Artur Borecki](https://github.com/pufereq)

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
networks:
gitea:
external: false
services:
gitea:
image: docker.gitea.com/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__ROOT_URL=http://localhost:3000/
restart: always
network_mode: host
# networks:
# - gitea
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./gitea/data:/data
# - ./gitea/config:/data/gitea/conf
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "3022:3022"
# kursy:
# build: .
# container_name: kursy
# restart: unless-stopped
# networks:
# - gitea
# volumes:
# - ./db:/app/db
# ports:
# - "8000:8000"

820
gitea/config/app.ini Normal file
View File

@@ -0,0 +1,820 @@
; ; Copy required sections to your own app.ini (default is custom/conf/app.ini)
; ; and modify as needed.
; ; Do not copy the whole file as-is, as it contains some invalid sections for illustrative purposes.
; ; If you don't know what a setting is you should not set it.
; ;
; ; see https://docs.gitea.com/administration/config-cheat-sheet for additional documentation.
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; Default Configuration (non-`app.ini` configuration)
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; These values are environment-dependent but form the basis of a lot of values. They will be
; ; reported as part of the default configuration when running `gitea help` or on start-up. The order they are emitted there is slightly different but we will list them here in the order they are set-up.
; ;
; ; - _`AppPath`_: This is the absolute path of the running gitea binary.
; ; - _`AppWorkPath`_: This refers to "working path" of the `gitea` binary. It is determined by using the first set thing in the following hierarchy:
; ; - The "WORK_PATH" option in "app.ini" file
; ; - The `--work-path` flag passed to the binary
; ; - The environment variable `$GITEA_WORK_DIR`
; ; - A built-in value set at build time (see building from source)
; ; - Otherwise it defaults to the directory of the _`AppPath`_
; ; - If any of the above are relative paths then they are made absolute against the directory of the _`AppPath`_
; ; - _`CustomPath`_: This is the base directory for custom templates and other options. It is determined by using the first set thing in the following hierarchy:
; ; - The `--custom-path` flag passed to the binary
; ; - The environment variable `$GITEA_CUSTOM`
; ; - A built-in value set at build time (see building from source)
; ; - Otherwise it defaults to _`AppWorkPath`_`/custom`
; ; - If any of the above are relative paths then they are made absolute against the directory of the _`AppWorkPath`_
; ; - _`CustomConf`_: This is the path to the `app.ini` file.
; ; - The `--config` flag passed to the binary
; ; - A built-in value set at build time (see building from source)
; ; - Otherwise it defaults to _`CustomPath`_`/conf/app.ini`
; ; - If any of the above are relative paths then they are made absolute against the directory of the _`CustomPath`_
; ;
; ; In addition there is _`StaticRootPath`_ which can be set as a built-in at build time, but will otherwise default to _`AppWorkPath`_
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; General Settings
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; App name that shows in every page title
; Gitea: Git with a cup of tea
APP_NAME = Studio77 Gitea (test instance)
; ;
; ; RUN_USER will automatically detect the current user - but you can set it here change it if you run locally
; git
RUN_USER = gitea
WORK_PATH = /var/lib/gitea
RUN_MODE = prod
; ;
; ; Application run mode, affects performance and debugging: "dev" or "prod", default is "prod"
; ; Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are treated as "prod" which is for production use.
; RUN_MODE = prod
; ;
; ; The working directory, see the comment of AppWorkPath above
; WORK_PATH =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[server]
SSH_DOMAIN = 127.0.0.1
DOMAIN = 127.0.0.1
HTTP_PORT = 3000
ROOT_URL = http://localhost:3000/
APP_DATA_PATH = /var/lib/gitea/data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = ZlMvLXiVZtuaVymLGuO4Qg5dFHCQ45fIzui99Qw9ecM
OFFLINE_MODE = true
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
; PROTOCOL = http
; ;
; ; Set the domain for the server.
; DOMAIN = localhost
; ;
; ; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
; ; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
; ROOT_URL =
; ;
; ; Controls how to detect the public URL.
; ; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior,
; ; especially when the Gitea instance needs to be accessed in a container network.
; ; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
; ; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
; PUBLIC_URL_DETECTION = legacy
; ;
; ; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
; ; DO NOT USE IT IN PRODUCTION!!!
; USE_SUB_URL_PATH = false
; ;
; ; when STATIC_URL_PREFIX is empty it will follow ROOT_URL
; STATIC_URL_PREFIX =
; ;
; ; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
; ; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use.
; ; Relative paths will be made absolute against the _`AppWorkPath`_.
; HTTP_ADDR = 0.0.0.0
; ;
; ; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket.
; HTTP_PORT = 3000
; ;
; ; Expect PROXY protocol headers on connections
; USE_PROXY_PROTOCOL = false
; ;
; ; Use PROXY protocol in TLS Bridging mode
; PROXY_PROTOCOL_TLS_BRIDGING = false
; ;
; ; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
; PROXY_PROTOCOL_HEADER_TIMEOUT = 5s
; ;
; ; Accept PROXY protocol headers with UNKNOWN type
; PROXY_PROTOCOL_ACCEPT_UNKNOWN = false
; ;
; ; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server
; ; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main
; ; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for
; ; PORT_TO_REDIRECT.
; REDIRECT_OTHER_PORT = false
; PORT_TO_REDIRECT = 80
; ;
; ; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL
; REDIRECTOR_USE_PROXY_PROTOCOL =
; ; Minimum and maximum supported TLS versions
; SSL_MIN_VERSION=TLSv1.2
; SSL_MAX_VERSION=
; ;
; ; SSL Curve Preferences
; SSL_CURVE_PREFERENCES=X25519,P256
; ;
; ; SSL Cipher Suites
; SSL_CIPHER_SUITES=; Will default to "ecdhe_ecdsa_with_aes_256_gcm_sha384,ecdhe_rsa_with_aes_256_gcm_sha384,ecdhe_ecdsa_with_aes_128_gcm_sha256,ecdhe_rsa_with_aes_128_gcm_sha256,ecdhe_ecdsa_with_chacha20_poly1305,ecdhe_rsa_with_chacha20_poly1305" if aes is supported by hardware, otherwise chacha will be first.
; ;
; ; Timeout for any write to the connection. (Set to -1 to disable all timeouts.)
; PER_WRITE_TIMEOUT = 30s
; ;
; ; Timeout per Kb written to connections.
; PER_WRITE_PER_KB_TIMEOUT = 30s
; ;
; ; Permission for unix socket
; UNIX_SOCKET_PERMISSION = 666
; ;
; ; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. In
; ; most cases you do not need to change the default value. Alter it only if
; ; your SSH server node is not the same as HTTP node. For different protocol, the default
; ; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`.
; ; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
; ; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`.
; ; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
; ; Most users don't need (and shouldn't) set this value.
; LOCAL_ROOT_URL =
; ;
; ; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL
; LOCAL_USE_PROXY_PROTOCOL =
; ;
; ; Disable SSH feature when not available
; DISABLE_SSH = false
; ;
; ; Whether to use the builtin SSH server or not.
; START_SSH_SERVER = false
; ;
; ; Expect PROXY protocol header on connections to the built-in SSH server
; SSH_SERVER_USE_PROXY_PROTOCOL = false
; ;
; ; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
; BUILTIN_SSH_SERVER_USER =
; ;
; ; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
; SSH_DOMAIN =
; ;
; ; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER.
; ; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username.
; ; This option is only for some advanced users who have configured their SSH reverse-proxy
; ; and need to use different usernames for git SSH clone.
; ; Most users should just leave it blank.
; SSH_USER =
; ;
; ; The network interface the builtin SSH server should listen on
; SSH_LISTEN_HOST =
; ;
; ; Port number to be exposed in clone URL
; SSH_PORT = 22
; ;
; ; The port number the builtin SSH server should listen on, defaults to SSH_PORT
; SSH_LISTEN_PORT =
; ;
; ; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
; SSH_ROOT_PATH =
; ;
; ; Gitea will create a authorized_keys file by default when it is not using the internal ssh server
; ; If you intend to use the AuthorizedKeysCommand functionality then you should turn this off.
; SSH_CREATE_AUTHORIZED_KEYS_FILE = true
; ;
; ; Gitea will create a authorized_principals file by default when it is not using the internal ssh server
; ; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off.
; SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true
; ;
; ; For the builtin SSH server, choose the supported ciphers/key-exchange-algorithms/MACs for SSH connections.
; ; The supported names are listed in https://github.com/golang/crypto/blob/master/ssh/common.go.
; ; Leave them empty to use the Golang crypto's recommended default values.
; ; For system SSH (non-builtin SSH server), this setting has no effect.
; SSH_SERVER_CIPHERS =
; SSH_SERVER_KEY_EXCHANGES =
; SSH_SERVER_MACS =
; ;
; ; For the built-in SSH server, choose the keypair to offer as the host key
; ; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
; ; relative paths are made absolute relative to the APP_DATA_PATH
; SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
; ;
; ; Enable SSH Authorized Key Backup when rewriting all keys, default is false
; SSH_AUTHORIZED_KEYS_BACKUP = false
; ;
; ; Determines which principals to allow
; ; - empty: if SSH_TRUSTED_USER_CA_KEYS is empty this will default to off, otherwise will default to email, username.
; ; - off: Do not allow authorized principals
; ; - email: the principal must match the user's email
; ; - username: the principal must match the user's username
; ; - anything: there will be no checking on the content of the principal
; SSH_AUTHORIZED_PRINCIPALS_ALLOW = email, username
; ;
; ; Enable SSH Authorized Principals Backup when rewriting all keys, default is true
; SSH_AUTHORIZED_PRINCIPALS_BACKUP = true
; ;
; ; Specifies the public keys of certificate authorities that are trusted to sign user certificates for authentication.
; ; Multiple keys should be comma separated.
; ; E.g."ssh-<algorithm> <key>". or "ssh-<algorithm> <key1>, ssh-<algorithm> <key2>".
; ; For more information see "TrustedUserCAKeys" in the sshd config manpages.
; SSH_TRUSTED_USER_CA_KEYS =
; ; Absolute path of the `TrustedUserCaKeys` file gitea will manage.
; ; Default this `RUN_USER`/.ssh/gitea-trusted-user-ca-keys.pem
; ; If you're running your own ssh server and you want to use the gitea managed file you'll also need to modify your
; ; sshd_config to point to this file. The official docker image will automatically work without further configuration.
; SSH_TRUSTED_USER_CA_KEYS_FILENAME =
; ;
; ; Enable exposure of SSH clone URL to anonymous visitors, default is false
; SSH_EXPOSE_ANONYMOUS = false
; ;
; ; Timeout for any write to ssh connections. (Set to -1 to disable all timeouts.)
; ; Will default to the PER_WRITE_TIMEOUT.
; SSH_PER_WRITE_TIMEOUT = 30s
; ;
; ; Timeout per Kb written to ssh connections.
; ; Will default to the PER_WRITE_PER_KB_TIMEOUT.
; SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
; ;
; ; Indicate whether to check minimum key size with corresponding type
; MINIMUM_KEY_SIZE_CHECK = false
; ;
; ; Disable CDN even in "prod" mode
; OFFLINE_MODE = true
; ;
; ; TLS Settings: Either ACME or manual
; ; (Other common TLS configuration are found before)
; ENABLE_ACME = false
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; ACME automatic TLS settings
; ;
; ; ACME directory URL (e.g. LetsEncrypt's staging/testing URL: https://acme-staging-v02.api.letsencrypt.org/directory)
; ; Leave empty to default to LetsEncrypt's (production) URL
; ACME_URL =
; ;
; ; Explicitly accept the ACME's TOS. The specific TOS cannot be retrieved at the moment.
; ACME_ACCEPTTOS = false
; ;
; ; If the ACME CA is not in your system's CA trust chain, it can be manually added here
; ACME_CA_ROOT =
; ;
; ; Email used for the ACME registration service
; ; Can be left blank to initialize at first run and use the cached value
; ACME_EMAIL =
; ;
; ; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
; ; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
; ACME_DIRECTORY = https
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Manual TLS settings: (Only applicable if ENABLE_ACME=false)
; ;
; ; Generate steps:
; ; $ ./gitea cert -ca=true -duration=8760h0m0s -host=myhost.example.com
; ;
; ; Or from a .pfx file exported from the Windows certificate store (do
; ; not forget to export the private key):
; ; $ openssl pkcs12 -in cert.pfx -out cert.pem -nokeys
; ; $ openssl pkcs12 -in cert.pfx -out key.pem -nocerts -nodes
; ; Paths are relative to CUSTOM_PATH
; CERT_FILE = https/cert.pem
; KEY_FILE = https/key.pem
; ;
; ; Root directory containing templates and static files.
; ; default is the path where Gitea is executed
; STATIC_ROOT_PATH = ; Will default to the built-in value _`StaticRootPath`_
; ;
; ; Default path for App data
; APP_DATA_PATH = data ; relative paths will be made absolute with _`AppWorkPath`_
; ;
; ; Base path for App's temp files, leave empty to use the managed tmp directory in APP_DATA_PATH
; APP_TEMP_PATH =
; ;
; ; Enable gzip compression for runtime-generated content, static resources excluded
; ENABLE_GZIP = false
; ;
; ; Application profiling (memory and cpu)
; ; For "web" command it listens on localhost:6060
; ; For "serve" command it dumps to disk at PPROF_DATA_PATH as (cpuprofile|memprofile)_<username>_<temporary id>
; ENABLE_PPROF = false
; ;
; ; PPROF_DATA_PATH, use an absolute path when you start gitea as service
; PPROF_DATA_PATH = data/tmp/pprof ; Path is relative to _`AppWorkPath`_
; ;
; ; Landing page, can be "home", "explore", "organizations", "login", or any URL such as "/org/repo" or even "https://anotherwebsite.com"
; ; The "login" choice is not a security measure but just a UI flow change, use REQUIRE_SIGNIN_VIEW to force users to log in.
; LANDING_PAGE = home
; ;
; ; Enables git-lfs support. true or false, default is false.
; LFS_START_SERVER = false
; ;
; ; Enables git-lfs SSH protocol support. true or false, default is false.
; LFS_ALLOW_PURE_SSH = false
; ;
; ; LFS authentication secret, change this yourself
; LFS_JWT_SECRET =
; ;
; ; Alternative location to specify LFS authentication secret. You cannot specify both this and LFS_JWT_SECRET, and must pick one
; LFS_JWT_SECRET_URI = file:/etc/gitea/lfs_jwt_secret
; ;
; ; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail.
; LFS_HTTP_AUTH_EXPIRY = 24h
; ;
; ; Maximum allowed LFS file size in bytes (Set to 0 for no limit).
; LFS_MAX_FILE_SIZE = 0
; ;
; ; Maximum number of locks returned per page
; LFS_LOCKS_PAGING_NUM = 50
; ;
; ; When clients make lfs batch requests, reject them if there are more pointers than this number
; ; zero means 'unlimited'
; LFS_MAX_BATCH_SIZE = 0
; ;
; ; Allow graceful restarts using SIGHUP to fork
; ALLOW_GRACEFUL_RESTARTS = true
; ;
; ; After a restart the parent will finish ongoing requests before
; ; shutting down. Force shutdown if this process takes longer than this delay.
; ; set to a negative value to disable
; GRACEFUL_HAMMER_TIME = 60s
; ;
; ; Allows the setting of a startup timeout and waithint for Windows as SVC service
; ; 0 disables this.
; STARTUP_TIMEOUT = 0
; ;
; ; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 6h
; STATIC_CACHE_TIME = 6h
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[database]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Database to use. Either "mysql", "postgres", "mssql" or "sqlite3".
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; MySQL Configuration
; ;
; DB_TYPE = mysql
; HOST = 127.0.0.1:3306 ; can use socket e.g. /var/run/mysqld/mysqld.sock
; NAME = gitea
; USER = root
; PASSWD = ;Use PASSWD = `your password` for quoting if you use special characters in the password.
; SSL_MODE = false ; either "false" (default), "true", or "skip-verify"
; CHARSET_COLLATION = ; Empty as default, Gitea will try to find a case-sensitive collation. Don't change it unless you clearly know what you need.
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Postgres Configuration
; ;
; DB_TYPE = postgres
; HOST = 127.0.0.1:5432 ; can use socket e.g. /var/run/postgresql/
; NAME = gitea
; USER = root
; PASSWD =
; SCHEMA =
; SSL_MODE=disable ;either "disable" (default), "require", or "verify-full"
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; SQLite Configuration
; ;
DB_TYPE = sqlite3
; defaults to data/gitea.db
PATH = /var/lib/gitea/data/gitea.db
; Query timeout defaults to: 500
SQLITE_TIMEOUT =
; defaults to sqlite database default (often DELETE), can be used to enable WAL mode. https://www.sqlite.org/pragma.html#pragma_journal_mode
SQLITE_JOURNAL_MODE =
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD =
SCHEMA =
SSL_MODE = disable
LOG_SQL = false
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; MSSQL Configuration
; ;
; DB_TYPE = mssql
; HOST = 172.17.0.2:1433
; NAME = gitea
; USER = SA
; PASSWD = MwantsaSecurePassword1
; CHARSET_COLLATION = ; Empty as default, Gitea will try to find a case-sensitive collation. Don't change it unless you clearly know what you need.
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Other settings
; ;
; ; For iterate buffer, default is 50
; ITERATE_BUFFER_SIZE = 50
; ;
; ; Show the database generated SQL
; LOG_SQL = false
; ;
; ; Maximum number of DB Connect retries
; DB_RETRIES = 10
; ;
; ; Backoff time per DB retry (time.Duration)
; DB_RETRY_BACKOFF = 3s
; ;
; ; Max idle database connections on connection pool, default is 2
; MAX_IDLE_CONNS = 2
; ;
; ; Database connection max life time, default is 0 or 3s mysql (See #6804 & #7071 for reasoning)
; CONN_MAX_LIFETIME = 3s
; ;
; ; Database maximum number of open connections, default is 0 meaning no maximum
; MAX_OPEN_CONNS = 0
; ;
; ; Whether execute database models migrations automatically
; AUTO_MIGRATION = true
; ;
; ; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
; ;
; SLOW_QUERY_THRESHOLD = 5s
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[security]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Whether the installer is disabled (set to true to disable the installer)
INSTALL_LOCK = true
; ;
; ; Global secret key that will be used
; ; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
SECRET_KEY =
; ;
; ; Alternative location to specify secret key, instead of this file; you cannot specify both this and SECRET_KEY, and must pick one
; ; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
; SECRET_KEY_URI = file:/etc/gitea/secret_key
; ;
; ; Secret used to validate communication within Gitea binary.
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NzM4MjYzMzJ9.XxUurNQ5drHXpepExUTNEtYS4yLpTdac_fGOeL0eVco
PASSWORD_HASH_ALGO = pbkdf2
; ;
; ; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
; INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
; ;
; ; How long to remember that a user is logged in before requiring relogin (in days)
; LOGIN_REMEMBER_DAYS = 31
; ;
; ; Name of the cookie used to store the current username.
; COOKIE_USERNAME = gitea_awesome
; ;
; ; Name of cookie used to store authentication information.
; COOKIE_REMEMBER_NAME = gitea_incredible
; ;
; ; Reverse proxy authentication header name of user name, email, and full name
; REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
; REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
; REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME
; ;
; ; Interpret X-Forwarded-For header or the X-Real-IP header and set this as the remote IP for the request
; REVERSE_PROXY_LIMIT = 1
; ;
; ; List of IP addresses and networks separated by comma of trusted proxy servers. Use `*` to trust all.
; REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.0/8,::1/128
; ;
; ; The minimum password length for new Users
; MIN_PASSWORD_LENGTH = 8
; ;
; ; Set to true to allow users to import local server paths
; IMPORT_LOCAL_PATHS = false
; ;
; ; Set to false to allow users with git hook privileges to create custom git hooks.
; ; Custom git hooks can be used to perform arbitrary code execution on the host operating system.
; ; This enables the users to access and modify this config file and the Gitea database and interrupt the Gitea service.
; ; By modifying the Gitea database, users can gain Gitea administrator privileges.
; ; It also enables them to access other resources available to the user on the operating system that is running the Gitea instance and perform arbitrary actions in the name of the Gitea OS user.
; ; WARNING: This maybe harmful to you website or your operating system.
; ; WARNING: Setting this to true does not change existing hooks in git repos; adjust it before if necessary.
; DISABLE_GIT_HOOKS = true
; ;
; ; Set to true to disable webhooks feature.
; DISABLE_WEBHOOKS = false
; ;
; ; Set to false to allow pushes to gitea repositories despite having an incomplete environment - NOT RECOMMENDED
; ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET = true
; ;
; ;Comma separated list of character classes required to pass minimum complexity.
; ;If left empty or no valid values are specified, the default is off (no checking)
; ;Classes include "lower,upper,digit,spec"
; PASSWORD_COMPLEXITY = off
; ;
; ; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt"
; PASSWORD_HASH_ALGO = pbkdf2
; ;
; ; Set false to allow JavaScript to read CSRF cookie
; CSRF_COOKIE_HTTP_ONLY = true
; ;
; ; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed
; PASSWORD_CHECK_PWN = false
; ;
; ; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
; ; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
; SUCCESSFUL_TOKENS_CACHE_SIZE = 20
; ;
; ; Reject API tokens sent in URL query string (Accept Header-based API tokens only). This avoids security vulnerabilities
; ; stemming from cached/logged plain-text API tokens.
; ; In future releases, this will become the default behavior
; DISABLE_QUERY_AUTH_TOKEN = false
; ;
; ; On user registration, record the IP address and user agent of the user to help identify potential abuse.
; ; RECORD_USER_SIGNUP_METADATA = false
; ;
; ; Set the two-factor auth behavior.
; ; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
; TWO_FACTOR_AUTH =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[camo]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; At the moment we only support images
; ;
; ; if the camo is enabled
; ENABLED = false
; ; url to a camo image proxy, it **is required** if camo is enabled.
; SERVER_URL =
; ; HMAC to encode urls with, it **is required** if camo is enabled.
; HMAC_KEY =
; ; Set to true to use camo for https too lese only non https urls are proxyed
; ; ALLWAYS is deprecated and will be removed in the future
; ALWAYS = false
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[oauth2]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Enables OAuth2 provider
ENABLED = true
JWT_SECRET = tU0mvpYYFatn8ZJ45ZnuA4RLy6kogrerIblDbihu8oU
; ;
; ; Algorithm used to sign OAuth2 tokens. Valid values: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
; JWT_SIGNING_ALGORITHM = RS256
; ;
; ; Private key file path used to sign OAuth2 tokens. The path is relative to APP_DATA_PATH.
; ; This setting is only needed if JWT_SIGNING_ALGORITHM is set to RS256, RS384, RS512, ES256, ES384 or ES512.
; ; The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you.
; JWT_SIGNING_PRIVATE_KEY_FILE = jwt/private.pem
; ;
; ; OAuth2 authentication secret for access and refresh tokens, change this yourself to a unique string. CLI generate option is helpful in this case. https://docs.gitea.io/en-us/command-line/#generate
; ; This setting is only needed if JWT_SIGNING_ALGORITHM is set to HS256, HS384 or HS512.
; JWT_SECRET =
; ;
; ; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
; JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
; ;
; ; The "issuer" claim identifies the principal that issued the JWT.
; ; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
; ; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
; JWT_CLAIM_ISSUER =
; ;
; ; Lifetime of an OAuth2 access token in seconds
; ACCESS_TOKEN_EXPIRATION_TIME = 3600
; ;
; ; Lifetime of an OAuth2 refresh token in hours
; REFRESH_TOKEN_EXPIRATION_TIME = 730
; ;
; ; Check if refresh token got already used
; INVALIDATE_REFRESH_TOKENS = false
; ;
; ; Maximum length of oauth2 token/cookie stored on server
; MAX_TOKEN_LENGTH = 32767
; ;
; ; Pre-register OAuth2 applications for some universally useful services
; ; * https://github.com/hickford/git-credential-oauth
; ; * https://github.com/git-ecosystem/git-credential-manager
; ; * https://gitea.com/gitea/tea
; DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[log]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; Root path for the log files - defaults to "{AppWorkPath}/log"
; ROOT_PATH =
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; Main Logger
; ;
; ; Either "console", "file" or "conn", default is "console"
; ; Use comma to separate multiple modes, e.g. "console, file"
MODE = console
; ;
; ; Either "Trace", "Debug", "Info", "Warn", "Error" or "None", default is "Info"
LEVEL = info
ROOT_PATH = /var/lib/gitea/log
; ;
; ; Print Stacktrace with logs (rarely helpful, do not set) Either "Trace", "Debug", "Info", "Warn", "Error", default is "None"
; STACKTRACE_LEVEL = None
; ;
; ; Buffer length of the channel, keep it as it is if you don't know what it is.
; BUFFER_LEN = 10000
; ;
; ; Sub logger modes, a single comma means use default MODE above, empty means disable it
; logger.access.MODE=
; logger.router.MODE=,
; logger.xorm.MODE=,
; ;
; ; Collect SSH logs (Creates log from ssh git request)
; ;
; ENABLE_SSH_LOG = false
; ;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Access Logger (Creates log in NCSA common log format)
; ;
; ; Print request id which parsed from request headers in access log, when access log is enabled.
; ; * E.g:
; ; * In request Header: X-Request-ID: test-id-123
; ; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID
; ; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "test-id-123"
; ;
; ; If you configure more than one in the .ini file, it will match in the order of configuration,
; ; and the first match will be finally printed in the log.
; ; * E.g:
; ; * In request Header: X-Trace-ID: trace-id-1q2w3e4r
; ; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID, X-Trace-ID, X-Req-ID
; ; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "trace-id-1q2w3e4r"
; ;
; REQUEST_ID_HEADERS =
; ;
; ; Sets the template used to create the access log.
; ACCESS_LOG_TEMPLATE = {{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; Log modes (aka log writers)
; ;
; [log.%(WriterMode)]
; MODE=console/file/conn/...
; LEVEL=
; FLAGS = stdflags
; EXPRESSION =
; PREFIX =
; COLORIZE = false
; ;
; [log.console]
; STDERR = false
; ;
; [log.file]
; ; Set the file_name for the logger. If this is a relative path this will be relative to ROOT_PATH
; FILE_NAME =
; ; This enables automated log rotate(switch of following options), default is true
; LOG_ROTATE = true
; ; Max size shift of a single file, default is 28 means 1 << 28, 256MB
; MAX_SIZE_SHIFT = 28
; ; Segment log daily, default is true
; DAILY_ROTATE = true
; ; delete the log file after n days, default is 7
; MAX_DAYS = 7
; ; compress logs with gzip
; COMPRESS = true
; ; compression level see godoc for compress/gzip
; COMPRESSION_LEVEL = -1
; ;
; [log.conn]
; ; Reconnect host for every single message, default is false
; RECONNECT_ON_MSG = false
; ; Try to reconnect when connection is lost, default is false
; RECONNECT = false
; ; Either "tcp", "unix" or "udp", default is "tcp"
; PROTOCOL = tcp
; ; Host address
; ADDR =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[git]
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;
; ; The path of git executable. If empty, Gitea searches through the PATH environment.
; PATH =
; ;
; ; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home"
; HOME_PATH =
; ;
; ; Disables highlight of added and removed changes
; DISABLE_DIFF_HIGHLIGHT = false
; ;
; ; Max number of lines allowed in a single file in diff view
; MAX_GIT_DIFF_LINES = 1000
; ;
; ; Max number of allowed characters in a line in diff view
; MAX_GIT_DIFF_LINE_CHARACTERS = 5000
; ;
; ; Max number of files shown in diff view
; MAX_GIT_DIFF_FILES = 100
; ;
; ; Set the default commits range size
; COMMITS_RANGE_SIZE = 50
; ;
; ; Set the default branches range size
; BRANCHES_RANGE_SIZE = 20
; ;
; ; Arguments for command 'git gc', e.g. "--aggressive --auto"
; ; see more on http://git-scm.com/docs/git-gc/
; GC_ARGS =
; ;
; ; If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1
; ; To enable this for Git over SSH when using a OpenSSH server, add `AcceptEnv GIT_PROTOCOL` to your sshd_config file.
; ENABLE_AUTO_GIT_WIRE_PROTOCOL = true
; ;
; ; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
; PULL_REQUEST_PUSH_MESSAGE = true
; ;
; ; (Go-Git only) Don't cache objects greater than this in memory. (Set to 0 to disable.)
; LARGE_OBJECT_THRESHOLD = 1048576
; ; Set to true to forcibly set core.protectNTFS=false
; DISABLE_CORE_PROTECT_NTFS=false
; ; Disable the usage of using partial clones for git.
; DISABLE_PARTIAL_CLONE = false
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; Git Operation timeout in seconds
; [git.timeout]
; DEFAULT = 360
; MIGRATE = 600
; MIRROR = 300
; CLONE = 300
; PULL = 300
; GC = 60
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ; Git config options
; ; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`.
; [git.config]
; diff.algorithm = histogram
; core.logAllRefUpdates = true
; gc.reflogExpire = 90
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
DEFAULT_ENABLE_TIMETRACKING = true
DEFAULT_USER_VISIBILITY = private
DEFAULT_USER_IS_RESTRICTED = true
NO_REPLY_ADDRESS = noreply.localhost
[repository]
ROOT = /var/lib/gitea/data/gitea-repositories
MAX_CREATION_LIMIT = 0
[lfs]
PATH = /var/lib/gitea/data/lfs
[mailer]
ENABLED = false
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
[cron.update_checker]
ENABLED = false
[session]
PROVIDER = file
[repository.pull-request]
DEFAULT_MERGE_STYLE = merge
[repository.signing]
DEFAULT_TRUST_MODEL = committer
[oauth2_client]
ENABLE_AUTO_REGISTRATION = true
ACCOUNT_LINKING = auto
OPENID_CONNECT_SCOPES = profile email

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "home"
def ready(self):
import home.signals

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,6 +18,79 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: home/templates/chat/admin/admin_chat.html:5
msgid "Chat with"
msgstr ""
#: home/templates/chat/admin/admin_chat.html:10
msgid "Admin Chat View"
msgstr ""
#: home/templates/chat/admin/admin_chat.html:11
msgid ""
"This is the admin view of the chat. Here you can manage conversations and "
"monitor user interactions."
msgstr ""
#: home/templates/chat/admin/admin_chat.html:19
#: home/templates/chat/user_chat.html:18
msgid "No messages found."
msgstr ""
#: home/templates/chat/admin/admin_chat.html:24
#: home/templates/chat/user_chat.html:23
msgid "Type your message here..."
msgstr ""
#: home/templates/chat/admin/admin_chat.html:25
#: home/templates/chat/user_chat.html:24
msgid "Send"
msgstr ""
#: home/templates/chat/admin/admin_chat_dashboard.html:5
#: home/templates/chat/user_chat.html:5
msgid "Chat"
msgstr ""
#: home/templates/chat/admin/admin_chat_dashboard.html:10
msgid "Admin Chat Dashboard"
msgstr ""
#: home/templates/chat/admin/admin_chat_dashboard.html:19
msgid "No active chats found."
msgstr ""
#: home/templates/chat/user_chat.html:9
msgid "Chat with Support"
msgstr ""
#: home/templates/chat/user_chat.html:10
msgid ""
"This is the user chat interface. Here you can communicate with our support "
"team for assistance."
msgstr ""
#: home/templates/home/course_index_page.html:4
#: home/templates/home/course_index_page.html:10
msgid "Courses"
msgstr ""
#: home/templates/home/course_index_page.html:12
msgid "Purchased Courses"
msgstr ""
#: home/templates/home/course_index_page.html:22
msgid "Purchased"
msgstr ""
#: home/templates/home/course_index_page.html:32
msgid "Available Courses"
msgstr ""
#: home/templates/home/course_index_page.html:42
msgid "Not Purchased"
msgstr ""
#: home/templates/home/course_module_page.html:21
msgid "Lessons"
msgstr ""
@@ -26,36 +99,40 @@ msgstr ""
msgid "No lessons yet."
msgstr ""
#: home/templates/home/course_page.html:26
msgid "Modules"
#: home/templates/home/course_page.html:31
msgid "Refund Purchase"
msgstr ""
#: home/templates/home/course_page.html:33
msgid "Modules"
msgstr ""
#: home/templates/home/course_page.html:40
msgid "No modules yet."
msgstr ""
#: home/templates/home/course_page.html:39
#: home/templates/home/course_page.html:46
msgid ""
"You need to be logged in to access this course. Please log in or sign up to "
"view the modules."
msgstr ""
#: home/templates/home/course_page.html:40
#: home/templates/home/course_page.html:47
#: home/templates/home/event_page.html:40
msgid "Login"
msgstr ""
#: home/templates/home/course_page.html:41
#: home/templates/home/course_page.html:48
#: home/templates/home/event_page.html:43
msgid "Sign Up"
msgstr ""
#: home/templates/home/course_page.html:46
#: home/templates/home/course_page.html:53
msgid ""
"You don't have access to this course. Please purchase it to view the modules."
msgstr ""
#: home/templates/home/course_page.html:47
#: home/templates/home/course_page.html:54
msgid "Purchase Course"
msgstr ""
@@ -91,46 +168,3 @@ msgstr ""
#: home/templates/home/event_page.html:54
msgid "Sign Up for Event"
msgstr ""
#: home/templates/home/welcome_page.html:6
msgid "Visit the Wagtail website"
msgstr ""
#: home/templates/home/welcome_page.html:15
msgid "View the release notes"
msgstr ""
#: home/templates/home/welcome_page.html:27
msgid "Welcome to your new Wagtail site!"
msgstr ""
#: home/templates/home/welcome_page.html:28
msgid ""
"Please feel free to <a href=\"https://github.com/wagtail/wagtail/wiki/"
"Slack\">join our community on Slack</a>, or get started with one of the "
"links below."
msgstr ""
#: home/templates/home/welcome_page.html:35
msgid "Wagtail Documentation"
msgstr ""
#: home/templates/home/welcome_page.html:36
msgid "Topics, references, & how-tos"
msgstr ""
#: home/templates/home/welcome_page.html:42
msgid "Tutorial"
msgstr ""
#: home/templates/home/welcome_page.html:43
msgid "Build your first Wagtail site"
msgstr ""
#: home/templates/home/welcome_page.html:49
msgid "Admin Interface"
msgstr ""
#: home/templates/home/welcome_page.html:50
msgid "Create your superuser first!"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -20,25 +20,102 @@ msgstr ""
"(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && "
"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#: home/templates/chat/admin/admin_chat.html:5
msgid "Chat with"
msgstr "Czat z"
#: home/templates/chat/admin/admin_chat.html:10
msgid "Admin Chat View"
msgstr "Widok administratora czatu"
#: home/templates/chat/admin/admin_chat.html:11
msgid ""
"This is the admin view of the chat. Here you can manage conversations and "
"monitor user interactions."
msgstr ""
#: home/templates/chat/admin/admin_chat.html:19
#: home/templates/chat/user_chat.html:18
msgid "No messages found."
msgstr "Brak wiadomości."
#: home/templates/chat/admin/admin_chat.html:24
#: home/templates/chat/user_chat.html:23
msgid "Type your message here..."
msgstr "Wiadomość..."
#: home/templates/chat/admin/admin_chat.html:25
#: home/templates/chat/user_chat.html:24
msgid "Send"
msgstr "Wyślij"
#: home/templates/chat/admin/admin_chat_dashboard.html:5
#: home/templates/chat/user_chat.html:5
msgid "Chat"
msgstr "Czat"
#: home/templates/chat/admin/admin_chat_dashboard.html:10
msgid "Admin Chat Dashboard"
msgstr "Panel administratora czatu"
#: home/templates/chat/admin/admin_chat_dashboard.html:19
msgid "No active chats found."
msgstr "Brak aktywnych czatów."
#: home/templates/chat/user_chat.html:9
msgid "Chat with Support"
msgstr "Czat z administracją"
#: home/templates/chat/user_chat.html:10
msgid ""
"This is the user chat interface. Here you can communicate with our support "
"team for assistance."
msgstr ""
"To jest interfejs czatu dla użytkowników. Tutaj możesz komunikować się z "
"naszym zespołem wsparcia w celu uzyskania pomocy."
#: home/templates/home/course_index_page.html:4
#: home/templates/home/course_index_page.html:10
msgid "Courses"
msgstr "Kursy"
#: home/templates/home/course_index_page.html:12
msgid "Purchased Courses"
msgstr "Zakupione kursy"
#: home/templates/home/course_index_page.html:22
msgid "Purchased"
msgstr "Zakupiony"
#: home/templates/home/course_index_page.html:32
msgid "Available Courses"
msgstr "Dostępne kursy"
#: home/templates/home/course_index_page.html:42
msgid "Not Purchased"
msgstr "Niezakupiony"
#: home/templates/home/course_module_page.html:21
msgid "Lessons"
msgstr "Lekcje"
#: home/templates/home/course_module_page.html:28
#, fuzzy
#| msgid "No modules yet."
msgid "No lessons yet."
msgstr "Brak lekcji."
#: home/templates/home/course_page.html:26
#: home/templates/home/course_page.html:31
msgid "Refund Purchase"
msgstr "Zwróć zakup"
#: home/templates/home/course_page.html:33
msgid "Modules"
msgstr "Moduły"
#: home/templates/home/course_page.html:33
#: home/templates/home/course_page.html:40
msgid "No modules yet."
msgstr "Brak modułów."
#: home/templates/home/course_page.html:39
#: home/templates/home/course_page.html:46
msgid ""
"You need to be logged in to access this course. Please log in or sign up to "
"view the modules."
@@ -46,22 +123,22 @@ msgstr ""
"Musisz być zalogowany, aby uzyskać dostęp do tego kursu. Zaloguj się lub "
"zarejestruj, aby zobaczyć moduły."
#: home/templates/home/course_page.html:40
#: home/templates/home/course_page.html:47
#: home/templates/home/event_page.html:40
msgid "Login"
msgstr "Zaloguj się"
#: home/templates/home/course_page.html:41
#: home/templates/home/course_page.html:48
#: home/templates/home/event_page.html:43
msgid "Sign Up"
msgstr "Zarejestruj się"
#: home/templates/home/course_page.html:46
#: home/templates/home/course_page.html:53
msgid ""
"You don't have access to this course. Please purchase it to view the modules."
msgstr "Nie masz dostępu do tego kursu. Zakup go, aby zobaczyć moduły."
#: home/templates/home/course_page.html:47
#: home/templates/home/course_page.html:54
msgid "Purchase Course"
msgstr "Kup kurs"
@@ -105,46 +182,3 @@ msgstr ""
#: home/templates/home/event_page.html:54
msgid "Sign Up for Event"
msgstr "Zapisz się"
#: home/templates/home/welcome_page.html:6
msgid "Visit the Wagtail website"
msgstr ""
#: home/templates/home/welcome_page.html:15
msgid "View the release notes"
msgstr ""
#: home/templates/home/welcome_page.html:27
msgid "Welcome to your new Wagtail site!"
msgstr ""
#: home/templates/home/welcome_page.html:28
msgid ""
"Please feel free to <a href=\"https://github.com/wagtail/wagtail/wiki/"
"Slack\">join our community on Slack</a>, or get started with one of the "
"links below."
msgstr ""
#: home/templates/home/welcome_page.html:35
msgid "Wagtail Documentation"
msgstr ""
#: home/templates/home/welcome_page.html:36
msgid "Topics, references, & how-tos"
msgstr ""
#: home/templates/home/welcome_page.html:42
msgid "Tutorial"
msgstr ""
#: home/templates/home/welcome_page.html:43
msgid "Build your first Wagtail site"
msgstr ""
#: home/templates/home/welcome_page.html:49
msgid "Admin Interface"
msgstr ""
#: home/templates/home/welcome_page.html:50
msgid "Create your superuser first!"
msgstr ""

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0.3 on 2026-03-19 14:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0017_chatmessage'),
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
]
operations = [
migrations.CreateModel(
name='CourseIndexPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-19 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0018_courseindexpage'),
]
operations = [
migrations.AddField(
model_name='coursepage',
name='description',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0.3 on 2026-03-23 11:59
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('home', '0019_coursepage_description'),
]
operations = [
migrations.AddField(
model_name='coursepage',
name='repository_url',
field=models.URLField(blank=True, null=True),
),
migrations.AlterField(
model_name='coursepage',
name='allowed_groups',
field=modelcluster.fields.ParentalManyToManyField(help_text="Additional groups that should have access to this course, e.g. Editors. NOTE: Users who purchase the course will be automatically added to a dedicated access group for this course, so you don't need to add that group here.", related_name='course_pages', to='auth.group'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 6.0.3 on 2026-03-30 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0020_coursepage_repository_url_and_more'),
]
operations = [
migrations.AddField(
model_name='modulelessonpage',
name='create_gitea_repo',
field=models.BooleanField(default=False, help_text='If enabled, a Gitea repository will be automatically created for this module when the module is published.'),
),
migrations.AddField(
model_name='modulelessonpage',
name='gitea_repo_url',
field=models.URLField(blank=True, help_text="URL of the Gitea repository for this lesson (auto-generated if 'create_gitea_repo' is enabled)", null=True),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 6.0.3 on 2026-03-30 07:53
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0021_modulelessonpage_create_gitea_repo_and_more'),
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.CreateModel(
name='BlogIndexPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='BlogPage',
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')),
('author', models.CharField(max_length=255)),
('body', wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2)], block_lookup={0: ('wagtail.blocks.CharBlock', (), {'form_classname': 'title'}), 1: ('wagtail.blocks.RichTextBlock', (), {}), 2: ('wagtail.images.blocks.ImageBlock', [], {})})),
('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0.3 on 2026-04-22 17:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0022_blogindexpage_blogpage'),
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
]
operations = [
migrations.CreateModel(
name='EventIndexPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
]

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.db import models
from django.forms import CheckboxSelectMultiple
@@ -7,13 +8,17 @@ from django.utils import timezone
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField
from wagtail.fields import RichTextField, StreamField
from wagtail.images.blocks import ImageBlock
from wagtail.models import Page
from wagtail.models.copying import ParentalManyToManyField
from wagtail_color_panel.edit_handlers import NativeColorPanel
from wagtail_color_panel.fields import ColorField
from purchase.models import CoursePurchase
class EmptyPage(Page):
pass
@@ -25,8 +30,67 @@ class HomePage(Page):
content_panels = Page.content_panels + ["body"]
class BlogIndexPage(Page):
subpage_types = ["home.BlogPage"]
def get_context(self, request):
context = super().get_context(request)
context["posts"] = self.get_children().live().order_by("-first_published_at")
return context
class CourseIndexPage(Page):
subpage_types = ["home.CoursePage"]
def get_context(self, request):
context = super().get_context(request)
all_courses = self.get_children().live()
purchased_courses = set()
other_courses = set()
for course in all_courses:
if course.specific._user_has_access(request.user):
purchased_courses.add(course)
else:
other_courses.add(course)
context["purchased_courses"] = sorted(
purchased_courses, key=lambda c: c.title.lower()
)
context["other_courses"] = sorted(other_courses, key=lambda c: c.title.lower())
return context
class EventIndexPage(Page):
subpage_types = ["home.EventPage"]
class BlogPage(Page):
author = models.CharField(max_length=255)
image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
body = StreamField(
[
("heading", blocks.CharBlock(classname="title")),
("paragraph", blocks.RichTextBlock()),
("image", ImageBlock()),
]
)
content_panels = Page.content_panels + [
FieldPanel("author"),
FieldPanel("image"),
FieldPanel("body"),
]
parent_page_types = ["home.BlogIndexPage"]
class CoursePage(Page):
body = RichTextField(blank=True)
course_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
@@ -34,28 +98,84 @@ class CoursePage(Page):
on_delete=models.SET_NULL,
related_name="+",
)
description = models.CharField(max_length=255, blank=True)
body = RichTextField(blank=True)
allowed_groups = ParentalManyToManyField(
Group,
related_name="course_pages",
help_text="Select a group to restrict access to this course. Non-members will be prompted to purchase the course to view modules.",
help_text="Additional groups that should have access to this course, e.g. Editors. NOTE: Users who purchase the course will be automatically added to a dedicated access group for this course, so you don't need to add that group here.",
)
repository_url = models.URLField(
null=True,
blank=True,
)
def _user_has_access(self, user):
if not user.is_authenticated:
return False
user_group_ids = user.groups.values_list("id", flat=True)
return self.allowed_groups.filter(id__in=user_group_ids).exists() # pyright: ignore[reportAttributeAccessIssue]
if self.allowed_groups.filter(id__in=user_group_ids).exists():
return True
return CoursePurchase.objects.filter(
user=user, course=self, refunded=False
).exists()
def _user_purchase_id(self, user):
if not user.is_authenticated:
return None
purchase = CoursePurchase.objects.filter(
user=user, course=self, refunded=False
).first()
print(f"User {user} purchase for course {self}: {purchase}")
return purchase.id if purchase else None
def mock_purchase(self, user):
"""Mock method to simulate purchasing this course for a user."""
if not user.is_authenticated:
return False
obj, created = CoursePurchase.objects.get_or_create(
user=user, course=self, refunded=False
)
# Add user to dedicated access group for this course
group_name = f"course_{self.id}_access"
group, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(group)
# Ensure allowed_groups only includes this access group
if not self.allowed_groups.filter(id=group.id).exists():
self.allowed_groups.add(group)
return created
def save(self, *args, **kwargs):
if self.id is not None:
group_name = f"course_{self.id}_access"
group, created = Group.objects.get_or_create(name=group_name)
if not self.allowed_groups.filter(id=group.id).exists():
self.allowed_groups.add(group)
super().save(*args, **kwargs)
def get_context(self, request):
context = super().get_context(request)
context["user_has_access"] = self._user_has_access(request.user)
context["user_purchase_id"] = self._user_purchase_id(request.user)
return context
content_panels = Page.content_panels + [
FieldPanel("course_image"),
FieldPanel("description"),
FieldPanel("body"),
FieldPanel("allowed_groups", widget=CheckboxSelectMultiple),
FieldPanel(
"repository_url",
read_only=True,
heading="Repository URL (auto-generated)",
),
]
parent_page_types = ["home.CourseIndexPage"]
subpage_types = ["home.CourseModulePage"]
@@ -84,6 +204,23 @@ class CourseModulePage(Page):
class ModuleLessonPage(Page):
body = RichTextField(blank=True)
create_gitea_repo = models.BooleanField(
default=False,
help_text="If enabled, a Gitea repository will be automatically created for this module when the module is published.",
)
gitea_repo_url = models.URLField(
null=True,
blank=True,
help_text="URL of the Gitea repository for this lesson (auto-generated if 'create_gitea_repo' is enabled)",
)
@property
def gitea_login_redirect_url(self):
gitea_root_url = getattr(settings, "GITEA_ROOT_URL")
if self.gitea_repo_url and gitea_root_url:
uri = str(self.gitea_repo_url).replace(gitea_root_url, "")
return f"{gitea_root_url}/user/login?redirect_to={uri}"
return None
@property
def module(self):
@@ -100,7 +237,15 @@ class ModuleLessonPage(Page):
return f"{module.full_title} - {self.title}"
return self.title
content_panels = Page.content_panels + ["body"]
content_panels = Page.content_panels + [
FieldPanel("body"),
FieldPanel("create_gitea_repo"),
FieldPanel(
"gitea_repo_url",
read_only=True,
heading="Gitea Repository URL",
),
]
parent_page_types = ["home.CourseModulePage"]
@@ -181,6 +326,8 @@ class EventPage(Page):
help_text="Select users who will be listed as hosts of this event.",
)
parent_page_types = ["home.EventIndexPage"]
def get_context(self, request):
context = super().get_context(request)
# Occurrence-specific context should be handled in views/templates
@@ -191,15 +338,26 @@ class EventPage(Page):
Generate EventOccurrence objects for this event based on recurrence settings.
For endless recurrence, generate up to days_ahead into the future.
"""
from .event_occurrence import EventOccurrence
now = timezone.now()
if not self.live:
# if not live, no future occurrences should be generated
self.occurrences.filter(start__gt=now).delete()
return
if not self.recurrence_enabled:
# if recurrence is not enabled, ensure there's at least one occurrence for the specified start/end
# and delete any other occurrences that don't match the current start/end
if self.occurrences.exists():
occurrence = self.occurrences.first()
if occurrence.start != self.start or occurrence.end != self.end:
occurrence.start = self.start
occurrence.end = self.end
occurrence.save(update_fields=["start", "end"])
self.occurrences.exclude(id=occurrence.id).delete()
else:
EventOccurrence.objects.create(
event=self, start=self.start, end=self.end

214
home/signals.py Normal file
View File

@@ -0,0 +1,214 @@
import logging as lg
import os
import requests
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from home.models.pages import CoursePage, ModuleLessonPage
GITEA_ORG_NAME = "Studio77"
logger = lg.getLogger(__name__)
@receiver(post_save, sender=CoursePage)
def create_gitea_team_on_course_creation(sender, instance, created, **kwargs):
if not instance.live:
logger.debug(
f"Course {instance.title} is not live, skipping Gitea team creation"
)
return
course = instance
team_name = f"course-{course.id}"
api_url = getattr(settings, "GITEA_URL", None)
if not api_url:
logger.debug("GITEA_URL is not set, skipping Gitea team creation")
return
# check if team already exists
try:
response = requests.get(
f"{api_url}/orgs/{GITEA_ORG_NAME}/teams",
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
teams = response.json()
if any(team["name"] == team_name for team in teams):
logger.info(f"Gitea team {team_name} already exists, skipping creation")
return
except Exception as e:
logger.exception(
f"Failed to check existing Gitea teams: {e}\n{response.text}",
e,
)
return
url = f"{api_url}/orgs/{GITEA_ORG_NAME}/teams"
payload = {
"can_create_org_repo": False,
"description": f"Team for course {course.title}",
"includes_all_repositories": False,
"name": team_name,
"permission": "read",
"units": [
# "repo.actions",
"repo.code",
# "repo.issues",
# "repo.ext_issues",
# "repo.wiki",
# "repo.ext_wiki",
# "repo.pulls",
# "repo.releases",
# "repo.projects",
# "repo.ext_wiki",
],
}
try:
response = requests.post(
url,
json=payload,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
logger.info(
f"Successfully created Gitea team `{team_name}` for course {course.title}"
)
except Exception as e:
logger.exception(
f"Failed to create Gitea team for course {course.title}: {e}\n{response.text}",
e,
)
@receiver(post_save, sender=ModuleLessonPage)
def create_gitea_repo_on_lesson_creation(
sender, instance: ModuleLessonPage, created, **kwargs
):
if not instance.live:
logger.debug(
f"Lesson {instance.title} is not live, skipping Gitea repository creation"
)
return
course = instance.module.course
repo_name = f"course-{course.id}-lesson-{instance.id}"
if not instance.create_gitea_repo:
logger.debug(
f"Lesson {instance.title} is not marked for Gitea repository creation, skipping"
)
return
if not course.live:
logger.debug(
f"Course {course.title} is not live, skipping Gitea repository creation for lesson {instance.title}"
)
return
api_url = getattr(settings, "GITEA_URL", None)
if not api_url:
logger.debug("GITEA_URL is not set, skipping Gitea repository creation")
return
def create_repository() -> None:
# check if repository already exists
try:
response = requests.get(
f"{api_url}/orgs/{GITEA_ORG_NAME}/repos",
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
repos = response.json()
if any(repo["name"] == repo_name for repo in repos):
logger.info(
f"Gitea repository {repo_name} already exists, skipping creation"
)
return
except Exception as e:
logger.exception(
f"Failed to check existing Gitea repositories: {e}\n{response.text}",
e,
)
return
# create lesson repository
url = f"{api_url}/orgs/{GITEA_ORG_NAME}/repos"
payload = {
"auto_init": True,
"default_branch": "main",
"description": f"{instance.module.course} {instance.module}: {instance.title}",
"name": repo_name,
"private": True,
}
try:
response = requests.post(
url,
json=payload,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
repo_url = response.json().get("html_url")
instance.gitea_repo_url = repo_url
instance.save(update_fields=["gitea_repo_url"])
logger.info(
f"Successfully created Gitea repository for lesson {instance.title}: {repo_url}"
)
except Exception as e:
logger.exception(
f"Failed to create Gitea repository for lesson {instance.title}: {e}\n{response.text}",
e,
)
def add_repository_to_team() -> None:
# add repository to course team
team_name = f"course-{course.id}"
try:
response = requests.get(
f"{api_url}/orgs/{GITEA_ORG_NAME}/teams",
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
teams = response.json()
team = next((team for team in teams if team["name"] == team_name), None)
if not team:
logger.error(
f"Gitea team {team_name} not found when trying to add repository {repo_name}"
)
return
team_id = team["id"]
add_repo_url = (
f"{api_url}/teams/{team_id}/repos/{GITEA_ORG_NAME}/{repo_name}"
)
add_response = requests.put(
add_repo_url,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
add_response.raise_for_status()
logger.info(
f"Successfully added repository {repo_name} to Gitea team {team_name}"
)
except Exception as e:
logger.exception(
f"Failed to add repository {repo_name} to Gitea team {team_name}: {e}\n{response.text}",
e,
)
create_repository()
add_repository_to_team()

View File

@@ -5,25 +5,88 @@
{% trans "Chat with" %} {{ chat_user.email }}
{% endblock titletag %}
{% block extra_css %}
<style>
.admin-chat-messages {
list-style: none;
padding: 0;
margin: 1.5em 0;
max-height: 350px;
overflow-y: auto;
border-radius: 6px;
border: 1px solid #e2e2e2;
}
.admin-chat-messages li {
padding: 0.7em 1em;
border-bottom: 1px solid #ececec;
display: flex;
align-items: baseline;
font-size: 1.05em;
}
.admin-chat-messages li:last-child {
border-bottom: none;
}
.admin-chat-message-meta {
color: #888;
font-size: 0.92em;
margin-right: 0.5em;
white-space: nowrap;
}
.admin-chat-form {
display: flex;
gap: 0.5em;
margin-top: 1em;
}
.admin-chat-form input[type="text"] {
flex: 1;
padding: 0.5em;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 1em;
}
.admin-chat-form button {
padding: 0.5em 1.2em;
border-radius: 4px;
border: none;
background: #0073e6;
color: #fff;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.admin-chat-form button:hover {
background: #005bb5;
}
.admin-message-admin {
background: #334;
}
.admin-message-user {
}
</style>
{% endblock extra_css %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Chat" icon="mail" %}
<h1>{% trans "Admin Chat View" %}</h1>
<p>{% trans "This is the admin view of the chat. Here you can manage conversations and monitor user interactions." %}</p>
<div style="padding: 0 3em;">
<a href="{% url 'user_chat' %}" class="button button-secondary">&larr; {% trans "Back" %}</a>
<h1>{% trans "Chat with" %} <em>{{ chat_user.email }}</em></h1>
<ul>
{% for message in chat_messages %}
<li>
<strong>{{ message.sender.email }}:</strong> {{ message.content }}
</li>
{% empty %}
<li>{% trans "No messages found." %}</li>
{% endfor %}
</ul>
<form action="/chat/send/{{ chat_user.id }}/" method="post">
{% csrf_token %}
<input type="text" name="content" placeholder="{% trans "Type your message here..." %}" required>
<button type="submit">{% trans "Send" %}</button>
</form>
<ul class="admin-chat-messages">
{% for message in chat_messages %}
<li class="{% if message.sender.id == chat_user.id %}admin-message-user{% else %}admin-message-admin{% endif %}">
<span class="admin-chat-message-meta">{{ message.timestamp|date:"Y-m-d H:i" }}</span>
<strong>{{ message.sender.email }}:&nbsp;</strong> {{ message.content }}
</li>
{% empty %}
<li>{% trans "No messages found." %}</li>
{% endfor %}
</ul>
<form action="/chat/send/{{ chat_user.id }}/" method="post" class="admin-chat-form">
{% csrf_token %}
<input type="text" name="content" placeholder="{% trans "Type your message here..." %}" required>
<button type="submit">{% trans "Send" %}</button>
</form>
</div>
{% endblock content %}

View File

@@ -6,21 +6,36 @@
{% endblock title %}
{% block content %}
<h1>{% trans "Chat with Support" %}</h1>
<p>{% trans "This is the user chat interface. Here you can communicate with our support team for assistance." %}</p>
<h1 class="text-2xl font-bold mb-4 text-gray-800">{% trans "Chat with Support" %}</h1>
<p class="mb-6 text-gray-600">{% trans "Welcome! We're here to help. Type your message below to start a conversation with our support team." %}</p>
<ul>
{% for message in chat_messages %}
<li>
<strong>{{ message.sender.email }}:</strong> {{ message.content }}
<ul class="flex flex-col gap-2 mb-6">
{% regroup chat_messages by timestamp.date as dated_messages %}
{% for day in dated_messages %}
<li class="flex justify-center my-2">
<span class="px-4 py-1 bg-gray-100 text-gray-500 text-xs rounded-full shadow">{{ day.grouper|date:'l, d M Y' }}</span>
</li>
{% for message in day.list %}
<li class="flex flex-col items-start {% if message.sender.id == user.id %}items-end{% endif %}">
<div class="max-w-xs px-4 py-2 rounded-2xl shadow text-sm
{% if message.sender.id == user.id %}
bg-blue-600 text-white rounded-br-none self-end
{% else %}
bg-gray-200 text-gray-900 rounded-bl-none self-start
{% endif %}">
<span class="block font-semibold text-xs mb-1 {% if message.sender.id == user.id %}text-blue-100{% else %}text-blue-700{% endif %}">{{ message.sender.email }}</span>
<span>{{ message.content }}</span>
<span class="block text-xs {% if message.sender.id == user.id %}text-blue-100{% else %}text-blue-700{% endif %} mt-1 text-right">{{ message.timestamp|date:'H:i' }}</span>
</div>
</li>
{% endfor %}
{% empty %}
<li>{% trans "No messages found." %}</li>
<li class="text-gray-400">{% trans "No messages found." %}</li>
{% endfor %}
</ul>
<form action="/chat/send/{{ user.id }}/" method="post">
<form action="/chat/send/{{ user.id }}/" method="post" class="flex gap-4">
{% csrf_token %}
<textarea name="content" placeholder="{% trans "Type your message here..." %}"></textarea>
<button type="submit">{% trans "Send" %}</button>
<textarea rows=1 name="content" placeholder="{% trans "Type your message here..." %}" class="grow resize-y min-h-10 border border-gray-300 rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-gray-800"></textarea>
<button type="submit" class="self-center px-5 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">{% trans "Send" %}</button>
</form>
{% endblock content %}

View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load static i18n wagtailcore_tags wagtailimages_tags %}
{% block title %}{% trans "Blog" %}{% endblock title %}
{% block body_class %}template-blogindex{% endblock body_class %}
{% block content %}
<h1 class="text-3xl font-bold mb-6 text-gray-800 text-center">
{% trans "Blog" %}
</h1>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{% for post in posts %}
<a href="{{ post.url }}" class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
{% image post.specific.image original %}
<div class="p-4">
<div class="flex justify-between items-center mb-2 text-gray-500">
<div class="flex items-center gap-1">
<i class="fi fi-br-circle-user leading-0"></i>
<span class="text-sm">{{ post.specific.author }}</span>
</div>
<div class="flex items-center gap-1">
<i class="fi fi-br-calendar leading-0"></i>
<span class="text-sm">{{ post.first_published_at|date:"F j, Y | H:i" }}</span>
</div>
</div>
<h2 class="text-xl font-semibold mb-2">{{ post.specific.title }}</h2>
<p class="text-gray-600">{{ post.specific.description|truncatewords:20 }}</p>
</div>
</a>
{% empty %}
<p class="text-gray-600">{% trans "No blog posts available." %}</p>
{% endfor %}
</div>
{% endblock content %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load static i18n wagtailcore_tags wagtailimages_tags %}
{% block title %}{{ page.specific.title }} | {% trans "Blog" %}{% endblock title %}
{% block body_class %}template-blog{% endblock body_class %}
{% block content %}
<div class="max-w-3xl mx-auto py-12 px-4">
<a href="{{ page.get_parent.url }}" class="inline-flex items-center gap-1 text-gray-500 text-4xl mb-4 hover:text-gray-700 transition-colors duration-300">
<i class="fi fi-br-arrow-left leading-0"></i>
</a>
<h1 class="text-4xl font-bold mb-6 text-gray-800">
{{ page.specific.title }}
</h1>
<div class="flex items-center gap-4 mb-8 text-gray-500">
<div class="flex items-center gap-1">
<i class="fi fi-br-circle-user leading-0"></i>
<span class="text-sm">{{ page.specific.author }}</span>
</div>
<div class="flex items-center gap-1">
<i class="fi fi-br-calendar leading-0"></i>
<span class="text-sm">{{ page.first_published_at|date:"F j, Y | H:i" }}</span>
</div>
</div>
{% if page.specific.image %}
<div class="mb-8">
{% image page.specific.image original class="w-full max-h-100 object-contain rounded-lg" %}
</div>
{% endif %}
<article class="prose max-w-none">
{% for block in page.body %}
{% if block.block_type == 'heading' %}
<h2>
{{ block.value }}
</h2>
{% else %}
<section class="block-{{ block.block_type }}">
{% include_block block %}
</section>
{% endif %}
{% endfor %}
</article>
</div>
{% endblock content %}

View File

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load static i18n wagtailcore_tags wagtailimages_tags %}
{% block title %}{% trans "Courses" %}{% endblock title %}
{% block body_class %}template-courseindex{% endblock body_class %}
{% block content %}
<h1 class="text-3xl font-bold mb-6 text-center">{% trans "Courses" %}</h1>
<h2 class="text-2xl font-semibold mb-4">{% trans "Purchased Courses" %}</h2>
<div class="flex flex-wrap -mx-4">
{% for course in purchased_courses %}
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
<a href="{{ course.url }}" class="block bg-green-50 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
{% image course.specific.course_image original %}
<div class="p-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-xl font-semibold">{{ course.specific.title }}</h2>
<div class="relative w-8 h-8 rounded-full bg-green-500">
<i class="fi fi-br-lock-open-alt leading-0 absolute left-0 top-1/2 translate-x-1/2 -translate-y-1/2 text-white" title="{% trans "Purchased" %}"></i>
</div>
</div>
<p class="text-gray-600">{{ course.specific.description|truncatewords:20 }}</p>
</div>
</a>
</div>
{% endfor %}
</div>
<h2 class="text-2xl font-semibold mb-4">{% trans "Available Courses" %}</h2>
<div class="flex flex-wrap -mx-4">
{% for course in other_courses %}
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
<a href="{{ course.url }}" class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
{% image course.specific.course_image original %}
<div class="p-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-xl font-semibold">{{ course.specific.title }}</h2>
<div class="relative w-8 h-8 rounded-full bg-gray-200">
<i class="fi fi-br-shopping-basket leading-0 absolute left-0 top-1/2 translate-x-1/2 -translate-y-1/2 text-gray-700" title="{% trans "Not Purchased" %}"></i>
</div>
</div>
<p class="text-gray-600">{{ course.specific.description|truncatewords:20 }}</p>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock content %}

View File

@@ -13,7 +13,9 @@
{% block content %}
<h2 class="not-prose text-xl mb-4 text-gray-700">
<a href="{{ page.course.url }}" class="font-bold">{{ page.course.title }}</a> &raquo; {{ page.title }}
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
&raquo; <a href="{{ page.course.url }}" class="font-bold hover:underline">{{ page.course.title }}</a>
&raquo; {{ page.title }}
</h2>
{{ page.body|richtext }}

View File

@@ -12,17 +12,25 @@
{% block content_class %}prose{% endblock content_class %}
{% block content %}
<h1 class="not-prose text-3xl mb-4 text-gray-700 font-bold">
{{ page.title }}
<h1 class="not-prose text-3xl mb-4 text-gray-700">
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
&raquo; {{ page.title }}
</h1>
{% if page.course_image %}
{% image page.course_image original alt=page.title class="w-full h-auto rounded-lg mb-6" %}
{% image page.course_image original alt=page.title class="w-full h-auto rounded-lg mb-4" %}
{% endif %}
<p class="not-prose text-gray-600 mb-6 text-lg">
{{ page.description }}
</p>
{{ page.body|richtext }}
{% if user_has_access %}
{% if user_purchase_id %}
<a href="{% url 'mock_refund_purchase' purchase_id=user_purchase_id %}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Refund Purchase" %}</a>
{% endif %}
<h2 class="not-prose text-2xl mt-8 mb-4 text-gray-700 font-semibold">{% trans "Modules" %}</h2>
<ul class="list-disc list-inside">
{% for module in page.get_children.specific.live %}
@@ -44,7 +52,7 @@
{% else %}
<div class="not-prose mt-8 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
<p>{% trans "You don't have access to this course. Please purchase it to view the modules." %}</p>
<a href="" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Purchase Course" %}</a>
<a href="{% url 'mock_purchase_course' course_id=page.id %}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Purchase Course" %}</a>
</div>
{% endif %}
{% endblock content %}

View File

@@ -13,13 +13,21 @@
{% block content %}
<h2 class="not-prose text-xl mb-4 text-gray-700">
<a href="{{ page.module.course.url }}" class="font-bold">{{ page.module.course.title }}</a>
&raquo;
<a href="{{ page.module.url }}" class="font-bold">{{ page.module.title }}</a>
&raquo;
<span class="text-gray-500">{{ page.title }}</span>
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
&raquo; <a href="{{ page.module.course.url }}" class="font-bold hover:underline">{{ page.module.course.title }}</a>
&raquo; <a href="{{ page.module.url }}" class="font-bold hover:underline">{{ page.module.title }}</a>
&raquo; <span class="text-gray-500">{{ page.title }}</span>
</h2>
{% if page.create_gitea_repo and page.gitea_repo_url %}
<p class="not-prose mb-6 text-gray-600 text-lg flex items-center gap-1">
<i class="fi fi-br-code-window leading-0"></i>
<a href="{{ page.gitea_login_redirect_url }}" class="hover:underline" target="_blank" rel="noopener noreferrer">
{% trans "View code on Gitea" %}
</a>
</p>
{% endif %}
{{ page.body|richtext }}
{% endblock content %}

View File

@@ -1,52 +0,0 @@
{% load i18n wagtailcore_tags %}
<header class="header">
<div class="logo">
<a href="https://wagtail.org/">
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
</a>
</div>
<div class="header-link">
{% comment %}
This works for all cases but prerelease versions:
{% endcomment %}
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
{% trans "View the release notes" %}
</a>
</div>
</header>
<main class="main">
<div class="figure">
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
</svg>
</div>
<div class="main-text">
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
</div>
</main>
<footer class="footer" role="contentinfo">
<a class="option option-one" href="{% wagtail_documentation_path %}/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
<div>
<h2>{% trans "Wagtail Documentation" %}</h2>
<p>{% trans "Topics, references, & how-tos" %}</p>
</div>
</a>
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<div>
<h2>{% trans "Tutorial" %}</h2>
<p>{% trans "Build your first Wagtail site" %}</p>
</div>
</a>
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
<div>
<h2>{% trans "Admin Interface" %}</h2>
<p>{% trans "Create your superuser first!" %}</p>
</div>
</a>
</footer>

View File

@@ -14,7 +14,6 @@ def admin_chat_dashboard(request):
@login_required
def admin_chat(request, user_id):
chat_user = User.objects.filter(id=user_id, is_staff=False).first()
print(chat_user)
chat_messages = ChatMessage.get_support_chat(chat_user)
return render(
request,

View File

@@ -1,13 +1,63 @@
import logging as lg
import os
import requests
from django import forms
from django.conf import settings
from django.core.handlers.wsgi import WSGIRequest
logger = lg.getLogger(__name__)
def create_gitea_account(user, password):
payload = {
"user_id": user.id,
"username": f"studio77-{user.id}",
"email": user.email,
"full_name": f"{user.first_name} {user.last_name}".strip(),
"password": password,
"must_change_password": False,
"visibility": "private",
}
api_url = getattr(settings, "GITEA_URL", None)
if api_url:
url = f"{api_url}/admin/users"
try:
response = requests.post(
url,
json=payload,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
logger.info(f"Successfully created Gitea account for {user.email}")
except requests.exceptions.HTTPError as http_err:
status_code = http_err.response.status_code
if status_code == 422:
logger.warning(
f"Gitea account for user {user.email} already exists. Skipping creation."
)
else:
logger.error(
f"HTTP error occurred while creating Gitea account for user {user.email}: {http_err}\n{response.text}"
)
raise http_err
except Exception as e:
logger.error(
f"Failed to create Gitea account for user {user.email}: {e}\n{response.text}"
)
raise e
class SignUpForm(forms.Form):
first_name = forms.CharField(max_length=60, required=True, label="First Name")
last_name = forms.CharField(max_length=60, required=True, label="Last Name")
def signup(self, request, user):
user.first_name = self.cleaned_data["first_name"]
user.last_name = self.cleaned_data["last_name"]
def signup(self, request: WSGIRequest, user):
user.first_name = self.cleaned_data["first_name"].strip().title()
user.last_name = self.cleaned_data["last_name"].strip().title()
user.save()
return user
# gitea account creation
password = request.POST.get("password1")
create_gitea_account(user, password)

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -26,7 +26,7 @@ msgstr ""
msgid "Sorry, you don't have permission to access this page."
msgstr ""
#: kursy/templates/calendar.html:4 kursy/templates/header.html:8
#: kursy/templates/calendar.html:4
msgid "Course Calendar"
msgstr ""
@@ -34,19 +34,27 @@ msgstr ""
msgid "Loading..."
msgstr ""
#: kursy/templates/header.html:10
#: kursy/templates/header.html:7
msgid "Courses"
msgstr ""
#: kursy/templates/header.html:8
msgid "Calendar"
msgstr ""
#: kursy/templates/header.html:13
msgid "Logout"
msgstr ""
#: kursy/templates/header.html:12 kursy/templates/occurrence_detail.html:39
#: kursy/templates/header.html:15 kursy/templates/occurrence_detail.html:39
msgid "Login"
msgstr ""
#: kursy/templates/header.html:13 kursy/templates/occurrence_detail.html:42
#: kursy/templates/header.html:16 kursy/templates/occurrence_detail.html:42
msgid "Sign Up"
msgstr ""
#: kursy/templates/header.html:32
#: kursy/templates/header.html:35
msgid "Search courses..."
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -28,7 +28,7 @@ msgstr "Odmowa dostępu"
msgid "Sorry, you don't have permission to access this page."
msgstr "Przepraszamy, ale nie masz uprawnień do dostępu do tej strony."
#: kursy/templates/calendar.html:4 kursy/templates/header.html:8
#: kursy/templates/calendar.html:4
msgid "Course Calendar"
msgstr "Kalendarz kursów"
@@ -36,19 +36,27 @@ msgstr "Kalendarz kursów"
msgid "Loading..."
msgstr "Ładowanie..."
#: kursy/templates/header.html:10
#: kursy/templates/header.html:7
msgid "Courses"
msgstr "Kursy"
#: kursy/templates/header.html:8
msgid "Calendar"
msgstr "Kalendarz"
#: kursy/templates/header.html:13
msgid "Logout"
msgstr "Wyloguj się"
#: kursy/templates/header.html:12 kursy/templates/occurrence_detail.html:39
#: kursy/templates/header.html:15 kursy/templates/occurrence_detail.html:39
msgid "Login"
msgstr "Zaloguj się"
#: kursy/templates/header.html:13 kursy/templates/occurrence_detail.html:42
#: kursy/templates/header.html:16 kursy/templates/occurrence_detail.html:42
msgid "Sign Up"
msgstr "Zarejestruj się"
#: kursy/templates/header.html:32
#: kursy/templates/header.html:35
msgid "Search courses..."
msgstr "Szukaj kursów..."
@@ -62,8 +70,6 @@ msgid "You are signed up for this event. We look forward to seeing you there!"
msgstr ""
#: kursy/templates/occurrence_detail.html:31
#, fuzzy
#| msgid "Sign Up"
msgid "Cancel Sign Up"
msgstr "Zrezygnuj"
@@ -71,17 +77,23 @@ msgstr "Zrezygnuj"
msgid ""
"You need to be logged in to sign up for this event. Please log in or sign up "
"to reserve your spot."
msgstr "Musisz być zalogowany, aby zapisać się na to wydarzenie. Zaloguj się lub zarejestruj, aby zarezerwować swoje miejsce."
msgstr ""
"Musisz być zalogowany, aby zapisać się na to wydarzenie. Zaloguj się lub "
"zarejestruj, aby zarezerwować swoje miejsce."
#: kursy/templates/occurrence_detail.html:47
msgid ""
"This event is fully booked. Please check back later for any cancellations."
msgstr "To wydarzenie jest w pełni zarezerwowane. Sprawdź ponownie później w przypadku zwolnienia miejsc."
msgstr ""
"To wydarzenie jest w pełni zarezerwowane. Sprawdź ponownie później w "
"przypadku zwolnienia miejsc."
#: kursy/templates/occurrence_detail.html:51
msgid ""
"You are not signed up for this event. Please sign up to reserve your spot."
msgstr "Nie jesteś zapisany na to wydarzenie. Zapisz się, aby zarezerwować swoje miejsce."
msgstr ""
"Nie jesteś zapisany na to wydarzenie. Zapisz się, aby zarezerwować swoje "
"miejsce."
#: kursy/templates/occurrence_detail.html:53
msgid "Sign Up for Event"

11
kursy/oauth_validators.py Normal file
View File

@@ -0,0 +1,11 @@
from oauth2_provider.oauth2_validators import OAuth2Validator
class CustomOAuth2Validator(OAuth2Validator):
def get_additional_claims(self, request):
print("get_additional_claims", request.user)
return {
"name": " ".join([request.user.first_name, request.user.last_name]),
"preferred_username": f"studio77-{request.user.id}",
"email": request.user.email,
}

View File

@@ -16,6 +16,7 @@ import os
from pathlib import Path
import dotenv
import logging as lg
PROJECT_DIR = Path(__file__).resolve().parent.parent
BASE_DIR = PROJECT_DIR.parent
@@ -32,6 +33,7 @@ dotenv.load_dotenv(BASE_DIR / ".env")
INSTALLED_APPS = [
"home",
"search",
"purchase",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
@@ -58,6 +60,7 @@ INSTALLED_APPS = [
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.github",
"oauth2_provider",
"tailwind",
"theme",
"widget_tweaks",
@@ -75,6 +78,7 @@ MIDDLEWARE = [
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
"allauth.account.middleware.AccountMiddleware",
"django.middleware.locale.LocaleMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
]
ROOT_URLCONF = "kursy.urls"
@@ -131,6 +135,26 @@ SOCIALACCOUNT_PROVIDERS = {
WSGI_APPLICATION = "kursy.wsgi.application"
OAUTH2_PROVIDER = {
"OIDC_ENABLED": True,
"OIDC_RPID_ENDPOINT": "http://127.0.0.1:8000/oauth2",
"OIDC_ISS_ENDPOINT": "http://127.0.0.1:8000",
"PKCE_REQUIRED": False,
"OAUTH2_VALIDATOR_CLASS": "kursy.oauth_validators.CustomOAuth2Validator",
"SCOPES": {
"openid": "OpenID Connect scope",
"profile": "User profile scope",
"email": "User email scope",
"read": "Read scope",
"write": "Write scope",
},
"OIDC_CLAIM_MAPS": {
"nickname": "preferred_username",
"email": "email",
},
"DEFAULT_SCOPES": ["openid", "profile", "email"],
}
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
@@ -138,7 +162,7 @@ WSGI_APPLICATION = "kursy.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"NAME": BASE_DIR / "db" / "db.sqlite3",
}
}
@@ -218,6 +242,43 @@ STORAGES = {
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
# Logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{asctime} : {levelname} : {filename}:{lineno} : {name} :: {message}",
"style": "{",
},
"simple": {"format": "{asctime} : {levelname} :: {message}", "style": "{"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": True,
},
"django.request": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"home": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
}
# Wagtail settings
WAGTAIL_SITE_NAME = "kursy"
@@ -252,3 +313,12 @@ WAGTAILDOCS_EXTENSIONS = [
]
TAILWIND_APP_NAME = "theme"
# Gitea API
GITEA_ROOT_URL = "http://localhost:3000"
GITEA_URL = f"{GITEA_ROOT_URL}/api/v1"
lg.basicConfig(
level=lg.DEBUG,
format="%(asctime)s : %(levelname)s : %(filename)s:%(lineno)d : %(name)s :: %(message)s",
)

View File

@@ -1,11 +1,16 @@
{% load i18n wagtailcore_tags %}
<header class="bg-blue-900 text-white shadow-md relative">
<header class="bg-blue-900 text-white shadow-md lg:sticky top-0 z-40">
<div class="container mx-auto flex items-center justify-between py-4 px-6">
{% wagtail_site as current_site %}
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
<nav class="flex items-center gap-4">
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
<a href="{% slugurl 'courses' %}" class="hover:underline">{% trans "Courses" %}</a>
<a href="{% slugurl 'blog' %}" class="hover:underline">{% trans "Blog" %}</a>
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Calendar" %}</a>
<a href="{% url 'user_chat' %}" class="hover:underline">{% trans "Help" %}</a>
</nav>
<nav class="flex items-center gap-4">
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Course Calendar" %}</a>
{% if user.is_authenticated %}
<a href="{% url 'account_logout' %}" class="hover:underline">{% trans "Logout" %}</a>
{% else %}
@@ -28,8 +33,8 @@
</div>
<div class="container mx-auto px-6 mb-2 md:mb-0">
<form action="{% url 'search' %}" method="get" class="flex items-center bg-blue-950 rounded-md md:w-auto md:absolute md:left-1/2 md:top-1/2 md:transform md:-translate-x-1/2 md:-translate-y-1/2 md:mt-0">
<input type="text" name="query" placeholder="{% trans 'Search courses...' %}" class="rounded-md px-3 py-2 w-full md:w-auto focus:outline-none">
<form action="{% url 'search' %}" method="get" class="flex items-center bg-blue-950 rounded-md mb-2 lg:w-auto lg:absolute lg:left-1/2 lg:top-1/2 lg:transform lg:-translate-x-1/2 lg:-translate-y-1/2 lg:mt-0">
<input type="text" name="query" placeholder="{% trans 'Search courses...' %}" class="rounded-lg px-3 py-2 w-full lg:w-auto focus:outline-none">
<button type="submit" class="bg-white text-blue-900 rounded-md px-3 py-2 hover:bg-gray-200 transition"><i class="fi fi-br-search"></i></button>
</form>
</div>

View File

@@ -20,7 +20,9 @@ urlpatterns = [
path("accounts/profile/", views.profile, name="profile"),
path("accounts/signup/", views.signup, name="signup"),
path("i18n/", include("django.conf.urls.i18n")),
path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("", include("home.urls")),
path("", include("purchase.urls")),
path("calendar/", views.calendar, name="calendar"),
# TODO: move occurrence related urls to home app
path(

0
purchase/__init__.py Normal file
View File

3
purchase/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
purchase/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class PurchaseConfig(AppConfig):
name = 'purchase'

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.3 on 2026-03-19 17:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('home', '0019_coursepage_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CoursePurchase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purchased_at', models.DateTimeField(auto_now_add=True)),
('refunded', models.BooleanField(default=False)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.coursepage')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

134
purchase/models.py Normal file
View File

@@ -0,0 +1,134 @@
import logging as lg
import os
import requests
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import models
GITEA_ORG_NAME = "Studio77"
logger = lg.getLogger(__name__)
class CoursePurchase(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
course = models.ForeignKey("home.CoursePage", on_delete=models.CASCADE)
purchased_at = models.DateTimeField(auto_now_add=True)
refunded = models.BooleanField(default=False)
def mock_refund(self):
self.refunded = True
self.save()
def _get_gitea_team_id(self, team_name):
api_url = getattr(settings, "GITEA_URL", None)
if not api_url:
logger.debug("GITEA_URL is not set, skipping Gitea team assignment")
return None
try:
response = requests.get(
f"{api_url}/orgs/{GITEA_ORG_NAME}/teams",
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
response.raise_for_status()
teams = response.json()
team = next((team for team in teams if team["name"] == team_name), None)
return team["id"] if team else None
except Exception as e:
logger.exception(
f"Failed to check existing Gitea teams: {e}\n{getattr(response, 'text', '')}",
e,
)
return None
def add_to_gitea_team(self):
course = self.course
user = self.user
team_name = f"course-{course.id}"
api_url = getattr(settings, "GITEA_URL", None)
if not api_url:
logger.debug("GITEA_URL is not set, skipping Gitea team assignment")
return
team_id = self._get_gitea_team_id(team_name)
if not team_id:
logger.warning(
f"Gitea team {team_name} not found for course {course.title}"
)
return
url = f"{api_url}/teams/{team_id}/members/studio77-{user.id}"
try:
response = requests.put(
url,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
if response.status_code == 204:
logger.info(
f"Successfully added user {user.email} to Gitea team {team_name}"
)
else:
response.raise_for_status()
except Exception as e:
logger.error(
f"Failed to add user {user.email} to Gitea team {team_name}: {e}\n{getattr(response, 'text', '')}"
)
def remove_from_gitea_team(self):
course = self.course
user = self.user
team_name = f"course-{course.id}"
api_url = getattr(settings, "GITEA_URL", None)
if not api_url:
logger.debug("GITEA_URL is not set, skipping Gitea team removal")
return
team_id = self._get_gitea_team_id(team_name)
if not team_id:
logger.warning(
f"Gitea team {team_name} not found for course {course.title}"
)
return
url = f"{api_url}/teams/{team_id}/members/studio77-{user.id}"
try:
response = requests.delete(
url,
timeout=5,
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
)
if response.status_code == 204:
logger.info(
f"Successfully removed user {user.email} from Gitea team {team_name}"
)
else:
response.raise_for_status()
except Exception as e:
logger.error(
f"Failed to remove user {user.email} from Gitea team {team_name}: {e}\n{getattr(response, 'text', '')}"
)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
group_name = f"course_{self.course.id}_access"
group, _ = Group.objects.get_or_create(name=group_name)
print(
f"Saving CoursePurchase for user {self.user} and course {self.course.title}, refunded={self.refunded}"
)
if self.refunded:
print(f"Removing user {self.user} from group {group_name} due to refund")
self.remove_from_gitea_team()
self.user.groups.remove(group)
else:
logger.debug(
f"Adding user {self.user} to group {group_name} for course {self.course.title}"
)
self.add_to_gitea_team()

3
purchase/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
purchase/urls.py Normal file
View File

@@ -0,0 +1,16 @@
from django.urls import path
from . import views
urlpatterns = [
path(
"mock-purchase/<int:course_id>/",
views.mock_purchase_course,
name="mock_purchase_course",
),
path(
"mock-refund/<int:purchase_id>/",
views.mock_refund_purchase,
name="mock_refund_purchase",
),
]

21
purchase/views.py Normal file
View File

@@ -0,0 +1,21 @@
from django.shortcuts import redirect, render
from django.urls import reverse
from home.models import CoursePage
from purchase.models import CoursePurchase
def mock_purchase_course(request, course_id):
course = CoursePage.objects.get(id=course_id)
course.mock_purchase(request.user)
return redirect(course.url)
def mock_refund_purchase(request, purchase_id):
purchase = CoursePurchase.objects.get(id=purchase_id)
purchase.mock_refund()
return redirect(purchase.course.url)

View File

@@ -8,10 +8,14 @@ dependencies = [
"django-allauth-ui>=1.8.1",
"django-allauth[socialaccount]>=65.15.0",
"django-browser-reload>=1.21.0",
"django-oauth-toolkit>=3.2.0",
"django-tailwind>=4.4.2",
"django-widget-tweaks>=1.5.1",
"gunicorn>=25.3.0",
"python-dotenv>=1.2.2",
"slippers>=0.6.2",
"uvicorn>=0.42.0",
"uvicorn-worker>=0.4.0",
"wagtail==7.3rc1",
"wagtail-color-panel>=1.7.1",
"wagtailmedia>=0.17.2",

376
uv.lock generated
View File

@@ -57,11 +57,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.2.25"
version = "2026.4.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
@@ -99,39 +99,55 @@ wheels = [
[[package]]
name = "charset-normalizer"
version = "3.4.5"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" },
{ url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" },
{ url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" },
{ url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" },
{ url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" },
{ url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" },
{ url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" },
{ url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" },
{ url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" },
{ url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" },
{ url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" },
{ url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" },
{ url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" },
{ url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "click"
version = "8.3.1"
version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
]
[[package]]
@@ -164,55 +180,55 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.5"
version = "46.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
]
[[package]]
@@ -226,29 +242,29 @@ wheels = [
[[package]]
name = "django"
version = "6.0.3"
version = "6.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/e1/894115c6bd70e2c8b66b0c40a3c367d83a5a48c034a4d904d31b62f7c53a/django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1", size = 10872701, upload-time = "2026-03-03T13:55:15.026Z" }
sdist = { url = "https://files.pythonhosted.org/packages/60/b9/4155091ad1788b38563bd77a7258c0834e8c12a7f56f6975deaf54f8b61d/django-6.0.4.tar.gz", hash = "sha256:8cfa2572b3f2768b2e84983cf3c4811877a01edb64e817986ec5d60751c113ac", size = 10907407, upload-time = "2026-04-07T13:55:44.961Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/b1/23f2556967c45e34d3d3cf032eb1bd3ef925ee458667fb99052a0b3ea3a6/django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3", size = 8358527, upload-time = "2026-03-03T13:55:10.552Z" },
{ url = "https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl", hash = "sha256:14359c809fc16e8f81fd2b59d7d348e4d2d799da6840b10522b6edf7b8afc1da", size = 8368342, upload-time = "2026-04-07T13:55:37.999Z" },
]
[[package]]
name = "django-allauth"
version = "65.15.0"
version = "65.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/c1/d3385f4c3169c1d6eea3c63aed0f36af51478c1d72e46db12bb1a08f8034/django_allauth-65.15.0.tar.gz", hash = "sha256:b404d48cf0c3ee14dacc834c541f30adedba2ff1c433980ecc494d6cb0b395a8", size = 2215709, upload-time = "2026-03-09T13:51:28.675Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3d/df/357187dfff18c7783e4911827a6c69437e290d7259a32a99c23fcd85997f/django_allauth-65.16.1.tar.gz", hash = "sha256:4425ac3088541c4c54983e16e08f6e3eb9f438dc1b1009534fa51c8bb739ed31", size = 2232835, upload-time = "2026-04-17T18:53:59.475Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/b8/c8411339171bd8bc075c09ef190fb42195e9a2149e5c5026e094fe62fce0/django_allauth-65.15.0-py3-none-any.whl", hash = "sha256:ad9fc49c49a9368eaa5bb95456b76e2a4f377b3c6862ee8443507816578c098d", size = 2022994, upload-time = "2026-03-09T13:51:19.711Z" },
{ url = "https://files.pythonhosted.org/packages/ad/58/d95b6c3088d83697bfd93782ee57bc6a6462e41eb19121a947b8a015396a/django_allauth-65.16.1-py3-none-any.whl", hash = "sha256:e49df24056bf37c44e56aaad1e51f78994b7d175bc3476d65e8f8f58390a8ce8", size = 2051868, upload-time = "2026-04-17T18:54:12.032Z" },
]
[package.optional-dependencies]
@@ -308,6 +324,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/e4/ec99d52aa04e204e938564b603f4591e2e82e236ed59af664fee35179e75/django_modelcluster-6.4.1-py2.py3-none-any.whl", hash = "sha256:ccc190cd9e22c24900ea2410bff64d444d48f43f0f4aedeed0f6cd94e2536698", size = 29315, upload-time = "2025-12-04T12:21:39.911Z" },
]
[[package]]
name = "django-oauth-toolkit"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "jwcrypto" },
{ name = "oauthlib" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/95/efd83b35c34b86eb2249d2b54c5eaf383c48f3f19034aa6f3807e37471b6/django_oauth_toolkit-3.2.0.tar.gz", hash = "sha256:c36761ae6810083d95a652e9c820046cde0d45a2e2a5574bbe7202656ec20bb6", size = 114211, upload-time = "2026-01-08T22:03:13.311Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/cc/f27a784c0ecd13335abd9ef85ebb80dbc04945f919da5f496f56e3562751/django_oauth_toolkit-3.2.0-py3-none-any.whl", hash = "sha256:bd2cd2719b010231a2f370f927dbcc740454fb1d0dd7e7f4138f36227363dc26", size = 87077, upload-time = "2026-01-08T22:03:12.123Z" },
]
[[package]]
name = "django-permissionedforms"
version = "0.1"
@@ -322,15 +353,15 @@ wheels = [
[[package]]
name = "django-stubs-ext"
version = "5.2.9"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/03/9c2be939490d2282328db4611bc5956899f5ff7eabc3e88bd4b964a87373/django_stubs_ext-5.2.9.tar.gz", hash = "sha256:6db4054d1580657b979b7d391474719f1a978773e66c7070a5e246cd445a25a9", size = 6497, upload-time = "2026-01-20T23:58:59.462Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/e6/5dcdaa785ec3eed5fc196c7e68fb7ad9d9fe6d5acccea4690e65f2546417/django_stubs_ext-6.0.3.tar.gz", hash = "sha256:3307d42132bc295d5744de6276bc5fdf6896efc70f891e21c0ae8bdf529d2762", size = 6663, upload-time = "2026-04-18T15:10:53.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/f7/0d5f7d7e76fe972d9f560f687fdc0cab4db9e1624fd90728ca29b4ed7a63/django_stubs_ext-5.2.9-py3-none-any.whl", hash = "sha256:230c51575551b0165be40177f0f6805f1e3ebf799b835c85f5d64c371ca6cf71", size = 9974, upload-time = "2026-01-20T23:58:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/10/fa/0a3a05c29d6295dbd52fa3cb4047a95de11ba4f2696072d6f3f2c1e6f370/django_stubs_ext-6.0.3-py3-none-any.whl", hash = "sha256:9e4105955419ae310d7da9cfd808e039d4dae3092c628f021057bb4f2c237f8f", size = 10354, upload-time = "2026-04-18T15:10:52.395Z" },
]
[[package]]
@@ -398,14 +429,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.16.1"
version = "3.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
]
[[package]]
@@ -436,12 +467,33 @@ wheels = [
]
[[package]]
name = "idna"
version = "3.11"
name = "gunicorn"
version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "idna"
version = "3.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
]
[[package]]
@@ -456,6 +508,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jwcrypto"
version = "1.5.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/90/f065668004d22715c1940d6e88e4c3afc8ee16d5664e4478d2c8fd23a250/jwcrypto-1.5.7.tar.gz", hash = "sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6", size = 89535, upload-time = "2026-04-07T00:35:36.116Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/24/fb7da4d6613de7001feaf540d4b5969c6b5a1c42839043b0196cb13aa057/jwcrypto-1.5.7-py3-none-any.whl", hash = "sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0", size = 94799, upload-time = "2026-04-07T00:35:35.085Z" },
]
[[package]]
name = "kursy"
version = "0.1.0"
@@ -466,10 +531,14 @@ dependencies = [
{ name = "django-allauth", extra = ["socialaccount"] },
{ name = "django-allauth-ui" },
{ name = "django-browser-reload" },
{ name = "django-oauth-toolkit" },
{ name = "django-tailwind" },
{ name = "django-widget-tweaks" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "slippers" },
{ name = "uvicorn" },
{ name = "uvicorn-worker" },
{ name = "wagtail" },
{ name = "wagtail-color-panel" },
{ name = "wagtailmedia" },
@@ -482,10 +551,14 @@ requires-dist = [
{ name = "django-allauth", extras = ["socialaccount"], specifier = ">=65.15.0" },
{ name = "django-allauth-ui", specifier = ">=1.8.1" },
{ name = "django-browser-reload", specifier = ">=1.21.0" },
{ name = "django-oauth-toolkit", specifier = ">=3.2.0" },
{ name = "django-tailwind", specifier = ">=4.4.2" },
{ name = "django-widget-tweaks", specifier = ">=1.5.1" },
{ name = "gunicorn", specifier = ">=25.3.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "slippers", specifier = ">=0.6.2" },
{ name = "uvicorn", specifier = ">=0.42.0" },
{ name = "uvicorn-worker", specifier = ">=0.4.0" },
{ name = "wagtail", specifier = "==7.3rc1" },
{ name = "wagtail-color-panel", specifier = ">=1.7.1" },
{ name = "wagtailmedia", specifier = ">=0.17.2" },
@@ -589,36 +662,45 @@ wheels = [
]
[[package]]
name = "pillow"
version = "12.1.1"
name = "packaging"
version = "26.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
]
[[package]]
name = "pillow"
version = "12.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
]
[[package]]
@@ -657,20 +739,20 @@ wheels = [
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pyjwt"
version = "2.11.0"
version = "2.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
]
[package.optional-dependencies]
@@ -748,7 +830,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -756,22 +838,22 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
]
[[package]]
name = "rich"
version = "14.3.3"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
@@ -785,7 +867,7 @@ wheels = [
[[package]]
name = "slippers"
version = "0.6.2"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
@@ -793,9 +875,9 @@ dependencies = [
{ name = "typeguard" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/c6/6af4500f31e53e9f782d264cb715f1a8cc9ac4248fba9fe2fe172822ea79/slippers-0.6.2.tar.gz", hash = "sha256:4cb555b8822ba0d404e5405723f5d723994022c29046008ee917081031bc0cf1", size = 59499, upload-time = "2023-08-01T05:04:18.964Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/3c/979ab01ee6515fb3d6f10cfc10ef2e6219817828b972c54d49f6ddd9ddd2/slippers-0.6.3.tar.gz", hash = "sha256:8602f462da79b707d25ec050fb51b6cbe5fb225b3c7faf40d1f02876d970e9f0", size = 59793, upload-time = "2026-03-30T04:51:59.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/8c/1fe2803870861f9a7f0cf502e4d1361dbb3685997118f4f76fd83ec9272f/slippers-0.6.2-py3-none-any.whl", hash = "sha256:739e05f85354becbf0a65daab831eea62557d89e7512042209ab629af4378bca", size = 61359, upload-time = "2023-08-01T05:04:17.378Z" },
{ url = "https://files.pythonhosted.org/packages/9c/b9/76c4b0535d26089d0d01f5c9cb3529b121babd1abf605e6c7e12acedb09b/slippers-0.6.3-py3-none-any.whl", hash = "sha256:cddd92cb998fb48472f863a811566f5a187b392f65ae4b0df81008b56dff214c", size = 62528, upload-time = "2026-03-30T04:51:58.227Z" },
]
[[package]]
@@ -854,11 +936,11 @@ wheels = [
[[package]]
name = "tzdata"
version = "2025.3"
version = "2026.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
]
[[package]]
@@ -870,6 +952,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "uvicorn"
version = "0.45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/62b0d9a2cfc8b4de6771322dae30f2db76c66dae9ec32e94e176a44ad563/uvicorn-0.45.0.tar.gz", hash = "sha256:3fe650df136c5bd2b9b06efc5980636344a2fbb840e9ddd86437d53144fa335d", size = 87818, upload-time = "2026-04-21T10:43:46.815Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/88/d0f7512465b166a4e931ccf7e77792be60fb88466a43964c7566cbaff752/uvicorn-0.45.0-py3-none-any.whl", hash = "sha256:2db26f588131aeec7439de00f2dd52d5f210710c1f01e407a52c90b880d1fd4f", size = 69838, upload-time = "2026-04-21T10:43:45.029Z" },
]
[[package]]
name = "uvicorn-worker"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gunicorn" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/59/9101b9c0680fd80e9d26c07deb822a5d18a324339fcf9cd017885ee808ad/uvicorn_worker-0.4.0.tar.gz", hash = "sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493", size = 9361, upload-time = "2025-09-20T10:47:01.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/25/09cd7a90c8bb7fb693be0d6704fccd5f9778d5513214b7a01cc4a94ff314/uvicorn_worker-0.4.0-py3-none-any.whl", hash = "sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde", size = 5364, upload-time = "2025-09-20T10:46:59.776Z" },
]
[[package]]
name = "wagtail"
version = "7.3rc1"
@@ -901,14 +1009,14 @@ wheels = [
[[package]]
name = "wagtail-color-panel"
version = "1.7.1"
version = "1.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wagtail" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/bd/043c8cfc6eae14303b555782add80148db0596441e8770723cf17e664d4d/wagtail_color_panel-1.7.1.tar.gz", hash = "sha256:efc42058a49c39bea6a61523b230c71cf31e5aeaa269281c53146eeec02fc509", size = 10315, upload-time = "2025-11-23T06:07:53.765Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/44/bd3e5a9ee9bc9b72153e0a227aae5b7e181d2ce4e601ff9b01c1074fbbb5/wagtail_color_panel-1.8.1.tar.gz", hash = "sha256:aca26f8b7178ff596aa2eac8982167396bbbc222fadbae4b6b1f18291b33473a", size = 10776, upload-time = "2026-04-12T19:16:14.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/1d/d004a5b57c8b1db6edee953e3222436c050dff2667320ca345964551640a/wagtail_color_panel-1.7.1-py3-none-any.whl", hash = "sha256:77ee02a6dd08b21e8806bb3e8c7dc01ad679dbeef864ad915fec32f5e8bcd574", size = 9383, upload-time = "2025-11-23T06:07:52.733Z" },
{ url = "https://files.pythonhosted.org/packages/6d/bb/86ee676a4613c0dade91685fb3402766de4c20874ad6f18d91807d9813cc/wagtail_color_panel-1.8.1-py3-none-any.whl", hash = "sha256:6e8aaeceac3618a9b69370b5097fa4d3c2db19b31bff95b7726343660793e55e", size = 9256, upload-time = "2026-04-12T19:16:13.68Z" },
]
[[package]]