Compare commits
125 Commits
a02617f8d2
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d319d8efa | |||
| 874f4e29db | |||
| 131fe8fecc | |||
| b154da09a5 | |||
| f47fd21deb | |||
| ac27137b77 | |||
| 7c445d4b66 | |||
| e4f0611ac5 | |||
| 33dee87cb2 | |||
| 3de6d37e03 | |||
| 5503bbe8f7 | |||
| dd5ecec3f6 | |||
| c778669a7a | |||
| 09f6f800de | |||
| 1ac17109a3 | |||
| 325e55417b | |||
| e363bfd6dd | |||
| 90308d2383 | |||
| de4f36a25c | |||
| 424ffcffaf | |||
| f0a638be2e | |||
| a61664a46e | |||
| 1a73a9e373 | |||
| 4877e859bd | |||
| 848a49c92d | |||
| ac9efaaf91 | |||
| 2024e2f90c | |||
| 6ec0f4e732 | |||
| 35e7e93600 | |||
| a7f43483f0 | |||
| d773addc53 | |||
| b143d92cb2 | |||
| 9d7a7f7432 | |||
| 0bb2eeeaa6 | |||
| b9acf6a1c7 | |||
| 57efd95b0c | |||
| 5696fdddaa | |||
| 37383b0736 | |||
| 784fdd1284 | |||
| 336e499973 | |||
| e4fa941fd6 | |||
| 48b3f46623 | |||
| 8caba9b85b | |||
| d0f687f56f | |||
| e70a9aeb9c | |||
| e0350de530 | |||
| 62bf3fdc08 | |||
| 2b2054ace6 | |||
| f129b0462a | |||
| aa047b3fd8 | |||
| 429b531bad | |||
| 0438a77149 | |||
| 40f0ef8ea5 | |||
| e53ecef5dc | |||
| bf39724837 | |||
| 6f82787022 | |||
| 46343c1ea8 | |||
| 76c8a277da | |||
| 169a6d9dfb | |||
| f59841b5b8 | |||
| 32c1e1e6ff | |||
| 8a0335fabc | |||
| 74b9d3bbc6 | |||
| ffe7828d8e | |||
| a12954fb84 | |||
| 7f50674bb8 | |||
| f9b3bc7096 | |||
| 932fe7316b | |||
| 84f125c4b3 | |||
| bb4d9993ec | |||
| beb74539e3 | |||
| f9eda0ca57 | |||
| 4a30b68b5c | |||
| e18ff79ba7 | |||
| 1ce2efd736 | |||
| 36046ef816 | |||
| e8a26f497e | |||
| 1ff72c7a94 | |||
| 580d60ec4f | |||
| 72def373e3 | |||
| a50cadc873 | |||
| 551057b067 | |||
| ad7ddbe887 | |||
| d6b7cdfe6a | |||
| 56286c2fd9 | |||
| ba4f6ad65d | |||
| ed7041ae40 | |||
| fa948a98e2 | |||
| e8a5091dfd | |||
| a0ee6ecd14 | |||
| d6c9aa3db3 | |||
| 9751d19401 | |||
| cda9adb986 | |||
| e7e7fd118d | |||
| 132b1e1722 | |||
| 7f557a3247 | |||
| 8cf1a55161 | |||
| 61a04e5040 | |||
| e5a0caa8b6 | |||
| 25e6088355 | |||
| 8ec391ccde | |||
| 3b007f324f | |||
| 6e003009fa | |||
| 69578f1dba | |||
| 3eb81b343c | |||
| ce5aac0e89 | |||
| c534f500ad | |||
| 63c6b645c3 | |||
| b16cb367d3 | |||
| 503233d323 | |||
| b50ab06a22 | |||
| cda339a336 | |||
| 541a73ce36 | |||
| 8932eeefbf | |||
| 80e5e2a422 | |||
| a686bccd54 | |||
| 6be67a9100 | |||
| bee360dfbb | |||
| a20a61be82 | |||
| b9675385aa | |||
| c33def1124 | |||
| 52dfa51af2 | |||
| 4661bcdffd | |||
| 27c06fe0b5 | |||
| 44bf6df686 |
@@ -7,3 +7,13 @@ venv
|
|||||||
db.sqlite3
|
db.sqlite3
|
||||||
static
|
static
|
||||||
media
|
media
|
||||||
|
docs
|
||||||
|
logs
|
||||||
|
staticfiles
|
||||||
|
.gitignore
|
||||||
|
AGENTS.md
|
||||||
|
Dockerfile
|
||||||
|
Makefile
|
||||||
|
nginx.conf
|
||||||
|
Procfile
|
||||||
|
uv.lock
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
allow:
|
||||||
|
- dependency-type: "direct"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -14,20 +14,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Configurar Python
|
- name: Configurar Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
- name: Configurar uv
|
||||||
|
uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6
|
||||||
- name: Instalar dependencias
|
- name: Instalar dependencias
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
uv sync --no-dev --no-install-project
|
||||||
pip install -r requirements.txt
|
|
||||||
- name: Ejecutar tests
|
- name: Ejecutar tests
|
||||||
env:
|
env:
|
||||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||||
run: |
|
run: |
|
||||||
python manage.py test
|
SECRET_KEY=testkeynotuseinproducto uv run python manage.py test
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -37,13 +38,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Configurar Docker Buildx
|
- name: Configurar Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||||
|
|
||||||
- name: Build (sin push)
|
- name: Build (sin push)
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|||||||
@@ -9,22 +9,25 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Configurar Python
|
- name: Configurar Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
- name: Configurar uv
|
||||||
|
uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6
|
||||||
- name: Instalar dependencias
|
- name: Instalar dependencias
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
uv sync --no-dev --no-install-project
|
||||||
pip install -r requirements.txt
|
|
||||||
- name: Ejecutar tests
|
- name: Ejecutar tests
|
||||||
env:
|
env:
|
||||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||||
run: |
|
run: |
|
||||||
python manage.py test
|
SECRET_KEY=donotusethisinproductionitisunsafe uv run python manage.py test
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test
|
needs: test
|
||||||
@@ -34,13 +37,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Configurar Docker Buildx
|
- name: Configurar Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||||
|
|
||||||
- name: Login en GHCR
|
- name: Login en GHCR
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -52,7 +55,7 @@ jobs:
|
|||||||
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
|
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build y Push
|
- name: Build y Push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
name: opencode
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
opencode:
|
|
||||||
if: |
|
|
||||||
contains(github.event.comment.body, ' /oc') ||
|
|
||||||
startsWith(github.event.comment.body, '/oc') ||
|
|
||||||
contains(github.event.comment.body, ' /opencode') ||
|
|
||||||
startsWith(github.event.comment.body, '/opencode')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run opencode
|
|
||||||
uses: anomalyco/opencode/github@latest
|
|
||||||
env:
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
||||||
with:
|
|
||||||
model: openai/gpt-5.3-codex
|
|
||||||
@@ -8,3 +8,4 @@ __pycache__/
|
|||||||
tienda/__pycache__/
|
tienda/__pycache__/
|
||||||
proyecto/__pycache__/
|
proyecto/__pycache__/
|
||||||
media
|
media
|
||||||
|
staticfiles
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"python.REPL.enableREPLSmartSend": false
|
"python.REPL.enableREPLSmartSend": false,
|
||||||
|
"makefile.configureOnOpen": false
|
||||||
}
|
}
|
||||||
@@ -3,59 +3,55 @@
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests (runs tienda/tests.py by default)
|
make test # Run tests
|
||||||
make test
|
python manage.py runserver # Dev server
|
||||||
|
python manage.py migrate # Migrations
|
||||||
# Or manually
|
python manage.py collectstatic # Static files (production)
|
||||||
python manage.py test
|
|
||||||
|
|
||||||
# Run dev server
|
|
||||||
python manage.py runserver
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
python manage.py migrate
|
|
||||||
|
|
||||||
# Static files (production)
|
|
||||||
python manage.py collectstatic
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Redis**: Must be running on `redis://127.0.0.1:6379/1` for sessions and caching
|
- **Redis**: `redis://127.0.0.1:6379/1` (Linux: `sudo systemctl start redis-server`)
|
||||||
- Start: `sudo systemctl start redis-server` (Linux) or `brew services start redis` (macOS)
|
- **PostgreSQL**: Default. Set `POSTGRES_ENABLED=False` for SQLite
|
||||||
- Verify: `redis-cli ping` → PONG
|
- **Environment**: Copy `.env.example` to `.env`
|
||||||
- **PostgreSQL**: Default database; set `POSTGRES_ENABLED=False` to use SQLite
|
|
||||||
- **Environment**: Copy `.env.example` to `.env` and configure required vars
|
|
||||||
|
|
||||||
## Important Quirks
|
## Quirks
|
||||||
|
|
||||||
1. **Migrations**: If `makemigrations` fails with error code 130, **check `tienda/migrations/`** - the file is often created despite the error
|
1. **Migrations**: If `makemigrations` fails with error 130, check `tienda/migrations/` - file often created anyway
|
||||||
2. **Test DB**: Uses SQLite regardless of POSTGRES_ENABLED (hardcoded in settings.py)
|
2. **Test DB**: Always uses SQLite (hardcoded)
|
||||||
3. **App URL**: Not at `/` - access at `http://localhost:8000/tienda/`
|
3. **App URL**: `http://localhost:8000/tienda/` (not `/`)
|
||||||
4. **Admin**: At `/admin/` (not `/tienda/admin/`)
|
4. **Admin**: `/admin/`
|
||||||
5. **Custom User Model**: AUTH_USER_MODEL = 'tienda.User' - use for all user-related queries
|
5. **User Model**: Use `tienda.User` for queries
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- `proyecto/` - Django settings, URLs, WSGI/ASGI
|
- `proyecto/` - Django settings, URLs, WSGI/ASGI
|
||||||
- `tienda/` - Main app (models, views, admin, templates)
|
- `tienda/` - Main app (models, views, admin, templates)
|
||||||
- `tienda/static/` - CSS, JS, images, fonts
|
|
||||||
- Templates extend `tienda/templates/tienda/base.html`
|
- Templates extend `tienda/templates/tienda/base.html`
|
||||||
|
|
||||||
## Shipping Restrictions
|
## Shipping
|
||||||
|
|
||||||
Only sells to Almería province, Spain (postal codes 04xxx). All addresses saved with country "España".
|
Only Almería province, Spain (04xxx). Country: "España".
|
||||||
|
|
||||||
## External Services
|
## External Services
|
||||||
|
|
||||||
- **Payment**: Stripe + PayPal (configured via .env)
|
- **Payment**: Stripe + PayPal (via .env)
|
||||||
- **Storage**: S3 support - set `S3_ENABLE=True` to enable
|
- **Storage**: S3 - set `S3_ENABLE=True`
|
||||||
- **Email**: SMTP required (see .env.example)
|
- **Email**: SMTP (see .env.example)
|
||||||
- **Async**: Celery uses Redis broker
|
- **Async**: Celery + Redis
|
||||||
|
|
||||||
## Useful References
|
## Deploy
|
||||||
|
|
||||||
- Full developer docs: `.github/copilot-instructions.md`
|
- Push to `origin` and `github` after changes
|
||||||
- Redis setup: `docs/REDIS_SETUP.md`
|
- GitHub Actions updates `ghcr.io/dsaub/proyecto-mvc:development`
|
||||||
- PayPal: `docs/PAYPAL_SETUP.md`, `docs/PAYPAL_TROUBLESHOOTING.md`
|
- SSH: `debian@172.16.14.221` (requires VPN - skip if unavailable)
|
||||||
- View documentation: `docs/views/`
|
- Stack: `/home/debian/composes/mvc/mvc.yml` on swarm
|
||||||
|
- Timeout: 1 minute max when updating services
|
||||||
|
- Docker job requires Test job to pass first
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- **Repo**: https://github.com/dsaub/proyecto-final
|
||||||
|
- **Python**: 3.14, use `.venv` virtualenv
|
||||||
|
- **GIT**: origin → git.elordenador.org, github → GitHub
|
||||||
|
- **Docs**: `.github/copilot-instructions.md`, `docs/`
|
||||||
+22
-4
@@ -4,16 +4,34 @@ ENV PYTHONDONTWRITEBYTECODE=1
|
|||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
COPY pyproject.toml uv.lock /app/
|
||||||
RUN apk --no-cache update && apk --no-cache upgrade
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . /app/
|
RUN apk --no-cache update \
|
||||||
|
&& apk --no-cache upgrade \
|
||||||
|
&& apk --no-cache add \
|
||||||
|
build-base \
|
||||||
|
freetype-dev \
|
||||||
|
jpeg-dev \
|
||||||
|
zlib-dev \
|
||||||
|
&& pip install --no-cache-dir uv \
|
||||||
|
&& uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
|
||||||
|
|
||||||
|
COPY ./entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
COPY ./proyecto /app/proyecto
|
||||||
|
COPY ./tienda /app/tienda
|
||||||
|
COPY ./manage.py /app/manage.py
|
||||||
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
RUN mkdir -pv /fonts
|
RUN mkdir -pv /fonts
|
||||||
COPY tienda/static/fonts/ /fonts/
|
COPY tienda/static/fonts/ /fonts/
|
||||||
|
|
||||||
|
RUN addgroup -S app \
|
||||||
|
&& adduser -S app -G app \
|
||||||
|
&& chown -R app:app /app /fonts
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
web: gunicorn proyecto.wsgi --bind 0.0.0.0:$PORT
|
||||||
|
worker: celery -A proyecto worker --loglevel=info
|
||||||
@@ -39,7 +39,7 @@ Con tus valores reales del Sandbox.
|
|||||||
|
|
||||||
### 6. Instalar el SDK de PayPal
|
### 6. Instalar el SDK de PayPal
|
||||||
```bash
|
```bash
|
||||||
pip install paypalrestsdk
|
uv add paypalrestsdk
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Usar cuentas de prueba para transacciones
|
### 7. Usar cuentas de prueba para transacciones
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Si todo está bien, deberías ver:
|
|||||||
|
|
||||||
## Checklist de Configuración
|
## Checklist de Configuración
|
||||||
|
|
||||||
- [ ] `pip install paypalrestsdk` (verificar con: `.venv/bin/pip list | grep paypal`)
|
- [ ] `uv add paypalrestsdk` (verificar con: `uv pip list | grep paypal`)
|
||||||
- [ ] `PAYPAL_CLIENT_ID` en settings.py (no vacío)
|
- [ ] `PAYPAL_CLIENT_ID` en settings.py (no vacío)
|
||||||
- [ ] `PAYPAL_CLIENT_SECRET` en settings.py (no vacío)
|
- [ ] `PAYPAL_CLIENT_SECRET` en settings.py (no vacío)
|
||||||
- [ ] `PAYPAL_MODE = 'sandbox'` en settings.py
|
- [ ] `PAYPAL_MODE = 'sandbox'` en settings.py
|
||||||
|
|||||||
+3
-3
@@ -5,10 +5,10 @@ set -eu
|
|||||||
echo "Sleeping due to mysql..."
|
echo "Sleeping due to mysql..."
|
||||||
sleep 10
|
sleep 10
|
||||||
echo "Running DB migrations..."
|
echo "Running DB migrations..."
|
||||||
python manage.py migrate
|
uv run python manage.py migrate
|
||||||
echo "Collecting STATIC..."
|
echo "Collecting STATIC..."
|
||||||
python manage.py collectstatic --noinput --clear
|
uv run python manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
echo "Running server!"
|
echo "Running server!"
|
||||||
|
|
||||||
gunicorn --bind 0.0.0.0:8000 proyecto.wsgi:application --forwarded-allow-ips="*"
|
uv run gunicorn --bind 0.0.0.0:8000 proyecto.wsgi:application --forwarded-allow-ips="*"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
from .celery import app as celery_app
|
from .celery import app as celery
|
||||||
__all__ = ('celery_app',)
|
__all__ = ('celery',)
|
||||||
+6
-1
@@ -1,8 +1,13 @@
|
|||||||
from celery import Celery
|
from celery import Celery
|
||||||
import os
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
app = Celery('proyecto')
|
app = Celery('proyecto')
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
|
||||||
app.config_from_object('django.conf:settings', namespace="CELERY")
|
app.config_from_object('django.conf:settings', namespace="CELERY")
|
||||||
|
|
||||||
|
user_options = {}
|
||||||
|
|
||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from jinja2 import Environment
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.templatetags.static import static
|
|
||||||
|
|
||||||
def environment(**options):
|
|
||||||
env = Environment(**options)
|
|
||||||
env.globals.update({
|
|
||||||
'static': static,
|
|
||||||
'url': reverse,
|
|
||||||
})
|
|
||||||
return env
|
|
||||||
+86
-121
@@ -11,83 +11,47 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os, sys
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import environ
|
||||||
|
|
||||||
|
|
||||||
|
DEV_ENV = len(sys.argv) > 1 and sys.argv[1] == 'runserver'
|
||||||
|
|
||||||
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
||||||
|
|
||||||
|
|
||||||
def load_dotenv(dotenv_path: Path) -> None:
|
|
||||||
if not dotenv_path.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
for raw_line in dotenv_path.read_text(encoding='utf-8').splitlines():
|
|
||||||
line = raw_line.strip()
|
|
||||||
if not line or line.startswith('#') or '=' not in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key, value = line.split('=', 1)
|
|
||||||
key = key.strip()
|
|
||||||
value = value.strip().strip('"').strip("'")
|
|
||||||
os.environ.setdefault(key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def env_bool(name: str, default: bool = False) -> bool:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return value.strip().lower() in {'1', 'true', 'yes', 'on'}
|
|
||||||
|
|
||||||
|
|
||||||
def env_list(name: str, default: list[str] | None = None) -> list[str]:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default or []
|
|
||||||
return [item.strip() for item in value.split(',') if item.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
def env_int(name: str, default: int) -> int:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return int(value)
|
|
||||||
|
|
||||||
|
|
||||||
def env_str(name: str, default: str = '') -> str:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
return value.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def env_optional_str(name: str) -> str | None:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
value = value.strip()
|
|
||||||
return value or None
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR / '.env')
|
env = environ.Env(
|
||||||
|
DEBUG=(bool, True),
|
||||||
|
S3_ENABLE=(bool, False),
|
||||||
|
S3_USE_LOCAL_URLS=(bool, False),
|
||||||
|
POSTGRES_ENABLED=(bool, True),
|
||||||
|
POSTGRES_PORT=(int, 5432),
|
||||||
|
SMTP_PORT=(int, 587),
|
||||||
|
AWS_S3_USE_SSL=(bool, True),
|
||||||
|
AWS_QUERYSTRING_AUTH=(bool, False),
|
||||||
|
)
|
||||||
|
env.read_env(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r2r(911)pv59i(6w)5e!_-^ao')
|
SECRET_KEY = env('SECRET_KEY', default='')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env_bool('DEBUG', True)
|
DEBUG = env.bool('DEBUG')
|
||||||
S3_ENABLE = env_bool('S3_ENABLE', False)
|
S3_ENABLE = env.bool('S3_ENABLE')
|
||||||
S3_USE_LOCAL_URLS = env_bool('S3_USE_LOCAL_URLS', False)
|
S3_USE_LOCAL_URLS = env.bool('S3_USE_LOCAL_URLS')
|
||||||
|
|
||||||
ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [
|
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[
|
||||||
'192.168.1.142',
|
|
||||||
'localhost',
|
'localhost',
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
|
'zkqpv8r3-8000.uks1.devtunnels.ms'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -101,7 +65,9 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.forms',
|
||||||
'compressor',
|
'compressor',
|
||||||
|
'ninja',
|
||||||
]
|
]
|
||||||
|
|
||||||
if S3_ENABLE:
|
if S3_ENABLE:
|
||||||
@@ -136,14 +102,6 @@ TEMPLATES = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.jinja2.Jinja2',
|
|
||||||
'DIRS': [BASE_DIR / 'templates/jinja2'],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'environment': 'proyecto.jinja2.environment',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'proyecto.wsgi.application'
|
WSGI_APPLICATION = 'proyecto.wsgi.application'
|
||||||
@@ -153,33 +111,25 @@ WSGI_APPLICATION = 'proyecto.wsgi.application'
|
|||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
# Usa PostgreSQL por defecto (POSTGRES_ENABLED=True); si no, SQLite.
|
# Usa PostgreSQL por defecto (POSTGRES_ENABLED=True); si no, SQLite.
|
||||||
|
|
||||||
if RUNNING_TESTS:
|
if RUNNING_TESTS or not env.bool('POSTGRES_ENABLED'):
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elif env_bool('POSTGRES_ENABLED', True):
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
|
||||||
'NAME': os.getenv('POSTGRES_DB', 'tienda'),
|
|
||||||
'USER': os.getenv('POSTGRES_USER', 'postgres'),
|
|
||||||
'PASSWORD': os.getenv('POSTGRES_PASSWORD', ''),
|
|
||||||
'HOST': os.getenv('POSTGRES_HOST', '127.0.0.1'),
|
|
||||||
'PORT': env_int('POSTGRES_PORT', 5432),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': env('POSTGRES_DB', default='tienda'),
|
||||||
|
'USER': env('POSTGRES_USER', default='postgres'),
|
||||||
|
'PASSWORD': env('POSTGRES_PASSWORD', default=''),
|
||||||
|
'HOST': env('POSTGRES_HOST', default='127.0.0.1'),
|
||||||
|
'PORT': env.int('POSTGRES_PORT'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@@ -214,8 +164,10 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
COMPRESS_ROOT = STATIC_ROOT
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'tienda' / 'static',
|
BASE_DIR / 'tienda' / 'static',
|
||||||
]
|
]
|
||||||
@@ -234,15 +186,15 @@ STORAGES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if S3_ENABLE:
|
if S3_ENABLE:
|
||||||
AWS_STORAGE_BUCKET_NAME = env_str('AWS_STORAGE_BUCKET_NAME') or None
|
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default='') or None
|
||||||
AWS_ACCESS_KEY_ID = env_optional_str('AWS_ACCESS_KEY_ID')
|
AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default=None)
|
||||||
AWS_SECRET_ACCESS_KEY = env_optional_str('AWS_SECRET_ACCESS_KEY')
|
AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default=None)
|
||||||
AWS_S3_REGION_NAME = env_optional_str('AWS_S3_REGION_NAME')
|
AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', default=None)
|
||||||
AWS_S3_ENDPOINT_URL = env_optional_str('AWS_S3_ENDPOINT_URL')
|
AWS_S3_ENDPOINT_URL = env('AWS_S3_ENDPOINT_URL', default=None)
|
||||||
AWS_S3_CUSTOM_DOMAIN = env_optional_str('AWS_S3_CUSTOM_DOMAIN')
|
AWS_S3_CUSTOM_DOMAIN = env('AWS_S3_CUSTOM_DOMAIN', default=None)
|
||||||
AWS_S3_USE_SSL = env_bool('AWS_S3_USE_SSL', True)
|
AWS_S3_USE_SSL = env.bool('AWS_S3_USE_SSL')
|
||||||
AWS_QUERYSTRING_AUTH = env_bool('AWS_QUERYSTRING_AUTH', False)
|
AWS_QUERYSTRING_AUTH = env.bool('AWS_QUERYSTRING_AUTH')
|
||||||
AWS_DEFAULT_ACL = env_str('AWS_DEFAULT_ACL', 'public-read') or None
|
AWS_DEFAULT_ACL = env('AWS_DEFAULT_ACL', default='public-read') or None
|
||||||
AWS_S3_OBJECT_PARAMETERS = {}
|
AWS_S3_OBJECT_PARAMETERS = {}
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
@@ -254,6 +206,14 @@ if S3_ENABLE:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if S3_ENABLE and AWS_S3_CUSTOM_DOMAIN:
|
||||||
|
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
|
||||||
|
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
|
||||||
|
else:
|
||||||
|
STATIC_URL = env("STATIC_URL", default="static/")
|
||||||
|
MEDIA_URL = env("MEDIA_URL", default="media/")
|
||||||
|
|
||||||
|
COMPRESS_URL = STATIC_URL
|
||||||
STATICFILES_FINDERS = [
|
STATICFILES_FINDERS = [
|
||||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
@@ -262,15 +222,13 @@ STATICFILES_FINDERS = [
|
|||||||
|
|
||||||
COMPRESS_PRECOMPILERS = ()
|
COMPRESS_PRECOMPILERS = ()
|
||||||
|
|
||||||
# Media files (User uploads)
|
MEDIA_ROOT = Path(env('MEDIA_ROOT', default='/app/media'))
|
||||||
MEDIA_URL = 'media/'
|
|
||||||
MEDIA_ROOT = Path(os.getenv('MEDIA_ROOT', '/app/media'))
|
|
||||||
|
|
||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django_redis.cache.RedisCache',
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'),
|
'LOCATION': env('REDIS_URL', default='redis://127.0.0.1:6379/1'),
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
}
|
}
|
||||||
@@ -295,33 +253,30 @@ MESSAGE_TAGS = {
|
|||||||
# Login URL
|
# Login URL
|
||||||
LOGIN_URL = '/tienda/login/'
|
LOGIN_URL = '/tienda/login/'
|
||||||
|
|
||||||
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '')
|
STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY', default='')
|
||||||
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
|
STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY', default='')
|
||||||
|
|
||||||
# PayPal Configuration (Sandbox)
|
# PayPal Configuration (Sandbox)
|
||||||
# Para obtener credenciales: https://sandbox.paypal.com/
|
# Para obtener credenciales: https://sandbox.paypal.com/
|
||||||
PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') # Reemplazar con tu Client ID de PayPal Sandbox
|
PAYPAL_CLIENT_ID = env('PAYPAL_CLIENT_ID', default='') # Reemplazar con tu Client ID de PayPal Sandbox
|
||||||
PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') # Reemplazar con tu Client Secret de PayPal Sandbox
|
PAYPAL_CLIENT_SECRET = env('PAYPAL_CLIENT_SECRET', default='') # Reemplazar con tu Client Secret de PayPal Sandbox
|
||||||
PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'sandbox') # Cambiar a 'live' en producción
|
PAYPAL_MODE = env('PAYPAL_MODE', default='sandbox') # Cambiar a 'live' en producción
|
||||||
|
|
||||||
|
|
||||||
SMTP_ENDPOINT = os.getenv('SMTP_ENDPOINT', 'smtp.email.eu-paris-1.oci.oraclecloud.com')
|
SMTP_ENDPOINT = env('SMTP_ENDPOINT', default='smtp.email.eu-paris-1.oci.oraclecloud.com')
|
||||||
SMTP_PORT = env_int('SMTP_PORT', 587)
|
SMTP_PORT = env.int('SMTP_PORT')
|
||||||
SECURITY = os.getenv('SECURITY', 'tls')
|
SECURITY = env('SECURITY', default='tls')
|
||||||
SMTP_USERNAME = os.getenv('SMTP_USERNAME', None)
|
SMTP_USERNAME = env('SMTP_USERNAME', default=None)
|
||||||
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None)
|
SMTP_PASSWORD = env('SMTP_PASSWORD', default=None)
|
||||||
SMTP_EMAIL = os.getenv("SMTP_EMAIL", None)
|
SMTP_EMAIL = env('SMTP_EMAIL', default=None)
|
||||||
if not RUNNING_TESTS and (SMTP_USERNAME is None or SMTP_PASSWORD is None or SMTP_EMAIL is None):
|
|
||||||
print("Se requieren credenciales SMTP")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'tienda.User'
|
AUTH_USER_MODEL = 'tienda.User'
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = os.getenv("DOMAIN", "localhost")
|
DOMAIN = env('DOMAIN', default='localhost')
|
||||||
PROTOCOL = os.getenv("PROTOCOL", "http")
|
PROTOCOL = env('PROTOCOL', default='http')
|
||||||
|
|
||||||
default_csrf_trusted_origins = []
|
default_csrf_trusted_origins = []
|
||||||
if DOMAIN:
|
if DOMAIN:
|
||||||
@@ -331,16 +286,16 @@ for host in ALLOWED_HOSTS:
|
|||||||
if host and host != '*':
|
if host and host != '*':
|
||||||
default_csrf_trusted_origins.append(f"{PROTOCOL}://{host}")
|
default_csrf_trusted_origins.append(f"{PROTOCOL}://{host}")
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = env_list(
|
CSRF_TRUSTED_ORIGINS = env.list(
|
||||||
'CSRF_TRUSTED_ORIGINS',
|
'CSRF_TRUSTED_ORIGINS',
|
||||||
list(dict.fromkeys(default_csrf_trusted_origins)),
|
default=list(dict.fromkeys(default_csrf_trusted_origins)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
|
LOG_LEVEL = env('LOG_LEVEL', default='INFO').upper()
|
||||||
LOG_DIR = Path(os.getenv('LOG_DIR', BASE_DIR / 'logs'))
|
LOG_DIR = Path(env('LOG_DIR', default=str(BASE_DIR / 'logs')))
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
LOG_FILE = LOG_DIR / os.getenv('LOG_FILE', 'app.log')
|
LOG_FILE = LOG_DIR / env('LOG_FILE', default='app.log')
|
||||||
|
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
@@ -400,8 +355,11 @@ logging.captureWarnings(True)
|
|||||||
|
|
||||||
if RUNNING_TESTS:
|
if RUNNING_TESTS:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
else:
|
elif SMTP_USERNAME and SMTP_PASSWORD and SMTP_EMAIL:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
else:
|
||||||
|
print("ADVERTENCIA: Sin credenciales SMTP - usando backend console")
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
EMAIL_HOST = SMTP_ENDPOINT
|
EMAIL_HOST = SMTP_ENDPOINT
|
||||||
EMAIL_PORT = SMTP_PORT
|
EMAIL_PORT = SMTP_PORT
|
||||||
@@ -411,13 +369,13 @@ EMAIL_HOST_USER = SMTP_USERNAME
|
|||||||
EMAIL_HOST_PASSWORD = SMTP_PASSWORD
|
EMAIL_HOST_PASSWORD = SMTP_PASSWORD
|
||||||
|
|
||||||
# El correo que se usará como remitente por defecto
|
# El correo que se usará como remitente por defecto
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", SMTP_EMAIL)
|
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='') or SMTP_EMAIL or 'no-reply@localhost'
|
||||||
|
|
||||||
# URL de Redis (asumiendo que corre en el puerto default 6379)
|
# URL de Redis (asumiendo que corre en el puerto default 6379)
|
||||||
CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
CELERY_BROKER_URL = env('REDIS_URL', default='redis://localhost:6379/0')
|
||||||
|
|
||||||
# Opcional: para guardar el resultado de las tareas
|
# Opcional: para guardar el resultado de las tareas
|
||||||
CELERY_RESULT_BACKEND = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
CELERY_RESULT_BACKEND = env('REDIS_URL', default='redis://localhost:6379/0')
|
||||||
|
|
||||||
# Configuraciones adicionales recomendadas
|
# Configuraciones adicionales recomendadas
|
||||||
CELERY_ACCEPT_CONTENT = ['json']
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
@@ -428,3 +386,10 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|||||||
|
|
||||||
USE_X_FORWARDED_HOST = True
|
USE_X_FORWARDED_HOST = True
|
||||||
SECURE_REFERER_POLICY = "strict-origin-when-cross-origin"
|
SECURE_REFERER_POLICY = "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
from django.forms.renderers import TemplatesSetting
|
||||||
|
|
||||||
|
class CustomFormRenderer(TemplatesSetting):
|
||||||
|
form_template_name = "tienda/form_snippet.html"
|
||||||
|
|
||||||
|
FORM_RENDERER = "proyecto.settings.CustomFormRenderer"
|
||||||
+7
-1
@@ -19,11 +19,17 @@ from django.conf.urls.static import static
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from tienda import views as tienda_views
|
from tienda import views as tienda_views
|
||||||
|
from tienda.api import router as api_router
|
||||||
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
|
api = NinjaAPI(title="Comercialmeria API", version="1.0.0")
|
||||||
|
api.add_router("/", api_router)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', tienda_views.home, name='home'),
|
path('', tienda_views.home, name='home'),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('tienda/', include('tienda.urls'))
|
path('tienda/', include('tienda.urls')),
|
||||||
|
path('api/', api.urls),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG and (
|
if settings.DEBUG and (
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# UV Config file
|
||||||
|
[project]
|
||||||
|
name = "proyecto-final"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"celery==5.6.3",
|
||||||
|
"Django==6.0.5",
|
||||||
|
"django-compressor==4.6.0",
|
||||||
|
"django-environ>=0.13.0",
|
||||||
|
"django-ninja>=1.6.2",
|
||||||
|
"django-redis==6.0.0",
|
||||||
|
# S3 backend requerido por tienda/storage_backends.py cuando S3_ENABLE=True.
|
||||||
|
"django-storages[s3]==1.14.6",
|
||||||
|
"fpdf2==2.8.7",
|
||||||
|
"gunicorn==26.0.0",
|
||||||
|
"paypalrestsdk==1.13.3",
|
||||||
|
"pillow==12.2.0",
|
||||||
|
"psycopg2-binary==2.9.12",
|
||||||
|
"requests==2.34.2",
|
||||||
|
"stripe==15.2.0",
|
||||||
|
"whitenoise==6.12.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = false
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
amqp==5.3.1
|
|
||||||
asgiref==3.11.0
|
|
||||||
billiard==4.2.4
|
|
||||||
celery==5.6.2
|
|
||||||
certifi==2026.1.4
|
|
||||||
cffi==2.0.0
|
|
||||||
charset-normalizer==3.4.4
|
|
||||||
click==8.3.1
|
|
||||||
click-didyoumean==0.3.1
|
|
||||||
click-plugins==1.1.1.2
|
|
||||||
click-repl==0.3.0
|
|
||||||
cryptography==46.0.7
|
|
||||||
Django==6.0.4
|
|
||||||
django-appconf==1.2.0
|
|
||||||
django-redis==5.4.0
|
|
||||||
django_compressor==4.6.0
|
|
||||||
django-storages[boto3]==1.14.6
|
|
||||||
gunicorn==25.1.0
|
|
||||||
idna==3.11
|
|
||||||
Jinja2==3.1.6
|
|
||||||
kombu==5.6.2
|
|
||||||
MarkupSafe==3.0.3
|
|
||||||
packaging==26.0
|
|
||||||
paypalrestsdk==1.13.3
|
|
||||||
pillow==12.2.0
|
|
||||||
boto3==1.42.97
|
|
||||||
prompt_toolkit==3.0.52
|
|
||||||
pycparser==3.0
|
|
||||||
pyOpenSSL==26.0.0
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
rcssmin==1.2.2
|
|
||||||
redis==5.2.1
|
|
||||||
requests==2.33.0
|
|
||||||
rjsmin==1.2.5
|
|
||||||
six==1.17.0
|
|
||||||
sqlparse==0.5.5
|
|
||||||
stripe==14.3.0
|
|
||||||
typing_extensions==4.15.0
|
|
||||||
tzdata==2025.3
|
|
||||||
tzlocal==5.3.1
|
|
||||||
urllib3==2.6.3
|
|
||||||
vine==5.1.0
|
|
||||||
wcwidth==0.6.0
|
|
||||||
whitenoise==6.12.0
|
|
||||||
fpdf2==2.8.7
|
|
||||||
psycopg2-binary==2.9.11
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
readonly HOSTS=(
|
|
||||||
"aws-docker-mysql"
|
|
||||||
"aws-docker-redis"
|
|
||||||
"aws-docker-celery"
|
|
||||||
"aws-docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly WAIT_SECONDS=5
|
|
||||||
readonly REMOTE_DEPLOY_DIR="/root/deploys"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Uso: $0 {start|stop|restart|update}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_status() {
|
|
||||||
local action="$1"
|
|
||||||
local host="$2"
|
|
||||||
local status="$3"
|
|
||||||
|
|
||||||
# Estilo similar al output de OpenRC.
|
|
||||||
printf "* %-8s %-16s [%s]\n" "$action" "$host" "$status"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_remote_compose() {
|
|
||||||
local host="$1"
|
|
||||||
local command="$2"
|
|
||||||
|
|
||||||
ssh -o BatchMode=yes -o LogLevel=ERROR -T "$host" "sudo -n sh -c \"cd '$REMOTE_DEPLOY_DIR' || exit 1; if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then docker compose $command; elif command -v docker-compose >/dev/null 2>&1; then docker-compose $command; else exit 1; fi\"" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
run_for_all_hosts() {
|
|
||||||
local mode="$1"
|
|
||||||
local host=""
|
|
||||||
local i=0
|
|
||||||
local total=${#HOSTS[@]}
|
|
||||||
|
|
||||||
for host in "${HOSTS[@]}"; do
|
|
||||||
case "$mode" in
|
|
||||||
start)
|
|
||||||
if run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Started" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Started" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if run_remote_compose "$host" "down"; then
|
|
||||||
print_status "Stopped" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Stopped" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
if run_remote_compose "$host" "down" && run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Restarted" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Restarted" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
update)
|
|
||||||
if run_remote_compose "$host" "pull" && run_remote_compose "$host" "down" && run_remote_compose "$host" "up -d"; then
|
|
||||||
print_status "Updated" "$host" "ok"
|
|
||||||
else
|
|
||||||
print_status "Updated" "$host" "fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
i=$((i + 1))
|
|
||||||
if [ "$i" -lt "$total" ]; then
|
|
||||||
sleep "$WAIT_SECONDS"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$#" -ne 1 ]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start|stop|restart|update)
|
|
||||||
run_for_all_hosts "$1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-109
@@ -1,109 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
Script para testear la configuración de PayPal
|
|
||||||
Ejecutar: python test_paypal.py
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import django
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
# Configurar Django solo cuando se ejecuta script manualmente.
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("TEST DE CONFIGURACIÓN DE PAYPAL")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Verificar configuración
|
|
||||||
print("\n1. Verificando configuración en settings.py:")
|
|
||||||
print(f" PAYPAL_MODE: {settings.PAYPAL_MODE}")
|
|
||||||
print(f" PAYPAL_CLIENT_ID: {settings.PAYPAL_CLIENT_ID[:20]}..." if settings.PAYPAL_CLIENT_ID else " ❌ NO CONFIGURADO")
|
|
||||||
print(f" PAYPAL_CLIENT_SECRET: {settings.PAYPAL_CLIENT_SECRET[:20]}..." if settings.PAYPAL_CLIENT_SECRET else " ❌ NO CONFIGURADO")
|
|
||||||
|
|
||||||
# Intentar importar paypalrestsdk
|
|
||||||
print("\n2. Verificando SDK de PayPal:")
|
|
||||||
try:
|
|
||||||
import paypalrestsdk
|
|
||||||
print(" ✓ paypalrestsdk importado correctamente")
|
|
||||||
print(f" Versión: {paypalrestsdk.__version__ if hasattr(paypalrestsdk, '__version__') else 'Desconocida'}")
|
|
||||||
except ImportError as e:
|
|
||||||
print(f" ❌ Error: {e}")
|
|
||||||
print(" SOLUCIÓN: pip install paypalrestsdk")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Intentar conectar a PayPal
|
|
||||||
print("\n3. Probando conexión a PayPal:")
|
|
||||||
try:
|
|
||||||
paypalrestsdk.configure({
|
|
||||||
"mode": settings.PAYPAL_MODE,
|
|
||||||
"client_id": settings.PAYPAL_CLIENT_ID,
|
|
||||||
"client_secret": settings.PAYPAL_CLIENT_SECRET
|
|
||||||
})
|
|
||||||
print(" ✓ Configuración de PayPal aplicada")
|
|
||||||
|
|
||||||
# Intentar crear un pago de prueba
|
|
||||||
print("\n4. Creando pago de prueba:")
|
|
||||||
test_payment = paypalrestsdk.Payment({
|
|
||||||
"intent": "sale",
|
|
||||||
"payer": {
|
|
||||||
"payment_method": "paypal"
|
|
||||||
},
|
|
||||||
"redirect_urls": {
|
|
||||||
"return_url": "http://localhost:8000/test-return",
|
|
||||||
"cancel_url": "http://localhost:8000/test-cancel"
|
|
||||||
},
|
|
||||||
"transactions": [
|
|
||||||
{
|
|
||||||
"amount": {
|
|
||||||
"total": "10.00",
|
|
||||||
"currency": "EUR",
|
|
||||||
"details": {
|
|
||||||
"subtotal": "10.00",
|
|
||||||
"tax": "0",
|
|
||||||
"shipping": "0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Pago de prueba",
|
|
||||||
"item_list": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": "Test Item",
|
|
||||||
"sku": "test_1",
|
|
||||||
"price": "10.00",
|
|
||||||
"currency": "EUR",
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
if test_payment.create():
|
|
||||||
print(" ✓ Pago creado exitosamente")
|
|
||||||
print(f" Payment ID: {test_payment.id}")
|
|
||||||
for link in test_payment.links:
|
|
||||||
if link.rel == "approval_url":
|
|
||||||
print(f" URL de aprobación: {link.href}")
|
|
||||||
else:
|
|
||||||
print(" ❌ Error al crear el pago:")
|
|
||||||
if hasattr(test_payment, 'error') and test_payment.error:
|
|
||||||
print(f" {test_payment.error}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Error de conexión: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("TEST COMPLETADO")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
Script de prueba para el cacheo de productos en Redis
|
|
||||||
Ejecutar: python test_product_cache.py
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import django
|
|
||||||
|
|
||||||
# Configurar Django
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
from tienda.models import Product
|
|
||||||
from django.core.cache import cache
|
|
||||||
import time
|
|
||||||
|
|
||||||
def test_product_cache():
|
|
||||||
"""Prueba el sistema de cacheo de productos"""
|
|
||||||
print("=" * 60)
|
|
||||||
print("TEST: Sistema de Cacheo de Productos en Redis")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Obtener un producto de prueba
|
|
||||||
try:
|
|
||||||
product = Product.objects.first()
|
|
||||||
if not product:
|
|
||||||
print("❌ No hay productos en la base de datos para probar")
|
|
||||||
return
|
|
||||||
|
|
||||||
product_id = product.id
|
|
||||||
cache_key = f'product_{product_id}'
|
|
||||||
|
|
||||||
print(f"\n📦 Producto de prueba: {product.name} (ID: {product_id})")
|
|
||||||
|
|
||||||
# 1. Limpiar caché del producto
|
|
||||||
cache.delete(cache_key)
|
|
||||||
print(f"\n1️⃣ Caché limpiado")
|
|
||||||
|
|
||||||
# 2. Primera visita (debe cargar desde BD)
|
|
||||||
print(f"\n2️⃣ Primera visita - Cargando desde BD...")
|
|
||||||
start_time = time.time()
|
|
||||||
cached_product = cache.get(cache_key)
|
|
||||||
if cached_product is None:
|
|
||||||
print(" ✅ No está en caché (esperado)")
|
|
||||||
product_from_db = Product.objects.select_related('category', 'primary_image', 'creator').prefetch_related('secondary_images').get(id=product_id)
|
|
||||||
cache.set(cache_key, product_from_db, 300)
|
|
||||||
print(f" ✅ Producto cacheado por 5 minutos")
|
|
||||||
db_time = (time.time() - start_time) * 1000
|
|
||||||
|
|
||||||
# 3. Segunda visita (debe cargar desde caché)
|
|
||||||
print(f"\n3️⃣ Segunda visita - Cargando desde caché...")
|
|
||||||
start_time = time.time()
|
|
||||||
cached_product = cache.get(cache_key)
|
|
||||||
if cached_product:
|
|
||||||
print(f" ✅ Encontrado en caché: {cached_product.name}")
|
|
||||||
cache_time = (time.time() - start_time) * 1000
|
|
||||||
|
|
||||||
# 4. Comparar tiempos
|
|
||||||
print(f"\n⏱️ Comparación de rendimiento:")
|
|
||||||
print(f" - Desde BD: {db_time:.2f}ms")
|
|
||||||
print(f" - Desde caché: {cache_time:.2f}ms")
|
|
||||||
speedup = db_time / cache_time if cache_time > 0 else float('inf')
|
|
||||||
print(f" - Mejora: {speedup:.1f}x más rápido")
|
|
||||||
|
|
||||||
# 5. Verificar TTL
|
|
||||||
ttl = cache.ttl(cache_key)
|
|
||||||
print(f"\n⏳ TTL (tiempo de vida): {ttl} segundos (~5 minutos)")
|
|
||||||
|
|
||||||
# 6. Verificar en Redis
|
|
||||||
print(f"\n🔍 Verificación en Redis:")
|
|
||||||
print(f" - Clave: {cache_key}")
|
|
||||||
print(f" - Base de datos: 1")
|
|
||||||
print(f" - Comando para ver: valkey-cli -n 1 GET ':1:{cache_key}'")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("✅ TEST COMPLETADO EXITOSAMENTE")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error durante el test: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_product_cache()
|
|
||||||
+66
-4
@@ -1,17 +1,72 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod
|
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod, Review
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import path
|
||||||
|
from django.contrib import messages
|
||||||
|
from . import tasks
|
||||||
|
|
||||||
admin.site.register(Category)
|
admin.site.register(Category)
|
||||||
admin.site.register(Image)
|
admin.site.register(Image)
|
||||||
admin.site.register(User)
|
|
||||||
admin.site.register(VerificationCode)
|
admin.site.register(VerificationCode)
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('username',)
|
||||||
|
actions = ['banear_usuario_action', 'desbanear_usuario_action']
|
||||||
|
def has_change_permission(self, request, obj = ...):
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
def banear_usuario_action(self, request, queryset):
|
||||||
|
usuarios_baneados = 0
|
||||||
|
for user in queryset:
|
||||||
|
# Desactiva usuario
|
||||||
|
if user.registration_status == User.RegisterStatus.BANNED:
|
||||||
|
continue
|
||||||
|
|
||||||
|
user.is_active = False
|
||||||
|
user.registration_status = User.RegisterStatus.BANNED
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Enviar task a Worker
|
||||||
|
tasks.banear_usuario.delay(user.email)
|
||||||
|
|
||||||
|
# Borrar productos
|
||||||
|
Product.objects.filter(creator=user).delete()
|
||||||
|
usuarios_baneados+=1
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Se ha(n) baneado {usuarios_baneados} usuario(s) correctamente.",
|
||||||
|
level=messages.SUCCESS
|
||||||
|
)
|
||||||
|
def desbanear_usuario_action(self, request, queryset):
|
||||||
|
user_desbaneados = 0
|
||||||
|
for user in queryset:
|
||||||
|
if user.registration_status != User.RegisterStatus.BANNED:
|
||||||
|
continue
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
user.registration_status = User.RegisterStatus.ACTIVE
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
tasks.desbanear_usuario.delay(user.email)
|
||||||
|
|
||||||
|
user_desbaneados -= 1
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Se ha(n) desbaneado {user_desbaneados} usuario(s)",
|
||||||
|
level=messages.SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
banear_usuario_action.short_description = "Banear usuarios seleccionados"
|
||||||
|
desbanear_usuario_action.short_description = "Desbanear usuarios seleccionados"
|
||||||
|
|
||||||
@admin.register(Product)
|
@admin.register(Product)
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'price', 'stock', 'category', 'creator')
|
list_display = ('id', 'sku', 'name', 'price', 'stock', 'category', 'creator')
|
||||||
search_fields = ('name', 'creator__username', 'creator__email')
|
search_fields = ('name', 'sku', 'creator__username', 'creator__email')
|
||||||
list_filter = ('category',)
|
list_filter = ('category',)
|
||||||
class CartItemInline(admin.TabularInline):
|
class CartItemInline(admin.TabularInline):
|
||||||
model = CartItem
|
model = CartItem
|
||||||
@@ -94,3 +149,10 @@ class SavedPaymentMethodAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('id', 'user', 'method_type', 'label', 'is_default', 'created_at')
|
list_display = ('id', 'user', 'method_type', 'label', 'is_default', 'created_at')
|
||||||
list_filter = ('method_type', 'is_default', 'created_at')
|
list_filter = ('method_type', 'is_default', 'created_at')
|
||||||
search_fields = ('user__username', 'user__email', 'label', 'paypal_email')
|
search_fields = ('user__username', 'user__email', 'label', 'paypal_email')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Review)
|
||||||
|
class ReviewAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'product', 'user', 'rating', 'title', 'created_at')
|
||||||
|
list_filter = ('rating', 'created_at')
|
||||||
|
search_fields = ('user__username', 'product__name', 'title', 'content')
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from ninja import Router, Schema
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from .models import Category, Product
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
class CategoryOut(Schema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
product_count: int
|
||||||
|
|
||||||
|
class ImageInfo(Schema):
|
||||||
|
url: str
|
||||||
|
alt: str
|
||||||
|
|
||||||
|
class ProductListOut(Schema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
sku: Optional[str] = None
|
||||||
|
briefdesc: str
|
||||||
|
price: float
|
||||||
|
price_with_vat: float
|
||||||
|
stock: int
|
||||||
|
category_id: int
|
||||||
|
category_name: str
|
||||||
|
primary_image: Optional[ImageInfo] = None
|
||||||
|
average_rating: float
|
||||||
|
reviews_count: int
|
||||||
|
|
||||||
|
class ProductDetailOut(ProductListOut):
|
||||||
|
description: str
|
||||||
|
secondary_images: list[ImageInfo]
|
||||||
|
|
||||||
|
|
||||||
|
def _image_info(img, request):
|
||||||
|
if not img:
|
||||||
|
return None
|
||||||
|
return ImageInfo(
|
||||||
|
url=request.build_absolute_uri(img.image.url),
|
||||||
|
alt=img.alt or img.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _product_to_list_out(p, request):
|
||||||
|
return ProductListOut(
|
||||||
|
id=p.id,
|
||||||
|
name=p.name,
|
||||||
|
sku=p.sku,
|
||||||
|
briefdesc=p.briefdesc,
|
||||||
|
price=p.price,
|
||||||
|
price_with_vat=p.get_price_with_vat(),
|
||||||
|
stock=p.stock,
|
||||||
|
category_id=p.category_id,
|
||||||
|
category_name=p.category.name,
|
||||||
|
primary_image=_image_info(p.primary_image, request),
|
||||||
|
average_rating=p.get_average_rating(),
|
||||||
|
reviews_count=p.get_reviews_count(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _product_to_detail_out(p, request):
|
||||||
|
base = _product_to_list_out(p, request)
|
||||||
|
data = base.dict()
|
||||||
|
data["description"] = p.description
|
||||||
|
data["secondary_images"] = [
|
||||||
|
_image_info(img, request) for img in p.secondary_images.all()
|
||||||
|
]
|
||||||
|
return ProductDetailOut(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/categorias", response=list[CategoryOut])
|
||||||
|
def listar_categorias(request):
|
||||||
|
qs = Category.objects.annotate(product_count=Count("product"))
|
||||||
|
return [
|
||||||
|
CategoryOut(id=c.id, name=c.name, product_count=c.product_count)
|
||||||
|
for c in qs
|
||||||
|
]
|
||||||
|
|
||||||
|
@router.get("/productos", response=list[ProductListOut])
|
||||||
|
def listar_productos(request, categoria_id: Optional[int] = None):
|
||||||
|
qs = Product.objects.select_related("category", "primary_image")
|
||||||
|
if categoria_id:
|
||||||
|
qs = qs.filter(category_id=categoria_id)
|
||||||
|
return [_product_to_list_out(p, request) for p in qs]
|
||||||
|
|
||||||
|
@router.get("/productos/{product_id}", response=ProductDetailOut)
|
||||||
|
def detalle_producto(request, product_id: int):
|
||||||
|
p = get_object_or_404(
|
||||||
|
Product.objects.select_related("category", "primary_image")
|
||||||
|
.prefetch_related("secondary_images"),
|
||||||
|
id=product_id,
|
||||||
|
)
|
||||||
|
return _product_to_detail_out(p, request)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
IMAGE_TYPE = "image/*"
|
||||||
|
EMAIL_FORMNAME = "Correo Electrónico"
|
||||||
|
INCORRECT_PASSWORDS = "Las contraseñas no coinciden"
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import FileExtensionValidator, MinLengthValidator, MaxLengthValidator
|
||||||
|
from .models import Category
|
||||||
|
from .constants import IMAGE_TYPE, EMAIL_FORMNAME, INCORRECT_PASSWORDS
|
||||||
|
ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
||||||
|
ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def validate_image_file(value):
|
||||||
|
ext = value.name.split('.')[-1].lower()
|
||||||
|
if ext not in ALLOWED_IMAGE_EXTENSIONS:
|
||||||
|
raise ValidationError(f'Tipo de archivo no permitido. Allowed: {", ".join(ALLOWED_IMAGE_EXTENSIONS)}')
|
||||||
|
if hasattr(value, 'content_type') and value.content_type not in ALLOWED_MIME_TYPES:
|
||||||
|
raise ValidationError(f'Tipo MIME no permitido. Allowed: {", ".join(ALLOWED_MIME_TYPES)}')
|
||||||
|
|
||||||
|
|
||||||
|
class ProductForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label="Nombre del Producto",
|
||||||
|
max_length=200,
|
||||||
|
required = True,
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Ej: iPhone 15 Pro Max'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
briefdesc = forms.CharField(
|
||||||
|
label="Descripción Breve",
|
||||||
|
max_length=250,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Una descripción corta para mostrar en las tarjetas de producto'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
widget=forms.Textarea(attrs={"rows": "5", "class": "form-control"}),
|
||||||
|
max_length=5000,
|
||||||
|
label="Descripción completa",
|
||||||
|
required = True
|
||||||
|
)
|
||||||
|
price = forms.FloatField(
|
||||||
|
label="Precio (en €)",
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': '15.99'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stock = forms.IntegerField(
|
||||||
|
label="Stock Disponible",
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
category = forms.ModelChoiceField(
|
||||||
|
queryset=Category.objects.all(),
|
||||||
|
label="Categoría",
|
||||||
|
required=True,
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
primary_image = forms.ImageField(
|
||||||
|
label="Imagen Principal",
|
||||||
|
required = False,
|
||||||
|
validators=[validate_image_file],
|
||||||
|
widget = forms.ClearableFileInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': IMAGE_TYPE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductEditForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label="Nombre del Producto",
|
||||||
|
max_length=200,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Ej: iPhone 15 Pro Max'})
|
||||||
|
)
|
||||||
|
briefdesc = forms.CharField(
|
||||||
|
label="Descripción Breve",
|
||||||
|
max_length=250,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Una descripción corta'})
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
widget=forms.Textarea(attrs={"rows": "5", "class": "form-control"}),
|
||||||
|
max_length=5000,
|
||||||
|
label="Descripción completa",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
price = forms.FloatField(
|
||||||
|
label="Precio (en €)",
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '15.99'})
|
||||||
|
)
|
||||||
|
stock = forms.IntegerField(
|
||||||
|
label="Stock Disponible",
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
category = forms.ModelChoiceField(
|
||||||
|
queryset=Category.objects.all(),
|
||||||
|
label="Categoría",
|
||||||
|
required=True,
|
||||||
|
widget=forms.Select(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
primary_image = forms.ImageField(
|
||||||
|
label="Imagen Principal (opcional)",
|
||||||
|
required=False,
|
||||||
|
validators=[validate_image_file],
|
||||||
|
widget=forms.ClearableFileInput(attrs={'class': 'form-control', 'accept': 'image/*'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SecondaryImageForm(forms.Form):
|
||||||
|
image = forms.ImageField(
|
||||||
|
label="Seleccionar Imagen",
|
||||||
|
required = True,
|
||||||
|
validators=[validate_image_file],
|
||||||
|
widget = forms.ClearableFileInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': IMAGE_TYPE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
alt = forms.CharField(
|
||||||
|
label="Texto Alternativo",
|
||||||
|
max_length=255,
|
||||||
|
required = False,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Descripción opcional de la imagen'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoginForm(forms.Form):
|
||||||
|
email = forms.EmailField(
|
||||||
|
label = "Correo Electrónico",
|
||||||
|
max_length=255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Correo Electronico de tu cuenta...'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Contraseña del usuario'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remember = forms.BooleanField(
|
||||||
|
required = False,
|
||||||
|
label = "Recuerdame",
|
||||||
|
widget = forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterForm(forms.Form):
|
||||||
|
name = forms.CharField(
|
||||||
|
label = "Nombre Completo",
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
email = forms.EmailField(
|
||||||
|
label = EMAIL_FORMNAME,
|
||||||
|
max_length = 255,
|
||||||
|
required = True,
|
||||||
|
widget = forms.TextInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password = forms.CharField(
|
||||||
|
label = "Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
min_length = 8,
|
||||||
|
required = True,
|
||||||
|
validators=[MinLengthValidator(8)],
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
password_confirm = forms.CharField(
|
||||||
|
label = "Verificar Contraseña",
|
||||||
|
max_length = 255,
|
||||||
|
min_length = 8,
|
||||||
|
required = True,
|
||||||
|
validators=[MinLengthValidator(8)],
|
||||||
|
widget = forms.PasswordInput(
|
||||||
|
attrs = {
|
||||||
|
'class': 'form-control'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
terms = forms.BooleanField(
|
||||||
|
required = True,
|
||||||
|
label = "Acepto los terminos y condiciones",
|
||||||
|
widget = forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
password = cleaned_data.get("password")
|
||||||
|
password_confirm = cleaned_data.get("password_confirm")
|
||||||
|
if password and password_confirm and password != password_confirm:
|
||||||
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
|
|
||||||
|
|
||||||
|
class EditProfileForm(forms.Form):
|
||||||
|
first_name = forms.CharField(
|
||||||
|
label="Nombre",
|
||||||
|
max_length=150,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
last_name = forms.CharField(
|
||||||
|
label="Apellidos",
|
||||||
|
max_length=150,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
email = forms.EmailField(
|
||||||
|
label=EMAIL_FORMNAME,
|
||||||
|
max_length=254,
|
||||||
|
required=True,
|
||||||
|
widget=forms.EmailInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordForm(forms.Form):
|
||||||
|
current_password = forms.CharField(
|
||||||
|
label="Contraseña Actual",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
new_password = forms.CharField(
|
||||||
|
label="Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
confirm_password = forms.CharField(
|
||||||
|
label="Confirmar Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
new_password = cleaned_data.get("new_password")
|
||||||
|
confirm_password = cleaned_data.get("confirm_password")
|
||||||
|
if new_password and confirm_password and new_password != confirm_password:
|
||||||
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
|
if new_password and len(new_password) < 8:
|
||||||
|
raise ValidationError("La contraseña debe tener al menos 8 caracteres.")
|
||||||
|
|
||||||
|
|
||||||
|
class ShippingAddressForm(forms.Form):
|
||||||
|
full_name = forms.CharField(
|
||||||
|
label="Nombre Completo",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Juan Pérez García'})
|
||||||
|
)
|
||||||
|
address_line_1 = forms.CharField(
|
||||||
|
label="Dirección",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Calle Mayor 123'})
|
||||||
|
)
|
||||||
|
address_line_2 = forms.CharField(
|
||||||
|
label="Dirección (línea 2)",
|
||||||
|
max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Piso, puerta, etc.'})
|
||||||
|
)
|
||||||
|
city = forms.CharField(
|
||||||
|
label="Población",
|
||||||
|
max_length=100,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Almería'})
|
||||||
|
)
|
||||||
|
postal_code = forms.CharField(
|
||||||
|
label="Código Postal",
|
||||||
|
max_length=5,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '04001'})
|
||||||
|
)
|
||||||
|
country = forms.CharField(
|
||||||
|
label="País",
|
||||||
|
max_length=100,
|
||||||
|
required=False,
|
||||||
|
initial="España",
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'readonly': True})
|
||||||
|
)
|
||||||
|
phone = forms.CharField(
|
||||||
|
label="Teléfono",
|
||||||
|
max_length=20,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '612 345 678'})
|
||||||
|
)
|
||||||
|
is_default = forms.BooleanField(
|
||||||
|
label="Establecer como dirección predeterminada",
|
||||||
|
required=False,
|
||||||
|
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordForm(forms.Form):
|
||||||
|
email = forms.EmailField(
|
||||||
|
label=EMAIL_FORMNAME,
|
||||||
|
max_length=254,
|
||||||
|
required=True,
|
||||||
|
widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'tu@email.com'})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordPhase2Form(forms.Form):
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Nueva Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
verify_password = forms.CharField(
|
||||||
|
label="Confirmar Contraseña",
|
||||||
|
max_length=128,
|
||||||
|
required=True,
|
||||||
|
widget=forms.PasswordInput(attrs={'class': 'form-control'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
password = cleaned_data.get("password")
|
||||||
|
verify_password = cleaned_data.get("verify_password")
|
||||||
|
if password and verify_password and password != verify_password:
|
||||||
|
raise ValidationError(INCORRECT_PASSWORDS)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewForm(forms.Form):
|
||||||
|
rating = forms.IntegerField(
|
||||||
|
label="Puntuación",
|
||||||
|
required=True,
|
||||||
|
min_value=1,
|
||||||
|
max_value=5,
|
||||||
|
widget=forms.HiddenInput()
|
||||||
|
)
|
||||||
|
title = forms.CharField(
|
||||||
|
label="Título",
|
||||||
|
max_length=200,
|
||||||
|
required=True,
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Título de tu valoración'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
content = forms.CharField(
|
||||||
|
label="Descripción",
|
||||||
|
max_length=2000,
|
||||||
|
required=True,
|
||||||
|
widget=forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 5,
|
||||||
|
'placeholder': 'Comparte tu experiencia con este producto...'
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.4 on 2026-05-05 07:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0006_alter_category_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='sku',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.4 on 2026-05-07 08:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0007_add_product_sku'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='briefdesc',
|
||||||
|
field=models.TextField(default='', max_length=250),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(default='', max_length=5000),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-08 11:32
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0008_alter_product_briefdesc_alter_product_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cartitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='orderitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockreservationitem',
|
||||||
|
name='quantity',
|
||||||
|
field=models.PositiveIntegerField(default=1, validators=[django.core.validators.MaxValueValidator(9999)]),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Review',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('rating', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(5)])),
|
||||||
|
('title', models.CharField(default='', max_length=200)),
|
||||||
|
('content', models.TextField(default='', max_length=2000)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('images', models.ManyToManyField(blank=True, related_name='product_reviews', to='tienda.image')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='tienda.product')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_reviews', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('product', 'user')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-26 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0009_alter_cartitem_quantity_alter_orderitem_quantity_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cart',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='order',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockreservation',
|
||||||
|
name='session_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=40),
|
||||||
|
),
|
||||||
|
]
|
||||||
+119
-11
@@ -1,10 +1,40 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unicodedata
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User, AbstractUser
|
from django.contrib.auth.models import User, AbstractUser
|
||||||
|
from django.core.validators import MaxValueValidator
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX, TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET
|
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX, TRANSACTION_CODE_LENGTH, TRANSACTION_CODE_ALPHABET
|
||||||
import random, string
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
MAX_QUANTITY = 9999
|
||||||
|
|
||||||
|
|
||||||
|
class BlankToNoneCharField(models.CharField):
|
||||||
|
"""Treat empty strings as None in Python, but store as empty strings in DB."""
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if value is None or value == "":
|
||||||
|
return ""
|
||||||
|
return super().get_prep_value(value)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
path = "django.db.models.CharField"
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
|
||||||
def generate_transaction_code() -> str:
|
def generate_transaction_code() -> str:
|
||||||
@@ -44,9 +74,10 @@ class VerificationCode(models.Model):
|
|||||||
default = VerificationModes.VERIFY_ACCOUNT
|
default = VerificationModes.VERIFY_ACCOUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def generate(user: User, code_mode: str) -> VerificationCode:
|
def generate(user: User, code_mode: str) -> VerificationCode:
|
||||||
while True:
|
while True:
|
||||||
code = "".join(random.choices(string.ascii_letters+string.digits, k=64))
|
code = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
if not VerificationCode.objects.filter(code=code).exists():
|
if not VerificationCode.objects.filter(code=code).exists():
|
||||||
return VerificationCode.objects.create(
|
return VerificationCode.objects.create(
|
||||||
code = code,
|
code = code,
|
||||||
@@ -83,8 +114,9 @@ class Image(models.Model):
|
|||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
name = models.CharField(max_length=200, default="")
|
name = models.CharField(max_length=200, default="")
|
||||||
description = models.TextField(default = "")
|
sku = models.CharField(max_length=50, unique=True, blank=True, null=True)
|
||||||
briefdesc = models.TextField(default = "")
|
description = models.TextField(default = "", max_length=5000)
|
||||||
|
briefdesc = models.TextField(default = "", max_length=250)
|
||||||
price = models.FloatField(default = 0)
|
price = models.FloatField(default = 0)
|
||||||
stock = models.PositiveIntegerField(default=0)
|
stock = models.PositiveIntegerField(default=0)
|
||||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||||
@@ -106,6 +138,7 @@ class Product(models.Model):
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
|
"sku": self.sku,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"briefdesc": self.briefdesc,
|
"briefdesc": self.briefdesc,
|
||||||
"price": self.price,
|
"price": self.price,
|
||||||
@@ -116,6 +149,26 @@ class Product(models.Model):
|
|||||||
"creator": self.creator.to_dict() if self.creator else None
|
"creator": self.creator.to_dict() if self.creator else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def has_user_purchased(self, user):
|
||||||
|
"""Verifica si el usuario ha comprado este producto al menos una vez"""
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
return OrderItem.objects.filter(
|
||||||
|
order__buyer=user,
|
||||||
|
product=self
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def get_average_rating(self):
|
||||||
|
"""Retorna la nota media de las valoraciones"""
|
||||||
|
reviews = self.reviews.all()
|
||||||
|
if not reviews.exists():
|
||||||
|
return 0
|
||||||
|
return round(reviews.aggregate(models.Avg('rating'))['rating__avg'], 1)
|
||||||
|
|
||||||
|
def get_reviews_count(self):
|
||||||
|
"""Retorna el número total de valoraciones"""
|
||||||
|
return self.reviews.count()
|
||||||
|
|
||||||
|
|
||||||
class StockReservation(models.Model):
|
class StockReservation(models.Model):
|
||||||
STATUS_ACTIVE = "active"
|
STATUS_ACTIVE = "active"
|
||||||
@@ -137,7 +190,7 @@ class StockReservation(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="stock_reservations")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="stock_reservations")
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = models.CharField(max_length=40, default="", blank=True)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
|
||||||
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES)
|
||||||
expires_at = models.DateTimeField(db_index=True)
|
expires_at = models.DateTimeField(db_index=True)
|
||||||
@@ -151,21 +204,33 @@ class StockReservation(models.Model):
|
|||||||
class StockReservationItem(models.Model):
|
class StockReservationItem(models.Model):
|
||||||
reservation = models.ForeignKey(StockReservation, on_delete=models.CASCADE, related_name="items")
|
reservation = models.ForeignKey(StockReservation, on_delete=models.CASCADE, related_name="items")
|
||||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="stock_reservation_items")
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="stock_reservation_items")
|
||||||
quantity = models.PositiveIntegerField(default=1)
|
quantity = models.PositiveIntegerField(default=1, validators=[MaxValueValidator(MAX_QUANTITY)])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("reservation", "product")
|
unique_together = ("reservation", "product")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
if self.quantity is not None and self.quantity > MAX_QUANTITY:
|
||||||
|
raise ValidationError(f'La cantidad no puede exceder {MAX_QUANTITY} unidades.')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.quantity}x {self.product.name} (reserva {self.reservation_id})"
|
return f"{self.quantity}x {self.product.name} (reserva {self.reservation_id})"
|
||||||
|
|
||||||
|
|
||||||
class Cart(models.Model):
|
class Cart(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = BlankToNoneCharField(max_length=40, default="", blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.session_key is None:
|
||||||
|
self.session_key = ""
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if self.session_key == "":
|
||||||
|
self.session_key = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Cart {self.id} - {self.user or self.session_key}"
|
return f"Cart {self.id} - {self.user or self.session_key}"
|
||||||
|
|
||||||
@@ -187,7 +252,7 @@ class Cart(models.Model):
|
|||||||
class CartItem(models.Model):
|
class CartItem(models.Model):
|
||||||
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
|
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
|
||||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
quantity = models.PositiveIntegerField(default=1)
|
quantity = models.PositiveIntegerField(default=1, validators=[MaxValueValidator(MAX_QUANTITY)])
|
||||||
added_at = models.DateTimeField(auto_now_add=True)
|
added_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -227,7 +292,7 @@ class Order(models.Model):
|
|||||||
|
|
||||||
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = models.CharField(max_length=40, default="", blank=True)
|
||||||
total = models.FloatField(default=0)
|
total = models.FloatField(default=0)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
||||||
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
|
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
|
||||||
@@ -262,7 +327,7 @@ class OrderItem(models.Model):
|
|||||||
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, blank=True)
|
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
product_name = models.CharField(max_length=200, default="")
|
product_name = models.CharField(max_length=200, default="")
|
||||||
seller = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='order_items_to_fulfill')
|
seller = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='order_items_to_fulfill')
|
||||||
quantity = models.PositiveIntegerField(default=1)
|
quantity = models.PositiveIntegerField(default=1, validators=[MaxValueValidator(MAX_QUANTITY)])
|
||||||
unit_price = models.FloatField(default=0)
|
unit_price = models.FloatField(default=0)
|
||||||
total_price = models.FloatField(default=0)
|
total_price = models.FloatField(default=0)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
|
||||||
@@ -320,6 +385,25 @@ class SavedPaymentMethod(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Review(models.Model):
|
||||||
|
"""Valoraciones de productos por usuarios que han realizado una compra"""
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='product_reviews')
|
||||||
|
rating = models.PositiveIntegerField(validators=[MaxValueValidator(5)])
|
||||||
|
title = models.CharField(max_length=200, default="")
|
||||||
|
content = models.TextField(max_length=2000, default="")
|
||||||
|
images = models.ManyToManyField(Image, related_name='product_reviews', blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('product', 'user')
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Valoración de {self.user.username} en {self.product.name} ({self.rating}★)"
|
||||||
|
|
||||||
|
|
||||||
class ShippingAddress(models.Model):
|
class ShippingAddress(models.Model):
|
||||||
"""Direcciones de entrega de los usuarios"""
|
"""Direcciones de entrega de los usuarios"""
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
|
||||||
@@ -342,8 +426,32 @@ class ShippingAddress(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.full_name} - {self.city}"
|
return f"{self.full_name} - {self.city}"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
from .vars import ALMERIA_POSTAL_CODE_PREFIX, ALMERIA_MUNICIPALITIES_DISPLAY
|
||||||
|
from .views import _normalize_location_text
|
||||||
|
|
||||||
|
postal_code = (self.postal_code or "").strip()
|
||||||
|
city = (self.city or "").strip()
|
||||||
|
|
||||||
|
almeria_prefix = getattr(settings, 'POSTGRES_ENABLED', False) and "04" or ALMERIA_POSTAL_CODE_PREFIX
|
||||||
|
|
||||||
|
if len(postal_code) != 5 or not postal_code.isdigit() or not postal_code.startswith(almeria_prefix):
|
||||||
|
raise ValidationError({
|
||||||
|
'postal_code': 'Solo realizamos envíos en la provincia de Almería (código postal 04xxx).'
|
||||||
|
})
|
||||||
|
|
||||||
|
normalized_city = _normalize_location_text(city)
|
||||||
|
normalized_municipalities = {_normalize_location_text(m) for m in ALMERIA_MUNICIPALITIES_DISPLAY}
|
||||||
|
|
||||||
|
if normalized_city not in normalized_municipalities:
|
||||||
|
raise ValidationError({
|
||||||
|
'city': 'El pueblo/ciudad debe pertenecer a la provincia de Almería.'
|
||||||
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Si se marca como predeterminada, desmarcar las demás del usuario
|
self.full_clean()
|
||||||
if self.is_default:
|
if self.is_default:
|
||||||
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
|
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
+5
-2
@@ -17,15 +17,18 @@ class Recibo(FPDF):
|
|||||||
|
|
||||||
def generar_recibo(cliente: str, total: float, objetos: list, metodo_pago: str, transaction_code: str):
|
def generar_recibo(cliente: str, total: float, objetos: list, metodo_pago: str, transaction_code: str):
|
||||||
pdf = Recibo()
|
pdf = Recibo()
|
||||||
font_path = "/fonts/Roboto-Regular.ttf"
|
|
||||||
pdf.add_font('Roboto', '', '/fonts/Roboto-Regular.ttf')
|
pdf.add_font('Roboto', '', '/fonts/Roboto-Regular.ttf')
|
||||||
pdf.add_font('Roboto', 'B', '/fonts/Roboto-Bold.ttf')
|
pdf.add_font('Roboto', 'B', '/fonts/Roboto-Bold.ttf')
|
||||||
pdf.add_page()
|
pdf.add_page()
|
||||||
pdf.set_font('Roboto', size=12)
|
pdf.set_font('Roboto', size=12)
|
||||||
|
|
||||||
|
METODOS_MAP = {"stripe": "Stripe", "paypal": "PayPal", "manual": "Manual"}
|
||||||
|
metodo_mostrar = METODOS_MAP.get(metodo_pago, metodo_pago)
|
||||||
|
|
||||||
pdf.cell(0, 10, f"Cliente: {cliente}", ln=True)
|
pdf.cell(0, 10, f"Cliente: {cliente}", ln=True)
|
||||||
pdf.cell(0, 10, f"ID de transaccion: {transaction_code}", ln=True)
|
pdf.cell(0, 10, f"ID de transaccion: {transaction_code}", ln=True)
|
||||||
pdf.cell(0, 10, f"")
|
pdf.cell(0, 10, f"Metodo de pago: {metodo_mostrar}", ln=True)
|
||||||
|
pdf.cell(0, 10, "")
|
||||||
|
|
||||||
DATA = []
|
DATA = []
|
||||||
DATA.append(
|
DATA.append(
|
||||||
|
|||||||
@@ -315,5 +315,26 @@ p.price {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.texto-ajustado {
|
.texto-ajustado {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--chat--color--primary: #513CB0;
|
||||||
|
--chat--color--primary-shade-50: #3f2a8f;
|
||||||
|
--chat--color--primary--shade-100: #361DA7;
|
||||||
|
--chat--color--secondary: #513CB0;
|
||||||
|
--chat--color-secondary-shade-50: #3f2a8f;
|
||||||
|
--chat--color--typing: #513CB0;
|
||||||
|
--chat--color-dark: #101330;
|
||||||
|
--chat--window--border-radius: 12px;
|
||||||
|
--chat--toggle--background: #513CB0;
|
||||||
|
--chat--toggle--hover--background: #3f2a8f;
|
||||||
|
--chat--toggle--active--background: #361DA7;
|
||||||
|
--chat--message--bot--background: #f0f0f0;
|
||||||
|
--chat--message--user--background: #513CB0;
|
||||||
|
--chat--header--background: #513CB0;
|
||||||
|
--chat--input--send--button--color: #513CB0;
|
||||||
|
--chat--input--send--button--color-hover: #3f2a8f;
|
||||||
|
--chat--close--button--color-hover: #FC3F44;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-7
@@ -4,28 +4,47 @@ from django.template.loader import render_to_string
|
|||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from .utilities import send_email, send_hemail
|
from .utilities import send_email, send_hemail
|
||||||
from .vars import login_message, verify_message
|
from .vars import login_message, verify_message
|
||||||
import random, string
|
import secrets
|
||||||
|
import string
|
||||||
from . import pdf
|
from . import pdf
|
||||||
|
|
||||||
from .models import User, VerificationCode
|
from .models import User, VerificationCode
|
||||||
@shared_task
|
@shared_task
|
||||||
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/welcome.html',
|
'tienda/emails/welcome.html',
|
||||||
{
|
{
|
||||||
"name": nombre_usuario
|
"name": nombre_usuario
|
||||||
},
|
},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
send_hemail(email_usuario, "Inicio de Sesión correcto", html_content, "Has iniciado sesión...")
|
send_hemail(email_usuario, "Inicio de Sesión correcto", html_content, "Has iniciado sesión...")
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def banear_usuario(email_usuario: str):
|
||||||
|
html_content = render_to_string(
|
||||||
|
'tienda/emails/ban.html',
|
||||||
|
{
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
send_hemail(email_usuario, "Cuenta Bloqueada", html_content, "Tu cuenta ha sido bloqueada...")
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def desbanear_usuario(email_usuario: str):
|
||||||
|
html_content = render_to_string(
|
||||||
|
'tienda/emails/unban.html',
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
send_hemail(email_usuario, "Cuenta Desbloqueada", html_content, "Tu cuenta ha sido desbloqueada...")
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def enviar_correo_confirmacion(id: int):
|
def enviar_correo_confirmacion(id: int):
|
||||||
usuario = User.objects.get(id=id)
|
usuario = User.objects.get(id=id)
|
||||||
code = VerificationCode.objects.create(
|
code = VerificationCode.objects.create(
|
||||||
user = usuario,
|
user = usuario,
|
||||||
code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
||||||
code = ''.join(random.choices(string.digits, k=12))
|
code = ''.join(secrets.choice(string.digits) for _ in range(12))
|
||||||
)
|
)
|
||||||
|
|
||||||
message = verify_message.format(name = usuario.get_full_name(), protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = code.code)
|
message = verify_message.format(name = usuario.get_full_name(), protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = code.code)
|
||||||
@@ -42,18 +61,17 @@ def enviar_correo_recuperacion(email: str):
|
|||||||
ver_code = VerificationCode.objects.create(
|
ver_code = VerificationCode.objects.create(
|
||||||
code_mode = VerificationCode.VerificationModes.RESET_PASSWORD,
|
code_mode = VerificationCode.VerificationModes.RESET_PASSWORD,
|
||||||
user = usuario,
|
user = usuario,
|
||||||
code = ''.join(random.choices(string.digits, k=12))
|
code = ''.join(secrets.choice(string.digits) for _ in range(12))
|
||||||
)
|
)
|
||||||
ver_code.save()
|
ver_code.save()
|
||||||
html_content = render_to_string(
|
html_content = render_to_string(
|
||||||
'emails/reset_pass.html',
|
'tienda/emails/reset_pass.html',
|
||||||
{
|
{
|
||||||
"name": usuario.get_full_name(),
|
"name": usuario.get_full_name(),
|
||||||
"domain": settings.DOMAIN,
|
"domain": settings.DOMAIN,
|
||||||
"protocol": settings.PROTOCOL,
|
"protocol": settings.PROTOCOL,
|
||||||
"code": ver_code.code
|
"code": ver_code.code
|
||||||
},
|
},
|
||||||
using='jinja2'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{% extends "tienda/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'index' %}">Inicio</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'producto' product.id %}">{{ product.name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Valorar Producto</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">
|
||||||
|
{% if existing_review %}Actualizar{% else %}Añadir{% endif %} valoración: {{ product.name }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label" for="rating-input">
|
||||||
|
Puntuación
|
||||||
|
<div class="star-rating d-flex gap-1" id="star-rating">
|
||||||
|
{% for i in "12345" %}
|
||||||
|
<span class="star fs-2 {% if form.initial.rating|default:0 >= i|add:0 %}text-warning text-dark{% else %}text-secondary{% endif %}" data-value="{{ i }}" style="cursor: pointer; font-size: 2rem;">★</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="rating" id="rating-input" value="{{ form.initial.rating|default:1 }}">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% if form.rating.errors %}
|
||||||
|
<div class="text-danger small">{{ form.rating.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Título</label>
|
||||||
|
{{ form.title }}
|
||||||
|
{% if form.title.errors %}
|
||||||
|
<div class="text-danger small">{{ form.title.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Descripción</label>
|
||||||
|
{{ form.content }}
|
||||||
|
{% if form.content.errors %}
|
||||||
|
<div class="text-danger small">{{ form.content.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% if existing_review %}Actualizar{% else %}Enviar{% endif %} valoración
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'producto' product.id %}" class="btn btn-outline-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const stars = document.querySelectorAll('#star-rating .star');
|
||||||
|
const ratingInput = document.getElementById('rating-input');
|
||||||
|
|
||||||
|
function updateStars(value) {
|
||||||
|
stars.forEach((star, index) => {
|
||||||
|
if (index < value) {
|
||||||
|
star.classList.remove('text-secondary');
|
||||||
|
star.classList.add('text-warning');
|
||||||
|
} else {
|
||||||
|
star.classList.remove('text-warning');
|
||||||
|
star.classList.add('text-secondary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ratingInput.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
stars.forEach(star => {
|
||||||
|
star.addEventListener('click', function() {
|
||||||
|
const value = Number.parseInt(this.dataset.value);
|
||||||
|
updateStars(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
star.addEventListener('mouseenter', function() {
|
||||||
|
const value = Number.parseInt(this.dataset.value);
|
||||||
|
stars.forEach((s, index) => {
|
||||||
|
if (index < value) {
|
||||||
|
s.classList.remove('text-secondary');
|
||||||
|
s.classList.add('text-warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
star.addEventListener('mouseleave', function() {
|
||||||
|
updateStars(Number.parseInt(ratingInput.value) || 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="https://js.stripe.com/v3/"></script>
|
<script src="https://js.stripe.com/v3/" integrity="sha384-353f1ae25ae0929bea5f9379a594131b27e45a89d8f918dcc040c4ccbe6fd35fe6fd1d61ccc6e0c911c9b54325235904"></script>
|
||||||
<style>
|
<style>
|
||||||
#card-element {
|
#card-element {
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid #ced4da;
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Datos de la tarjeta</label>
|
<label id="label-card-data" class="form-label">Datos de la tarjeta <input type="hidden"></label>
|
||||||
<div id="card-element"></div>
|
<div id="card-element" aria-labelledby="label-card-data"></div>
|
||||||
<div id="card-errors" role="alert"></div>
|
<div id="card-errors" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Sitio web de comercio local Almeriense">
|
|
||||||
<title>Comercialmeria</title>
|
<title>Comercialmeria</title>
|
||||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
<meta name="description" content="Sitio web de comercio local Almeriense">
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
|
||||||
</noscript>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
<link rel="preload" href="{% static 'css/custom.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
<link rel="preload" href="{% static 'css/custom.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
<noscript><link rel="stylesheet" href="{% static 'css/custom.css' %}"></noscript>
|
<noscript><link rel="stylesheet" href="{% static 'css/custom.css' %}"></noscript>
|
||||||
@@ -111,8 +106,8 @@
|
|||||||
<!-- Barra de búsqueda con sugerencias -->
|
<!-- Barra de búsqueda con sugerencias -->
|
||||||
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
|
<form class="search-suggestions-container" method="GET" action="{% url 'search' %}" role="search" id="searchForm">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" type="search" name="q" id="searchInput" placeholder="Buscar productos..." aria-label="Buscar" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="searchSuggestions" aria-activedescendant="" aria-haspopup="listbox">
|
<input class="form-control" type="search" name="q" id="searchInput" placeholder="Buscar productos..." aria-label="Buscar" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="searchSuggestions" aria-activedescendant="searchbutton" aria-haspopup="listbox">
|
||||||
<button class="btn btn-outline-primary" type="submit" aria-label="Buscar productos">🔍 Buscar</button>
|
<button class="btn btn-outline-primary" type="submit" id="searchbutton" aria-label="Buscar productos">🔍 Buscar</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
|
<div class="search-suggestions" id="searchSuggestions" role="listbox" aria-label="Sugerencias de búsqueda"></div>
|
||||||
</form>
|
</form>
|
||||||
@@ -344,5 +339,27 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endcache %}
|
{% endcache %}
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css" rel="stylesheet" integrity="sha384-b7166c239e461f42296ad7248c04ef6768e9340a51aef45fad197acf8f4c16f119f36376b19516548885a9ecabdccc10" />
|
||||||
|
<script type="module">
|
||||||
|
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js';
|
||||||
|
|
||||||
|
createChat({
|
||||||
|
webhookUrl: 'https://n8n.elordenador.org/webhook/0e2cbe42-39d2-4e86-be62-c12542e246d4/chat',
|
||||||
|
initialMessages: [
|
||||||
|
'¡Hola! 👋',
|
||||||
|
'Soy el asistente virtual de Comercialmeria. ¿En qué puedo ayudarte?'
|
||||||
|
],
|
||||||
|
i18n: {
|
||||||
|
en: {
|
||||||
|
title: 'Chat de Soporte',
|
||||||
|
subtitle: 'Estamos aquí para ayudarte 24/7.',
|
||||||
|
footer: '',
|
||||||
|
getStarted: 'Nueva conversación',
|
||||||
|
inputPlaceholder: 'Escribe tu mensaje...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% load vat_filters %}
|
{% load vat_filters %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="https://js.stripe.com/v3/"></script>
|
<script src="https://js.stripe.com/v3/" integrity="sha384-353f1ae25ae0929bea5f9379a594131b27e45a89d8f918dcc040c4ccbe6fd35fe6fd1d61ccc6e0c911c9b54325235904"></script>
|
||||||
<script src="https://www.paypal.com/sdk/js?client-id={{ paypal_client_id }}¤cy=EUR" defer></script>
|
<script src="https://www.paypal.com/sdk/js?client-id={{ paypal_client_id }}¤cy=EUR" defer></script>
|
||||||
<style>
|
<style>
|
||||||
#card-element {
|
#card-element {
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Producto</th>
|
<th scope="col">Producto</th>
|
||||||
<th class="text-end">Precio (sin IVA)</th>
|
<th scope="col" class="text-end">Precio (sin IVA)</th>
|
||||||
<th class="text-end">Cantidad</th>
|
<th scope="col" class="text-end">Cantidad</th>
|
||||||
<th class="text-end">Stock actual</th>
|
<th scope="col" class="text-end">Stock actual</th>
|
||||||
<th class="text-end">Subtotal (con IVA)</th>
|
<th scope="col" class="text-end">Subtotal (con IVA)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -104,16 +104,16 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" class="text-end">Subtotal:</th>
|
<th colspan="4" scope="row" class="text-end">Subtotal:</th>
|
||||||
<th class="text-end">{{ cart.get_total|format_price }}€</th>
|
<td class="text-end">{{ cart.get_total|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" class="text-end">IVA (21%):</th>
|
<th scope="row" colspan="4" class="text-end">IVA (21%):</th>
|
||||||
<th class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
<td class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="background-color: #f8f9fa;">
|
<tr style="background-color: #f8f9fa;">
|
||||||
<th colspan="4" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
<th scope="row" colspan="4" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
||||||
<th class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
<td class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<h5 class="card-title mb-3">2) Selecciona tu método de pago</h5>
|
<h5 class="card-title mb-3">2) Selecciona tu método de pago</h5>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<ul class="nav nav-tabs mb-3" id="paymentTabs" role="tablist">
|
<ul class="nav nav-tabs mb-3" id="paymentTabs">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button"
|
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button"
|
||||||
role="tab" aria-selected="true" aria-controls="pane-card" tabindex="0">
|
role="tab" aria-selected="true" aria-controls="pane-card" tabindex="0">
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
<!-- Tarjeta tab -->
|
<!-- Tarjeta tab -->
|
||||||
<div id="pane-card" class="payment-tab-content active"
|
<div id="pane-card" class="payment-tab-content active"
|
||||||
role="tabpanel" aria-labelledby="tab-card" tabindex="0">
|
role="tabpanel" aria-labelledby="tab-card">
|
||||||
{% if saved_cards %}
|
{% if saved_cards %}
|
||||||
<fieldset class="mb-3">
|
<fieldset class="mb-3">
|
||||||
<legend class="fw-semibold fs-6 mb-2">Selección de tarjeta</legend>
|
<legend class="fw-semibold fs-6 mb-2">Selección de tarjeta</legend>
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
|
|
||||||
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
|
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Número de tarjeta</label>
|
<label id="label-card-number" class="form-label">Número de tarjeta <input type="hidden"></label>
|
||||||
<div id="card-element"></div>
|
<div id="card-element" aria-labelledby="label-card-number"></div>
|
||||||
<div id="card-errors" role="alert"></div>
|
<div id="card-errors" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
|
|
||||||
<!-- PayPal tab -->
|
<!-- PayPal tab -->
|
||||||
<div id="pane-paypal" class="payment-tab-content"
|
<div id="pane-paypal" class="payment-tab-content"
|
||||||
role="tabpanel" aria-labelledby="tab-paypal" tabindex="0">
|
role="tabpanel" aria-labelledby="tab-paypal">
|
||||||
{% if saved_paypal %}
|
{% if saved_paypal %}
|
||||||
<div class="alert alert-light border mb-3">
|
<div class="alert alert-light border mb-3">
|
||||||
<small class="text-muted">Cuenta PayPal guardada:</small>
|
<small class="text-muted">Cuenta PayPal guardada:</small>
|
||||||
|
|||||||
@@ -13,74 +13,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
<!-- Nombre del producto -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Nombre del Producto <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="name" name="name" required maxlength="200"
|
|
||||||
placeholder="Ej: iPhone 15 Pro Max">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción breve -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="briefdesc" class="form-label">Descripción Breve</label>
|
|
||||||
<input type="text" class="form-control" id="briefdesc" name="briefdesc" maxlength="250"
|
|
||||||
placeholder="Una descripción corta para mostrar en las tarjetas de producto">
|
|
||||||
<div class="form-text">Opcional. Se mostrará en las vistas de listado de productos.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción completa -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="description" class="form-label">Descripción Completa <span class="text-danger">*</span></label>
|
|
||||||
<textarea class="form-control" id="description" name="description" rows="5" required
|
|
||||||
placeholder="Describe tu producto en detalle..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Precio -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="price" class="form-label">Precio <span class="text-danger">*</span></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
<input type="number" class="form-control" id="price" name="price" required
|
|
||||||
min="0" step="0.01" placeholder="0.00">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
|
|
||||||
<input type="number" class="form-control" id="stock" name="stock" required
|
|
||||||
min="0" step="1" placeholder="0">
|
|
||||||
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoría -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" id="category" name="category" required>
|
|
||||||
<option value="" selected disabled>Selecciona una categoría</option>
|
|
||||||
{% for category in categories %}
|
|
||||||
<option value="{{ category.id }}">{{ category.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imagen principal -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="primary_image" class="form-label">Imagen Principal</label>
|
|
||||||
<input type="file" class="form-control" id="primary_image" name="primary_image"
|
|
||||||
accept="image/*">
|
|
||||||
<div class="form-text">Opcional. Esta será la imagen destacada del producto.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imágenes secundarias -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
|
||||||
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
|
||||||
accept="image/*" multiple>
|
|
||||||
<div class="form-text">Opcional. Puedes seleccionar múltiples imágenes adicionales.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botones -->
|
<!-- Botones -->
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
|||||||
@@ -21,52 +21,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ form.as_p }}
|
||||||
<label for="full_name" class="form-label">Nombre Completo *</label>
|
|
||||||
<input type="text" class="form-control" id="full_name" name="full_name" value="{{ direccion.full_name|default:'' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="address_line_1" class="form-label">Dirección *</label>
|
|
||||||
<input type="text" class="form-control" id="address_line_1" name="address_line_1" value="{{ direccion.address_line_1|default:'' }}" placeholder="Calle, número, piso, puerta" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="address_line_2" class="form-label">Dirección (línea 2)</label>
|
|
||||||
<input type="text" class="form-control" id="address_line_2" name="address_line_2" value="{{ direccion.address_line_2|default:'' }}" placeholder="Edificio, bloque, etc. (opcional)">
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="city" class="form-label">Ciudad/Pueblo (Almería) *</label>
|
|
||||||
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" list="almeria-towns" autocomplete="off" required>
|
|
||||||
<datalist id="almeria-towns">
|
|
||||||
{% for town in almeria_municipalities %}
|
|
||||||
<option value="{{ town }}"></option>
|
|
||||||
{% endfor %}
|
|
||||||
</datalist>
|
|
||||||
<div class="form-text">Selecciona o escribe un municipio de la provincia de Almería.</div>
|
|
||||||
<div class="invalid-feedback" id="city-validation-message">
|
|
||||||
El pueblo/ciudad debe pertenecer a la provincia de Almería.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="postal_code" class="form-label">Código Postal *</label>
|
|
||||||
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" pattern="04[0-9]{3}" maxlength="5" placeholder="04XXX" required>
|
|
||||||
<div class="form-text">Solo aceptamos códigos postales de Almería (04xxx).</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="country" class="form-label">País *</label>
|
|
||||||
<input type="text" class="form-control" id="country" name="country" value="España" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="phone" class="form-label">Teléfono *</label>
|
|
||||||
<input type="tel" class="form-control" id="phone" name="phone" value="{{ direccion.phone|default:'' }}" placeholder="+34 600 000 000" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="is_default" name="is_default" {% if direccion.is_default %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="is_default">
|
|
||||||
Establecer como dirección predeterminada
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">{% if direccion %}Actualizar{% else %}Crear{% endif %} Dirección</button>
|
<button type="submit" class="btn btn-primary">{% if direccion %}Actualizar{% else %}Crear{% endif %} Dirección</button>
|
||||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-secondary">Cancelar</a>
|
<a href="{% url 'direcciones_usuario' %}" class="btn btn-secondary">Cancelar</a>
|
||||||
@@ -80,7 +35,6 @@
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const cityInput = document.getElementById('city');
|
const cityInput = document.getElementById('city');
|
||||||
const cityValidationMessage = document.getElementById('city-validation-message');
|
|
||||||
const form = cityInput ? cityInput.form : null;
|
const form = cityInput ? cityInput.form : null;
|
||||||
|
|
||||||
if (!cityInput || !form) {
|
if (!cityInput || !form) {
|
||||||
@@ -123,8 +77,6 @@
|
|||||||
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
||||||
cityInput.classList.add('is-invalid');
|
cityInput.classList.add('is-invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cityInput.addEventListener('input', validateTown);
|
cityInput.addEventListener('input', validateTown);
|
||||||
|
|||||||
@@ -37,18 +37,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ form.as_p }}
|
||||||
<label for="first_name" class="form-label">Nombre</label>
|
|
||||||
<input type="text" class="form-control" id="first_name" name="first_name" value="{{ user.first_name }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="last_name" class="form-label">Apellidos</label>
|
|
||||||
<input type="text" class="form-control" id="last_name" name="last_name" value="{{ user.last_name }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||||
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
||||||
@@ -69,19 +58,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{ password_form.as_p }}
|
||||||
<label for="current_password" class="form-label">Contraseña Actual</label>
|
|
||||||
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="new_password" class="form-label">Nueva Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
|
||||||
<small class="text-muted">Mínimo 8 caracteres</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="confirm_password" class="form-label">Confirmar Nueva Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,67 +13,9 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
<!-- Nombre del producto -->
|
<!-- Imágenes secundarias (no incluidas en el form) -->
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Nombre del Producto <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" class="form-control" id="name" name="name" required maxlength="200"
|
|
||||||
value="{{ producto.name }}" placeholder="Ej: iPhone 15 Pro Max">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción breve -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="briefdesc" class="form-label">Descripción Breve</label>
|
|
||||||
<input type="text" class="form-control" id="briefdesc" name="briefdesc" maxlength="250"
|
|
||||||
value="{{ producto.briefdesc }}" placeholder="Una descripción corta para mostrar en las tarjetas de producto">
|
|
||||||
<div class="form-text">Opcional. Se mostrará en las vistas de listado de productos.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción completa -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="description" class="form-label">Descripción Completa <span class="text-danger">*</span></label>
|
|
||||||
<textarea class="form-control" id="description" name="description" rows="5" required
|
|
||||||
placeholder="Describe tu producto en detalle...">{{ producto.description }}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Precio -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="price" class="form-label">Precio <span class="text-danger">*</span></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
<input type="number" class="form-control" id="price" name="price" required
|
|
||||||
min="0" step="0.01" value="{{ producto.price }}" placeholder="0.00">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
|
|
||||||
<input type="number" class="form-control" id="stock" name="stock" required
|
|
||||||
min="0" step="1" value="{{ producto.stock }}" placeholder="0">
|
|
||||||
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoría -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" id="category" name="category" required>
|
|
||||||
<option value="" disabled>Selecciona una categoría</option>
|
|
||||||
{% for category in categories %}
|
|
||||||
<option value="{{ category.id }}" {% if producto.category.id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imagen principal -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="primary_image" class="form-label">Imagen Principal</label>
|
|
||||||
<input type="file" class="form-control" id="primary_image" name="primary_image"
|
|
||||||
accept="image/*">
|
|
||||||
<div class="form-text">Opcional. Si subes una nueva, reemplazará la actual.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Imágenes secundarias -->
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
<label for="secondary_images" class="form-label">Imágenes Secundarias</label>
|
||||||
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
<input type="file" class="form-control" id="secondary_images" name="secondary_images"
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<th align="center" style="padding: 20px;">
|
||||||
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido bloqueada</h1>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 40px">
|
||||||
|
<svg fill="#FF0000" width="128px" height="128px" viewBox="-3.2 -3.2 38.40 38.40" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="#FF0000" stroke-width="0.00032"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(6.4,6.4), scale(0.6)"><rect x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" fill="#1a5fb4" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.152"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g><g id="SVGRepo_iconCarrier"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g></svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
||||||
|
<p>Lamentamos informarle de que el equipo de moderación ha tomado acciones en su cuenta</p>
|
||||||
|
<p>Su cuenta ha sido bloqueada indefinidamente y sus productos han sido eliminados de la tienda.</p>
|
||||||
|
<p>Si desea apelar, por favor, contacte con Soporte Técnico</p>
|
||||||
|
<p></p>
|
||||||
|
<p style="color: gray;">Este email ha sido enviado automaticamente, no responda a este correo.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<table border="0" cellpadding="0" style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido desbloqueada</h1>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 40px">
|
||||||
|
<svg fill="#FF0000" width="128px" height="128px" viewBox="-3.2 -3.2 38.40 38.40" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="#FF0000" stroke-width="0.00032"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(6.4,6.4), scale(0.6)"><rect x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" fill="#1a5fb4" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.152"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g><g id="SVGRepo_iconCarrier"> <title>alert</title> <path d="M14.611 18.856c-0.346 0.352-0.52 0.782-0.52 1.292 0 0.551 0.197 1.014 0.59 1.389 0.363 0.346 0.799 0.519 1.309 0.519 0.521 0 0.971-0.188 1.346-0.566s0.562-0.828 0.562-1.35c0-0.504-0.182-0.943-0.545-1.318-0.363-0.381-0.801-0.571-1.311-0.571-0.567-0.001-1.044 0.201-1.431 0.605v0zM14.391 10.788c-0.299 0.451-0.447 1.011-0.447 1.679 0 0.545 0.092 1.146 0.276 1.802s0.435 1.271 0.751 1.846c0.428 0.779 0.76 1.169 0.994 1.169 0.24 0 0.557-0.305 0.949-0.914 0.346-0.539 0.622-1.152 0.83-1.841s0.312-1.332 0.312-1.93c0-0.902-0.244-1.6-0.73-2.092-0.363-0.375-0.805-0.563-1.326-0.563-0.703 0-1.24 0.282-1.609 0.844v0z"></path> </g></svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 30px; font-family: sans-serif; line-height: 1.5; color: #444444;">
|
||||||
|
<p>Hemos aceptado la apelación del previo baneo de su cuenta</p>
|
||||||
|
<p>Su cuenta ha sido desbloqueada y ya puede entrar, pero los productos seguirán eliminados, por lo que deberá recrearlos para seguir vendiendolos</p>
|
||||||
|
<p>Muchas gracias por su paciencia</p>
|
||||||
|
<p></p>
|
||||||
|
<p style="color: gray;">Este email ha sido enviado automaticamente, no responda a este correo.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 20px;">
|
<th scope="col" align="center" style="padding: 20px;">
|
||||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
<th scope="col" align="center" style="background-color: #007bff; padding: 40px;">
|
||||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">¡Hola {{ name }}!</h1>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 40px">
|
<td align="center" style="padding: 40px">
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field.label_tag }} {{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{% extends "tienda/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4 mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2>Gestionar Imágenes</h2>
|
||||||
|
<p class="text-muted mb-0">Producto: <strong>{{ producto.name }}</strong></p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'mis_productos' %}" class="btn btn-outline-secondary">← Volver a Mis Productos</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Imagen Principal</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if producto.primary_image %}
|
||||||
|
<img src="{{ producto.primary_image.image.url }}" alt="{{ producto.primary_image.alt|default:producto.name }} - imagen principal" class="rounded" style="width: 200px; height: 200px; object-fit: cover;">
|
||||||
|
<p class="mt-2 text-muted mb-0">Esta imagen no se puede cambiar desde aquí.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No hay imagen principal asignada.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Imágenes Secundarias</h5>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#agregarImagenModal">
|
||||||
|
➕ Agregar Imagen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if secondary_images %}
|
||||||
|
<div class="row">
|
||||||
|
{% for img in secondary_images %}
|
||||||
|
<div class="col-md-3 col-sm-4 col-6 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<img src="{{ img.image.url }}" alt="{{ img.alt|default:producto.name }}" class="card-img-top" style="height: 180px; object-fit: cover;">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<form method="POST" action="{% url 'eliminar_imagen_secundaria' producto.id img.id %}" onsubmit="return confirm('¿Seguro que quieres eliminar esta imagen?');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm w-100">🗑 Eliminar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted text-center py-4">No hay imágenes secundarias. ¡Agrega una!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="agregarImagenModal" tabindex="-1" aria-labelledby="agregarImagenModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="agregarImagenModalLabel">Agregar Imagen Secundaria</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
{{ form }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Subir Imagen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,22 +12,7 @@
|
|||||||
<form method="post" action="{% url 'login' %}">
|
<form method="post" action="{% url 'login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="mb-3">
|
{{ form }}
|
||||||
<label for="loginEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="loginEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="loginPassword" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="loginPassword" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember">
|
|
||||||
<label class="form-check-label" for="rememberMe">
|
|
||||||
Recordarme
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Iniciar Sesión</button>
|
<button type="submit" class="btn btn-primary">Iniciar Sesión</button>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<td class="text-end">{{ producto.stock }}</td>
|
<td class="text-end">{{ producto.stock }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<a href="{% url 'gestionar_imagenes' producto.id %}" class="btn btn-outline-secondary btn-sm">Gestionar Imágenes</a>
|
||||||
<a href="{% url 'editar_producto' producto.id %}" class="btn btn-outline-primary btn-sm">Editar</a>
|
<a href="{% url 'editar_producto' producto.id %}" class="btn btn-outline-primary btn-sm">Editar</a>
|
||||||
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@@ -62,4 +62,101 @@
|
|||||||
{{ product.description }}
|
{{ product.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h4 class="mb-3">Valoraciones</h4>
|
||||||
|
<div id="reviews-summary" class="mb-4">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="fs-4" id="average-rating">0.0</div>
|
||||||
|
<div>
|
||||||
|
<div id="stars-display"></div>
|
||||||
|
<small class="text-muted" id="reviews-count">0 valoraciones</small>
|
||||||
|
</div>
|
||||||
|
{% if can_review %}
|
||||||
|
<a href="{% url 'add_review' product.id %}" class="btn btn-sm btn-outline-primary ms-auto">Valorar este producto</a>
|
||||||
|
{% elif user_has_review %}
|
||||||
|
<div class="ms-auto">
|
||||||
|
<a href="{% url 'add_review' product.id %}" class="btn btn-sm btn-outline-primary">Editar mi valoración</a>
|
||||||
|
<form method="post" action="{% url 'delete_review' user_review_id %}" style="display:inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta valoración?');">Eliminar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% elif user.is_authenticated %}
|
||||||
|
<span class="text-muted ms-auto">Solo puedes valorar productos que hayas comprado</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'login' %}?next={% url 'producto' product.id %}" class="btn btn-sm btn-outline-primary ms-auto">Inicia sesión para valorar</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="reviews-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
async function loadReviews() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("{% url 'product_reviews' product.id %}");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('average-rating').textContent = data.average_rating;
|
||||||
|
document.getElementById('reviews-count').textContent = data.reviews_count + ' valoraciones';
|
||||||
|
|
||||||
|
let starsHtml = '';
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
starsHtml += `<span class="${i <= Math.round(data.average_rating) ? 'text-warning' : 'text-secondary'}">★</span>`;
|
||||||
|
}
|
||||||
|
document.getElementById('stars-display').innerHTML = starsHtml;
|
||||||
|
|
||||||
|
const reviewsList = document.getElementById('reviews-list');
|
||||||
|
if (data.reviews.length === 0) {
|
||||||
|
reviewsList.innerHTML = '<p class="text-muted">Aún no hay valoraciones para este producto.</p>';
|
||||||
|
} else {
|
||||||
|
let reviewsHtml = '';
|
||||||
|
data.reviews.forEach(review => {
|
||||||
|
let imagesHtml = '';
|
||||||
|
if (review.images && review.images.length > 0) {
|
||||||
|
imagesHtml = '<div class="mt-2">';
|
||||||
|
review.images.forEach(img => {
|
||||||
|
imagesHtml += `<img src="${img.image}" class="img-thumbnail me-1" style="max-width: 80px; max-height: 80px;" alt="">`;
|
||||||
|
});
|
||||||
|
imagesHtml += '</div>';
|
||||||
|
}
|
||||||
|
const actionsHtml = review.is_owner
|
||||||
|
? `<div class="mt-2">
|
||||||
|
<a href="/tienda/producto/${review.id}/valorar/" class="btn btn-sm btn-outline-primary me-1">Editar</a>
|
||||||
|
<form method="post" action="/tienda/producto/${review.id}/valorar/eliminar/" style="display:inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta valoración?');">Eliminar</button>
|
||||||
|
</form>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
reviewsHtml += `
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<strong>${review.user}</strong>
|
||||||
|
<span class="text-warning ms-1">${'★'.repeat(review.rating)}</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">${new Date(review.created_at).toLocaleDateString('es-ES')}</small>
|
||||||
|
</div>
|
||||||
|
<h6 class="mt-2">${review.title}</h6>
|
||||||
|
<p class="mb-1">${review.content}</p>
|
||||||
|
${imagesHtml}
|
||||||
|
${actionsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
reviewsList.innerHTML = reviewsHtml;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading reviews:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadReviews();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,33 +12,7 @@
|
|||||||
<form method="post" action="{% url 'register' %}">
|
<form method="post" action="{% url 'register' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="mb-3">
|
{{ form }}
|
||||||
<label for="registerName" class="form-label">Nombre Completo</label>
|
|
||||||
<input type="text" class="form-control" id="registerName" name="name" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="registerEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerPassword" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="registerPassword" name="password" required>
|
|
||||||
<div class="form-text">La contraseña debe tener al menos 8 caracteres.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="registerPasswordConfirm" class="form-label">Confirmar Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="registerPasswordConfirm" name="password_confirm" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="acceptTerms" name="terms" required>
|
|
||||||
<label class="form-check-label" for="acceptTerms">
|
|
||||||
Acepto los <a href="{% url 'terminos' %}" target="_blank">términos y condiciones</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
||||||
|
|||||||
@@ -11,11 +11,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{% url 'reset_password' %}">
|
<form method="post" action="{% url 'reset_password' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
<div class="mb-3">
|
|
||||||
<label for="loginEmail" class="form-label">Correo Electrónico</label>
|
|
||||||
<input type="email" class="form-control" id="loginEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||||
|
|||||||
@@ -11,16 +11,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="verify_password" class="form-label">Verificar contraseña</label>
|
|
||||||
<input type="password" class="form-control" id="verify_password" name="verify_password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||||
|
|||||||
+186
-4
@@ -14,7 +14,9 @@ from .models import (
|
|||||||
StockReservation, StockReservationItem, Cart, CartItem,
|
StockReservation, StockReservationItem, Cart, CartItem,
|
||||||
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
||||||
)
|
)
|
||||||
|
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
||||||
|
import secrets
|
||||||
import string
|
import string
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -23,6 +25,185 @@ import random
|
|||||||
class UserModelTests(TestCase):
|
class UserModelTests(TestCase):
|
||||||
"""Tests exhaustivos para el modelo User."""
|
"""Tests exhaustivos para el modelo User."""
|
||||||
|
|
||||||
|
|
||||||
|
class FormTests(TestCase):
|
||||||
|
"""Tests para formularios Django."""
|
||||||
|
|
||||||
|
def test_user_register_form_terms_required(self):
|
||||||
|
"""El campo terms debe ser obligatorio."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("terms", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_terms_off_not_checked(self):
|
||||||
|
"""Si terms está en off (None/false), debe fallar."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
"terms": False,
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("terms", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_terms_on(self):
|
||||||
|
"""Si terms está marcado, el formulario debe ser válido."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "password123",
|
||||||
|
"terms": True,
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_user_register_form_passwords_mismatch(self):
|
||||||
|
"""Las contraseñas deben coincidir."""
|
||||||
|
form = UserRegisterForm(data={
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"password_confirm": "different_password",
|
||||||
|
"terms": True,
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_user_register_form_empty_fields(self):
|
||||||
|
"""Los campos obligatorios no pueden estar vacíos."""
|
||||||
|
form = UserRegisterForm(data={})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("name", form.errors)
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
self.assertIn("password", form.errors)
|
||||||
|
self.assertIn("password_confirm", form.errors)
|
||||||
|
|
||||||
|
def test_user_login_form_valid(self):
|
||||||
|
"""Login con datos válidos."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_user_login_form_missing_email(self):
|
||||||
|
"""Email es obligatorio en login."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_user_login_form_invalid_email_format(self):
|
||||||
|
"""Email debe tener formato válido."""
|
||||||
|
form = UserLoginForm(data={
|
||||||
|
"email": "not-an-email",
|
||||||
|
"password": "password123",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_edit_profile_form_valid(self):
|
||||||
|
"""Formulario de edición de perfil válido."""
|
||||||
|
form = EditProfileForm(data={
|
||||||
|
"first_name": "Juan",
|
||||||
|
"last_name": "Pérez",
|
||||||
|
"email": "juan@example.com",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_edit_profile_form_missing_email(self):
|
||||||
|
"""Email es obligatorio en perfil."""
|
||||||
|
form = EditProfileForm(data={
|
||||||
|
"first_name": "Juan",
|
||||||
|
"last_name": "Pérez",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_change_password_form_passwords_mismatch(self):
|
||||||
|
"""Las nuevas contraseñas deben coincidir."""
|
||||||
|
form = ChangePasswordForm(data={
|
||||||
|
"current_password": "oldpass123",
|
||||||
|
"new_password": "newpass123",
|
||||||
|
"confirm_password": "differentpass",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_change_password_form_short_password(self):
|
||||||
|
"""La nueva contraseña debe tener al menos 8 caracteres."""
|
||||||
|
form = ChangePasswordForm(data={
|
||||||
|
"current_password": "oldpass123",
|
||||||
|
"new_password": "short",
|
||||||
|
"confirm_password": "short",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
def test_shipping_address_form_valid(self):
|
||||||
|
"""Dirección con datos válidos."""
|
||||||
|
form = ShippingAddressForm(data={
|
||||||
|
"full_name": "Juan Pérez",
|
||||||
|
"address_line_1": "Calle Mayor 123",
|
||||||
|
"city": "Almería",
|
||||||
|
"postal_code": "04001",
|
||||||
|
"country": "España",
|
||||||
|
"phone": "612345678",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_shipping_address_form_missing_required_fields(self):
|
||||||
|
"""Campos obligatorios no pueden estar vacíos."""
|
||||||
|
form = ShippingAddressForm(data={})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("full_name", form.errors)
|
||||||
|
self.assertIn("address_line_1", form.errors)
|
||||||
|
self.assertIn("city", form.errors)
|
||||||
|
self.assertIn("postal_code", form.errors)
|
||||||
|
self.assertIn("phone", form.errors)
|
||||||
|
|
||||||
|
def test_reset_password_form_valid_email(self):
|
||||||
|
"""Formulario de recuperación de contraseña."""
|
||||||
|
form = ResetPasswordForm(data={
|
||||||
|
"email": "test@example.com",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_reset_password_form_invalid_email(self):
|
||||||
|
"""Email inválido."""
|
||||||
|
form = ResetPasswordForm(data={
|
||||||
|
"email": "not-an-email",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("email", form.errors)
|
||||||
|
|
||||||
|
def test_reset_password_phase2_form_valid(self):
|
||||||
|
"""Cambio de contraseña válido."""
|
||||||
|
form = ResetPasswordPhase2Form(data={
|
||||||
|
"password": "newpass123",
|
||||||
|
"verify_password": "newpass123",
|
||||||
|
})
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_reset_password_phase2_form_mismatch(self):
|
||||||
|
"""Las contraseñas deben coincidir."""
|
||||||
|
form = ResetPasswordPhase2Form(data={
|
||||||
|
"password": "newpass123",
|
||||||
|
"verify_password": "different",
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("__all__", form.errors)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== ENDPOINT VIEW TESTS ====================
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user_data = {
|
self.user_data = {
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
@@ -155,7 +336,7 @@ class VerificationCodeModelTests(TestCase):
|
|||||||
"""50 códigos pueden crearse sin conflictos."""
|
"""50 códigos pueden crearse sin conflictos."""
|
||||||
codes = []
|
codes = []
|
||||||
for i in range(50):
|
for i in range(50):
|
||||||
mode = random.choice([
|
mode = secrets.choice([
|
||||||
VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
VerificationCode.VerificationModes.VERIFY_ACCOUNT,
|
||||||
VerificationCode.VerificationModes.RESET_PASSWORD
|
VerificationCode.VerificationModes.RESET_PASSWORD
|
||||||
])
|
])
|
||||||
@@ -197,7 +378,7 @@ class CategoryModelTests(TestCase):
|
|||||||
"""100 categorías pueden crearse sin problemas."""
|
"""100 categorías pueden crearse sin problemas."""
|
||||||
categories = []
|
categories = []
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
cat = Category.objects.create(name=f"Category_{i}_{random.randint(1000, 9999)}")
|
cat = Category.objects.create(name=f"Category_{i}_{1000 + secrets.randbelow(9000)}")
|
||||||
categories.append(cat)
|
categories.append(cat)
|
||||||
|
|
||||||
self.assertEqual(len(categories), 100)
|
self.assertEqual(len(categories), 100)
|
||||||
@@ -1455,6 +1636,7 @@ class EndpointViewTests(TestCase):
|
|||||||
"email": "nuevo@example.com",
|
"email": "nuevo@example.com",
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
"password_confirm": self.password,
|
"password_confirm": self.password,
|
||||||
|
"terms": "on",
|
||||||
})
|
})
|
||||||
self.assertEqual(register_response.status_code, 302)
|
self.assertEqual(register_response.status_code, 302)
|
||||||
confirm_delay.assert_called_once()
|
confirm_delay.assert_called_once()
|
||||||
@@ -1605,7 +1787,7 @@ class EndpointViewTests(TestCase):
|
|||||||
self.assertTrue(OrderMessage.objects.filter(order_item=item, sender=self.seller).exists())
|
self.assertTrue(OrderMessage.objects.filter(order_item=item, sender=self.seller).exists())
|
||||||
|
|
||||||
delete_get = self.client.get(reverse("borrar_producto", args=[created.id]))
|
delete_get = self.client.get(reverse("borrar_producto", args=[created.id]))
|
||||||
self.assertEqual(delete_get.status_code, 302)
|
self.assertEqual(delete_get.status_code, 405)
|
||||||
delete_post = self.client.post(reverse("borrar_producto", args=[created.id]))
|
delete_post = self.client.post(reverse("borrar_producto", args=[created.id]))
|
||||||
self.assertEqual(delete_post.status_code, 302)
|
self.assertEqual(delete_post.status_code, 302)
|
||||||
self.assertFalse(Product.objects.filter(id=created.id).exists())
|
self.assertFalse(Product.objects.filter(id=created.id).exists())
|
||||||
@@ -1887,7 +2069,7 @@ class EndpointViewTests(TestCase):
|
|||||||
self.assertEqual(new_address.full_name, "Comprador Dos Editado")
|
self.assertEqual(new_address.full_name, "Comprador Dos Editado")
|
||||||
|
|
||||||
delete_get = self.client.get(reverse("eliminar_direccion", args=[new_address.id]))
|
delete_get = self.client.get(reverse("eliminar_direccion", args=[new_address.id]))
|
||||||
self.assertEqual(delete_get.status_code, 302)
|
self.assertEqual(delete_get.status_code, 405)
|
||||||
delete_post = self.client.post(reverse("eliminar_direccion", args=[new_address.id]))
|
delete_post = self.client.post(reverse("eliminar_direccion", args=[new_address.id]))
|
||||||
self.assertEqual(delete_post.status_code, 302)
|
self.assertEqual(delete_post.status_code, 302)
|
||||||
self.assertFalse(ShippingAddress.objects.filter(id=new_address.id).exists())
|
self.assertFalse(ShippingAddress.objects.filter(id=new_address.id).exists())
|
||||||
|
|||||||
+6
-1
@@ -18,6 +18,8 @@ urlpatterns = [
|
|||||||
path("venta/crear-producto/", views.crear_producto, name="crear_producto"),
|
path("venta/crear-producto/", views.crear_producto, name="crear_producto"),
|
||||||
path("venta/editar-producto/<int:id>/", views.editar_producto, name="editar_producto"),
|
path("venta/editar-producto/<int:id>/", views.editar_producto, name="editar_producto"),
|
||||||
path("venta/borrar-producto/<int:id>/", views.borrar_producto, name="borrar_producto"),
|
path("venta/borrar-producto/<int:id>/", views.borrar_producto, name="borrar_producto"),
|
||||||
|
path("venta/gestionar-imagenes/<int:id>/", views.gestionar_imagenes, name="gestionar_imagenes"),
|
||||||
|
path("venta/gestionar-imagenes/<int:product_id>/eliminar/<int:image_id>/", views.eliminar_imagen_secundaria, name="eliminar_imagen_secundaria"),
|
||||||
# Carrito
|
# Carrito
|
||||||
path("cart/", views.view_cart, name="view_cart"),
|
path("cart/", views.view_cart, name="view_cart"),
|
||||||
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
||||||
@@ -66,5 +68,8 @@ urlpatterns = [
|
|||||||
path("sobre-nosotros", views.sobre_nosotros, name="sobre_nosotros"),
|
path("sobre-nosotros", views.sobre_nosotros, name="sobre_nosotros"),
|
||||||
path("ayuda", views.ayuda, name="ayuda"),
|
path("ayuda", views.ayuda, name="ayuda"),
|
||||||
path("reset-password", views.reset_password, name="reset_password"),
|
path("reset-password", views.reset_password, name="reset_password"),
|
||||||
path("reset-password-phase2/<str:code>", views.reset_password_phase2, name="reset_password_phase2")
|
path("reset-password-phase2/<str:code>", views.reset_password_phase2, name="reset_password_phase2"),
|
||||||
|
path("producto/<int:product_id>/valorar/", views.add_review, name="add_review"),
|
||||||
|
path("valoracion/<int:review_id>/eliminar/", views.delete_review, name="delete_review"),
|
||||||
|
path("api/producto/<int:product_id>/valoraciones/", views.product_reviews, name="product_reviews"),
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-26
@@ -4,30 +4,6 @@ from django.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("email.system")
|
logger = logging.getLogger("email.system")
|
||||||
#
|
|
||||||
#def send_email(dest: str, title: str, body: str):
|
|
||||||
# context = ssl.create_default_context()
|
|
||||||
# try:
|
|
||||||
# with smtplib.SMTP(settings.SMTP_ENDPOINT, settings.SMTP_PORT) as server:
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# server.ehlo()
|
|
||||||
# server.starttls(context=context)
|
|
||||||
# server.ehlo()
|
|
||||||
# server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
|
|
||||||
#
|
|
||||||
# message = """\
|
|
||||||
#Subject: {}
|
|
||||||
#{}
|
|
||||||
# """.format(title, body)
|
|
||||||
# server.sendmail(settings.SMTP_EMAIL, dest, message)
|
|
||||||
# logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
|
||||||
#
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
|
||||||
# return (False, e)
|
|
||||||
#
|
|
||||||
# return (True,)
|
|
||||||
|
|
||||||
def send_email(dest: str, title: str, body: str):
|
def send_email(dest: str, title: str, body: str):
|
||||||
try:
|
try:
|
||||||
@@ -40,7 +16,7 @@ def send_email(dest: str, title: str, body: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
return (True,)
|
return (True, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
return (False, e)
|
return (False, e)
|
||||||
@@ -57,7 +33,7 @@ def send_hemail(dest: str, title: str, body: str, nbody: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
return (True,)
|
return (True, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
return (False, e)
|
return (False, e)
|
||||||
+586
-462
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,872 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amqp"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "vine" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "billiard"
|
||||||
|
version = "4.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boto3"
|
||||||
|
version = "1.43.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "s3transfer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0a/37/78c630d1308964aa9abf44951d9c4df776546ff37251ec2434944e205c4e/boto3-1.43.6.tar.gz", hash = "sha256:e6315effaf12b890b99956e6f8e2c3000a3f64e4ee91943cec3895ce9a836afb", size = 113153, upload-time = "2026-05-07T20:49:59.694Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/e2/3c2eef44f55eafab256836d1d9479bd6a74f70c26cbfdc0639a0e23e4327/boto3-1.43.6-py3-none-any.whl", hash = "sha256:179601ec2992726a718053bf41e43c223ceba397d31ceab11f64d9c910d9fc3a", size = 140502, upload-time = "2026-05-07T20:49:57.8Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "botocore"
|
||||||
|
version = "1.43.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/79/a7/23d0f5028011455096a1eeac0ddf3cbe147b3e855e127342f8202552194d/botocore-1.43.6.tar.gz", hash = "sha256:b1e395b347356860398da42e61c808cf1e34b6fa7180cf2b9d87d986e1a06ba0", size = 15336070, upload-time = "2026-05-07T20:49:48.14Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/c8/6f47223840e8d8cfa8c9f7c0ec1b77970417f257fc885169ff4f6326ce09/botocore-1.43.6-py3-none-any.whl", hash = "sha256:b6d1fdbc6f65a5fe0b7e947823aa37535d3f39f3ba4d21110fab1f55bbbcc04b", size = 15017094, upload-time = "2026-05-07T20:49:44.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "celery"
|
||||||
|
version = "5.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "billiard" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "click-didyoumean" },
|
||||||
|
{ name = "click-plugins" },
|
||||||
|
{ name = "click-repl" },
|
||||||
|
{ name = "kombu" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "tzlocal" },
|
||||||
|
{ name = "vine" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.4.22"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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/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]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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/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.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
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/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]]
|
||||||
|
name = "click-didyoumean"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click-plugins"
|
||||||
|
version = "1.1.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click-repl"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "prompt-toolkit" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "48.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defusedxml"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "6.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f1/bf85f0d29ef76abf901f193fe8fef4769d3da7794197832bc30151c071d8/django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269", size = 10924131, upload-time = "2026-05-05T13:54:39.329Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/5b/1328f8b84fce040c404f76822bf8c57d254e368e8cbd8bd67ec2b26d75f5/django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0", size = 8368680, upload-time = "2026-05-05T13:54:33.532Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-appconf"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/a2/e58bec8d7941b914af52a67c35b5709eceed2caa2848f28437f1666ed668/django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec", size = 16127, upload-time = "2025-11-08T15:46:27.304Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/e6/4c34d94dfb74bbcbc489606e61f1924933de30d22c593dd1f429f35fbd7f/django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4", size = 6500, upload-time = "2025-11-08T15:46:25.957Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-compressor"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-appconf" },
|
||||||
|
{ name = "rcssmin" },
|
||||||
|
{ name = "rjsmin" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/e4/c6d87b1341d744ceafa85eeceb2adabb1c62b795b8207cbc580fb70df8f4/django_compressor-4.6.0.tar.gz", hash = "sha256:c7478feab98f3368780591f9ee28a433350f5277dd28811f7f710f5bc6dff3c0", size = 99735, upload-time = "2025-11-10T13:12:11.439Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/9d/9a0ba39f33574994e5b33aea55a68e8fad72b8dd923a82300e4e91774f59/django_compressor-4.6.0-py3-none-any.whl", hash = "sha256:6e7b21020a0d86272c5e37000c33accc4ebeb77394a3dd86d775a09aae7aade4", size = 96828, upload-time = "2025-11-10T13:12:10.001Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-environ"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/3c/60983e6ec9b24a8d8588eecebfd21123cba980bce0a905807a27692f0860/django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f", size = 63529, upload-time = "2026-02-18T01:08:08.791Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e", size = 20682, upload-time = "2026-02-18T01:08:07.359Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-ninja"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/7c/3307e17b872f545c88314b2737a22f965785dfb5a120d739b0131d0492c3/django_ninja-1.6.2.tar.gz", hash = "sha256:d56ae5aa4791068ef4ac9a66cfdf2fc11f507413ded35abb79c51d0d52ad6412", size = 3685599, upload-time = "2026-03-18T20:06:47.284Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/0c/25f72060a39632fbd2d90e9c8b6052a09cd45b0598fc06c0758d313f0052/django_ninja-1.6.2-py3-none-any.whl", hash = "sha256:20095f5900bada22ea00cf1a58af50bdb285b2354c61a9d9b47d0dc89ac462d6", size = 2374994, upload-time = "2026-03-18T20:06:45.676Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-redis"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "redis" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/08/53/dbcfa1e528e0d6c39947092625b2c89274b5d88f14d357cee53c4d6dbbd4/django_redis-6.0.0.tar.gz", hash = "sha256:2d9cb12a20424a4c4dde082c6122f486628bae2d9c2bee4c0126a4de7fda00dd", size = 56904, upload-time = "2025-06-17T18:15:46.376Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/79/055dfcc508cfe9f439d9f453741188d633efa9eab90fc78a67b0ab50b137/django_redis-6.0.0-py3-none-any.whl", hash = "sha256:20bf0063a8abee567eb5f77f375143c32810c8700c0674ced34737f8de4e36c0", size = 33687, upload-time = "2025-06-17T18:15:34.165Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-storages"
|
||||||
|
version = "1.14.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/d6/2e50e378fff0408d558f36c4acffc090f9a641fd6e084af9e54d45307efa/django_storages-1.14.6.tar.gz", hash = "sha256:7a25ce8f4214f69ac9c7ce87e2603887f7ae99326c316bc8d2d75375e09341c9", size = 87587, upload-time = "2025-04-02T02:34:55.103Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/21/3cedee63417bc5553eed0c204be478071c9ab208e5e259e97287590194f1/django_storages-1.14.6-py3-none-any.whl", hash = "sha256:11b7b6200e1cb5ffcd9962bd3673a39c7d6a6109e8096f0e03d46fab3d3aabd9", size = 33095, upload-time = "2025-04-02T02:34:53.291Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
s3 = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fonttools"
|
||||||
|
version = "4.62.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fpdf2"
|
||||||
|
version = "2.8.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "defusedxml" },
|
||||||
|
{ name = "fonttools" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/27/f2/72feae0b2827ed38013e4307b14f95bf0b3d124adfef4d38a7d57533f7be/fpdf2-2.8.7.tar.gz", hash = "sha256:7060ccee5a9c7ab0a271fb765a36a23639f83ef8996c34e3d46af0a17ede57f9", size = 362351, upload-time = "2026-02-28T05:39:16.456Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/0a/cf50ecffa1e3747ed9380a3adfc829259f1f86b3fdbd9e505af789003141/fpdf2-2.8.7-py3-none-any.whl", hash = "sha256:d391fc508a3ce02fc43a577c830cda4fe6f37646f2d143d489839940932fbc19", size = 327056, upload-time = "2026-02-28T05:39:14.619Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "26.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jmespath"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kombu"
|
||||||
|
version = "5.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "amqp" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
{ name = "vine" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paypalrestsdk"
|
||||||
|
version = "1.13.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyopenssl" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/5e585f31a1c6caeec1af093edc3c6046a46af330ab9d9f91bbf86b019b59/paypalrestsdk-1.13.3.tar.gz", hash = "sha256:dac236492a9ac1260a760014a2e56ab38b09d8143295b5e896545359b61fedd6", size = 23865, upload-time = "2023-11-01T20:50:00.725Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/e0/ce62183f4ca1d9cab773a086a5d49e934f3a782960558ff971adc9fc9d05/paypalrestsdk-1.13.3-py3-none-any.whl", hash = "sha256:a3f51616ee8f6d975a5a5a8c2049db63653c843479c8fdd71c9d588a31e14560", size = 23681, upload-time = "2023-11-01T20:49:57.307Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "prompt-toolkit"
|
||||||
|
version = "3.0.52"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "wcwidth" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proyecto-final"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "celery" },
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "django-compressor" },
|
||||||
|
{ name = "django-environ" },
|
||||||
|
{ name = "django-ninja" },
|
||||||
|
{ name = "django-redis" },
|
||||||
|
{ name = "django-storages", extra = ["s3"] },
|
||||||
|
{ name = "fpdf2" },
|
||||||
|
{ name = "gunicorn" },
|
||||||
|
{ name = "paypalrestsdk" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "psycopg2-binary" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "stripe" },
|
||||||
|
{ name = "whitenoise" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "celery", specifier = "==5.6.3" },
|
||||||
|
{ name = "django", specifier = "==6.0.5" },
|
||||||
|
{ name = "django-compressor", specifier = "==4.6.0" },
|
||||||
|
{ name = "django-environ", specifier = ">=0.13.0" },
|
||||||
|
{ name = "django-ninja", specifier = ">=1.6.2" },
|
||||||
|
{ name = "django-redis", specifier = "==6.0.0" },
|
||||||
|
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
|
||||||
|
{ name = "fpdf2", specifier = "==2.8.7" },
|
||||||
|
{ name = "gunicorn", specifier = "==26.0.0" },
|
||||||
|
{ name = "paypalrestsdk", specifier = "==1.13.3" },
|
||||||
|
{ name = "pillow", specifier = "==12.2.0" },
|
||||||
|
{ name = "psycopg2-binary", specifier = "==2.9.12" },
|
||||||
|
{ name = "requests", specifier = "==2.34.2" },
|
||||||
|
{ name = "stripe", specifier = "==15.1.0" },
|
||||||
|
{ name = "whitenoise", specifier = "==6.12.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.12"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.13.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyopenssl"
|
||||||
|
version = "26.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcssmin"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/af/c9654b4f9b054ec163ed7cb20d8db0e5ae05e2e9ce99a4c11d91a2180b3f/rcssmin-1.2.2.tar.gz", hash = "sha256:806986eaf7414545edc28a1d29523e9560e49e151ff4a337d9d1f0271d6e1cc4", size = 587012, upload-time = "2025-10-12T10:48:08.932Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/40/9c4cb3133f6d4ddfbeada76988a10ff2a974706fd6fcbb97edd8c0f4cc76/rcssmin-1.2.2-cp314-cp314-manylinux1_i686.whl", hash = "sha256:540dd3aa586b5f8f4c4b90db37e6a31c04718cdf90dbe9bec43c3b4dd50519e7", size = 49032, upload-time = "2025-10-12T10:48:53.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/84/a411a48fd4179a88c68a2ad3649b408fa7887a421d3435c10ae6f5724e3a/rcssmin-1.2.2-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:6ea38a38eec263858b70bed6715478dcfed7fbc5d63333a8c512631ee22baad9", size = 49497, upload-time = "2025-10-12T10:48:54.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/32/5663a71a9304e0c9f33b765264508229d026359cfff746e1d0a593d809ea/rcssmin-1.2.2-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:07dc7d352e8eb08de82fc4c545dd04f9f487466c8370051e0bee4eb1e4dc85d0", size = 50382, upload-time = "2025-10-12T10:48:55.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/28/e411eb191ffff7bd712f2eb0f691cb7ca514b1876d6bff2f5ae61359b8db/rcssmin-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cdccb0e08281f0dd5d463c16ec61a06bd1534de50206dc72918be3c10dcb82e5", size = 50962, upload-time = "2025-10-12T10:48:56.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/3f/cdb99526d294c5dd4b919dc4ef492b7bd11e08b585d15ec641dfb9423493/rcssmin-1.2.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2b6d5e2e2fd65738d57ef65aaaed2cff2288eccff7f704bf3d579e6f451cb60a", size = 52504, upload-time = "2025-10-12T10:48:57.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/60/a8183401fa64e93e1d52b2cdf275a2c11e0993f5f3162c573a67872b535d/rcssmin-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7018d4197713c7797d1a67ed47ab53d4706c2e9ed134123c30a47d389dda5386", size = 50561, upload-time = "2025-10-12T10:48:58.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/5e/496d6c9c309e2fe79e6a69f25f7a6d18f545edb4ea3584f461b9f84b0d60/rcssmin-1.2.2-cp314-cp314t-manylinux1_i686.whl", hash = "sha256:0162c32ce946978edc834d4fba705ac5f9422d7f556f3264cc4fc67c7ee39171", size = 51214, upload-time = "2025-10-12T10:49:00.021Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/78/87da6706d5856ceee71421ba831d2f5d93c3e6865acfbb56ace8d54587cc/rcssmin-1.2.2-cp314-cp314t-manylinux1_x86_64.whl", hash = "sha256:f17dc92553a46412c49f972f0ab31088032b9482a9c421bc2d39691a5d8842aa", size = 51608, upload-time = "2025-10-12T10:49:01.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/6c/204b0262c11ac2da2b8df2d8fed76f1959273fbc8376450d0ac022d754b7/rcssmin-1.2.2-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:40c7dfba098bbd129d8c35dd8b604275585f9dc0496e5d17dbe7fd6b873b0233", size = 53349, upload-time = "2025-10-12T10:49:02.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/7b/9aae16756d3f33cbc512760ba3e69c3856a51aa293e463f2ca97760d1b1b/rcssmin-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d0197fab78ebbe33f5df9caf2572ef2d44bbe243a9130881a0c5c53ba03641fa", size = 53066, upload-time = "2025-10-12T10:49:03.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/18/b06fadfa9b85e486bb1571050217cb539c062d1ae4cd32b1a31c36f67fd4/rcssmin-1.2.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:19e53c58768369366fdaef00da59f275f724f229994ea885309df6ca368ff3c8", size = 54271, upload-time = "2025-10-12T10:49:04.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/55/f29ce21f8e5a1f3c19d43b67b907268d227b7edcda2ca200ca0028734a5e/rcssmin-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8d3de1a870e00d157f3a7b1797498fdc09a3774629079572350f75783bb94b9a", size = 52423, upload-time = "2025-10-12T10:49:06.04Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "7.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.34.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rjsmin"
|
||||||
|
version = "1.2.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/59/16/14288d309d0f42c6586440c47bf6ec1a880218f698f30293fa3782db4008/rjsmin-1.2.5.tar.gz", hash = "sha256:a3f8040b0273dec773e0e807e86a4d0a9535516c0a0a35aa1bb6de6e15bb1f09", size = 427399, upload-time = "2025-10-12T10:50:27.422Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ed/b472d5a3fd7d63c016893f7d438e677901fea28089b5d30cd1a115bcc887/rjsmin-1.2.5-cp314-cp314-manylinux1_i686.whl", hash = "sha256:7096357ed596fdfe0acb750f8cbfca338f3c845cc12def3861e23ed811589d15", size = 31983, upload-time = "2025-10-12T10:51:11.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/e8/e76fa527fde17fd08288e4efef25c0aba7979ed5740eeab7bdff507bdeba/rjsmin-1.2.5-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:4e80b05803749502995fe33b6f5fd589b51dc46e50d873baf0b515c8f6e7b668", size = 32002, upload-time = "2025-10-12T10:51:12.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/6c/ee395ef8ee117ba2d158a23a9502bc4a706e02f63bfdf6d01b802ae6ee9a/rjsmin-1.2.5-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:b6d0bc092acc3f54ea63ec1dcb808edaac5e956141d89fd0d038e80de5322052", size = 32435, upload-time = "2025-10-12T10:51:13.147Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/78/c157d33aa6148f0e8c57bb91a41969e1a4aab929f3bb0a8d9ff3b5e21556/rjsmin-1.2.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e2943259be7beafdcb0847c2a901f223bf9044bdfa8105e1be1ad67d6c47795", size = 32877, upload-time = "2025-10-12T10:51:14.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/49/6252145bf85d87c815aaf441c5efdf1ce918db5ab6e915cf6d0d99ca3969/rjsmin-1.2.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0387568c27fb49e55c1d0dfc27b54fc63d04b7756b1fed9743078130262907f", size = 32957, upload-time = "2025-10-12T10:51:15.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/7e/c321c047b1a2fb7fa5ac818c37c1a15d348e1c12a1148de8ca5192a83b8f/rjsmin-1.2.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8196f1ecb0dff6c8647d4622e496869e94f1be92567ea2e941aa18d49a1a4347", size = 32456, upload-time = "2025-10-12T10:51:16.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/d7/2d190ce5ad10832df62edd4d9b1ae7092fd259ca58b39a1e202337f511a9/rjsmin-1.2.5-cp314-cp314t-manylinux1_i686.whl", hash = "sha256:9dd9f66568be9c8676278f140aa54102fab9af7feb59adf0c7a85bef49fe70df", size = 34115, upload-time = "2025-10-12T10:51:17.911Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/ab/e7bcf261ede4cef7a0693927d7dcd1612bb59ba6c05191f58a92deec9f01/rjsmin-1.2.5-cp314-cp314t-manylinux1_x86_64.whl", hash = "sha256:5b8f72f7d96e5e1d30a33182cb39d4eb4516ddcd9b2f984813a9eefe66f8e180", size = 33977, upload-time = "2025-10-12T10:51:18.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/75/f1ff5f2199437b534204b40aa46c55c703489063cf7806c948a1a665575e/rjsmin-1.2.5-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:8c5906bd8830f616e992ad5e7277d0ea12c530110da188b2b9da23e9524a7cbc", size = 34604, upload-time = "2025-10-12T10:51:20.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/dc/acd463d88c56476cc683f1c6cce893c590007dccd390747e824b8e923d63/rjsmin-1.2.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8207bac0d3bab7791fd667f0863b5f32e51047845179b94b28c716e6514a9234", size = 34775, upload-time = "2025-10-12T10:51:21.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/56/e6f61718d1c36e646aabe552ad1f8f77744a4c57524eaa782b5b44eba220/rjsmin-1.2.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1e3ab93a51d7581ba0a3b6a383df2929b86d9d55f9516764678f9b4e409826e8", size = 34682, upload-time = "2025-10-12T10:51:22.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/f3/37a4672ddb1307eb57d9b54ba89a48f483a04a63cac4e1471fdb4cba76e6/rjsmin-1.2.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47dad1732a2c4779bdc76d5b3183fdf2ec27838f31071fa9dfcc79483d3480e2", size = 34161, upload-time = "2025-10-12T10:51:23.761Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "s3transfer"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stripe"
|
||||||
|
version = "15.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/45/26/5d6f5f5beae6f1ff78213e2e6f4fbd431518dcd98733cdd39fb4ba0d01d3/stripe-15.1.0.tar.gz", hash = "sha256:24bd3b6bd0969a4841bd4d7681556a9e35e46c414a07c8590a225fbd5a878450", size = 1501673, upload-time = "2026-04-24T00:18:58.612Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/4e/fd9cb74ddf1e61fb6241e2f6799a81ef99bf6cf2e94f8812ee1cd5458e5d/stripe-15.1.0-py3-none-any.whl", hash = "sha256:bdfb556be08662a41833e6403607ebf12e0062cae4f9b93e2b89b6ba926d7c82", size = 2143199, upload-time = "2026-04-24T00:18:56.027Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vine"
|
||||||
|
version = "5.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wcwidth"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whitenoise"
|
||||||
|
version = "6.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user