Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| a02617f8d2 | |||
| 53b4e89347 | |||
| df0579dd86 | |||
| 1022a44f12 | |||
| bb697d92c6 |
@@ -0,0 +1,15 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
- dependency-type: "direct"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -19,15 +19,16 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
- name: Configurar uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Instalar dependencias
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
uv sync --no-dev --no-install-project
|
||||
- name: Ejecutar tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||
run: |
|
||||
python manage.py test
|
||||
uv run python manage.py test
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -16,15 +16,16 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
- name: Configurar uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Instalar dependencias
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
uv sync --no-dev --no-install-project
|
||||
- name: Ejecutar tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||
run: |
|
||||
python manage.py test
|
||||
uv run python manage.py test
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
@@ -8,3 +8,4 @@ __pycache__/
|
||||
tienda/__pycache__/
|
||||
proyecto/__pycache__/
|
||||
media
|
||||
staticfiles
|
||||
|
||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"python.REPL.enableREPLSmartSend": false
|
||||
"python.REPL.enableREPLSmartSend": false,
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# AGENTS.md - Django Tienda Project
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
make test # Run tests
|
||||
python manage.py runserver # Dev server
|
||||
python manage.py migrate # Migrations
|
||||
python manage.py collectstatic # Static files (production)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Redis**: `redis://127.0.0.1:6379/1` (Linux: `sudo systemctl start redis-server`)
|
||||
- **PostgreSQL**: Default. Set `POSTGRES_ENABLED=False` for SQLite
|
||||
- **Environment**: Copy `.env.example` to `.env`
|
||||
|
||||
## Quirks
|
||||
|
||||
1. **Migrations**: If `makemigrations` fails with error 130, check `tienda/migrations/` - file often created anyway
|
||||
2. **Test DB**: Always uses SQLite (hardcoded)
|
||||
3. **App URL**: `http://localhost:8000/tienda/` (not `/`)
|
||||
4. **Admin**: `/admin/`
|
||||
5. **User Model**: Use `tienda.User` for queries
|
||||
|
||||
## Architecture
|
||||
|
||||
- `proyecto/` - Django settings, URLs, WSGI/ASGI
|
||||
- `tienda/` - Main app (models, views, admin, templates)
|
||||
- Templates extend `tienda/templates/tienda/base.html`
|
||||
|
||||
## Shipping
|
||||
|
||||
Only Almería province, Spain (04xxx). Country: "España".
|
||||
|
||||
## External Services
|
||||
|
||||
- **Payment**: Stripe + PayPal (via .env)
|
||||
- **Storage**: S3 - set `S3_ENABLE=True`
|
||||
- **Email**: SMTP (see .env.example)
|
||||
- **Async**: Celery + Redis
|
||||
|
||||
## Deploy
|
||||
|
||||
- Push to `origin` and `github` after changes
|
||||
- GitHub Actions updates `ghcr.io/dsaub/proyecto-mvc:development`
|
||||
- SSH: `debian@172.16.14.221` (requires VPN - skip if unavailable)
|
||||
- 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/`
|
||||
+3
-2
@@ -4,9 +4,10 @@ ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
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
|
||||
RUN pip install --no-cache-dir uv
|
||||
RUN uv sync --no-dev --no-install-project # Install only dependencies, not the local project package
|
||||
|
||||
COPY . /app/
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
@@ -39,7 +39,7 @@ Con tus valores reales del Sandbox.
|
||||
|
||||
### 6. Instalar el SDK de PayPal
|
||||
```bash
|
||||
pip install paypalrestsdk
|
||||
uv add paypalrestsdk
|
||||
```
|
||||
|
||||
### 7. Usar cuentas de prueba para transacciones
|
||||
@@ -67,7 +67,7 @@ Si todo está bien, deberías ver:
|
||||
|
||||
## 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_SECRET` en settings.py (no vacío)
|
||||
- [ ] `PAYPAL_MODE = 'sandbox'` en settings.py
|
||||
+3
-3
@@ -5,10 +5,10 @@ set -eu
|
||||
echo "Sleeping due to mysql..."
|
||||
sleep 10
|
||||
echo "Running DB migrations..."
|
||||
python manage.py migrate
|
||||
uv run python manage.py migrate
|
||||
echo "Collecting STATIC..."
|
||||
python manage.py collectstatic --noinput --clear
|
||||
uv run python manage.py collectstatic --noinput --clear
|
||||
|
||||
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
|
||||
__all__ = ('celery_app',)
|
||||
from .celery import app as celery
|
||||
__all__ = ('celery',)
|
||||
+6
-1
@@ -1,8 +1,13 @@
|
||||
from celery import Celery
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
||||
django.setup()
|
||||
|
||||
app = Celery('proyecto')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
|
||||
app.config_from_object('django.conf:settings', namespace="CELERY")
|
||||
|
||||
user_options = {}
|
||||
|
||||
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
|
||||
+11
-8
@@ -14,6 +14,7 @@ import logging
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
|
||||
DEV_ENV = (sys.argv[1] == 'runserver')
|
||||
|
||||
RUNNING_TESTS = any(arg in {'test', 'pytest'} for arg in sys.argv) or 'PYTEST_CURRENT_TEST' in os.environ
|
||||
|
||||
@@ -101,6 +102,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.forms',
|
||||
'compressor',
|
||||
]
|
||||
|
||||
@@ -136,14 +138,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'
|
||||
@@ -216,6 +210,8 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
COMPRESS_ROOT = STATIC_ROOT
|
||||
COMPRESS_URL = STATIC_URL
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'tienda' / 'static',
|
||||
]
|
||||
@@ -428,3 +424,10 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
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"
|
||||
@@ -0,0 +1,23 @@
|
||||
[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-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.33.1",
|
||||
"stripe==15.1.0",
|
||||
"whitenoise==6.12.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
+27
-22
@@ -1,46 +1,51 @@
|
||||
amqp==5.3.1
|
||||
asgiref==3.11.0
|
||||
asgiref==3.11.1
|
||||
billiard==4.2.4
|
||||
celery==5.6.2
|
||||
certifi==2026.1.4
|
||||
boto3==1.43.5
|
||||
botocore==1.43.5
|
||||
celery==5.6.3
|
||||
certifi==2026.4.22
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
charset-normalizer==3.4.7
|
||||
click==8.3.3
|
||||
click-didyoumean==0.3.1
|
||||
click-plugins==1.1.1.2
|
||||
click-repl==0.3.0
|
||||
cryptography==46.0.7
|
||||
Django==6.0.4
|
||||
cryptography==48.0.0
|
||||
defusedxml==0.7.1
|
||||
Django==6.0.5
|
||||
django-appconf==1.2.0
|
||||
django-redis==5.4.0
|
||||
django-redis==6.0.0
|
||||
django-storages==1.14.6
|
||||
django_compressor==4.6.0
|
||||
django-storages[boto3]==1.14.6
|
||||
gunicorn==25.1.0
|
||||
idna==3.11
|
||||
Jinja2==3.1.6
|
||||
fonttools==4.62.1
|
||||
fpdf2==2.8.7
|
||||
gunicorn==26.0.0
|
||||
idna==3.13
|
||||
|
||||
jmespath==1.1.0
|
||||
kombu==5.6.2
|
||||
MarkupSafe==3.0.3
|
||||
packaging==26.0
|
||||
packaging==26.2
|
||||
paypalrestsdk==1.13.3
|
||||
pillow==12.2.0
|
||||
boto3==1.42.97
|
||||
prompt_toolkit==3.0.52
|
||||
psycopg2-binary==2.9.12
|
||||
pycparser==3.0
|
||||
pyOpenSSL==26.0.0
|
||||
pyOpenSSL==26.2.0
|
||||
python-dateutil==2.9.0.post0
|
||||
rcssmin==1.2.2
|
||||
redis==5.2.1
|
||||
requests==2.33.0
|
||||
redis==7.4.0
|
||||
requests==2.33.1
|
||||
rjsmin==1.2.5
|
||||
s3transfer==0.17.0
|
||||
six==1.17.0
|
||||
sqlparse==0.5.5
|
||||
stripe==14.3.0
|
||||
stripe==15.1.0
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
tzdata==2026.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.6.3
|
||||
vine==5.1.0
|
||||
wcwidth==0.6.0
|
||||
wcwidth==0.7.0
|
||||
whitenoise==6.12.0
|
||||
fpdf2==2.8.7
|
||||
psycopg2-binary==2.9.11
|
||||
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
+1
-1
@@ -33,7 +33,7 @@ def main() -> None:
|
||||
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")
|
||||
print(" SOLUCIÓN: uv add paypalrestsdk")
|
||||
sys.exit(1)
|
||||
|
||||
# Intentar conectar a PayPal
|
||||
|
||||
+60
-3
@@ -1,17 +1,74 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod
|
||||
# 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(Image)
|
||||
admin.site.register(User)
|
||||
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:
|
||||
user: User = user
|
||||
# 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:
|
||||
user: User = user
|
||||
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)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'price', 'stock', 'category', 'creator')
|
||||
search_fields = ('name', 'creator__username', 'creator__email')
|
||||
list_display = ('id', 'sku', 'name', 'price', 'stock', 'category', 'creator')
|
||||
search_fields = ('name', 'sku', 'creator__username', 'creator__email')
|
||||
list_filter = ('category',)
|
||||
class CartItemInline(admin.TabularInline):
|
||||
model = CartItem
|
||||
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import Category
|
||||
|
||||
|
||||
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,
|
||||
widget = forms.ClearableFileInput(
|
||||
attrs = {
|
||||
'class': 'form-control',
|
||||
'accept': 'image/*'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
widget=forms.ClearableFileInput(attrs={'class': 'form-control', 'accept': 'image/*'})
|
||||
)
|
||||
|
||||
|
||||
class SecondaryImageForm(forms.Form):
|
||||
image = forms.ImageField(
|
||||
label="Seleccionar Imagen",
|
||||
required = True,
|
||||
widget = forms.ClearableFileInput(
|
||||
attrs = {
|
||||
'class': 'form-control',
|
||||
'accept': 'image/*'
|
||||
}
|
||||
)
|
||||
)
|
||||
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 = "Correo Electrónico",
|
||||
max_length = 255,
|
||||
required = True,
|
||||
widget = forms.TextInput(
|
||||
attrs = {
|
||||
'class': 'form-control'
|
||||
}
|
||||
)
|
||||
)
|
||||
password = forms.CharField(
|
||||
label = "Contraseña",
|
||||
max_length = 255,
|
||||
required = True,
|
||||
widget = forms.PasswordInput(
|
||||
attrs = {
|
||||
'class': 'form-control'
|
||||
}
|
||||
)
|
||||
)
|
||||
password_confirm = forms.CharField(
|
||||
label = "Verificar Contraseña",
|
||||
max_length = 255,
|
||||
required = True,
|
||||
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("Las contraseñas no coinciden.")
|
||||
|
||||
|
||||
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="Correo Electrónico",
|
||||
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("Las contraseñas no coinciden.")
|
||||
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="Correo Electrónico",
|
||||
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("Las contraseñas no coinciden.")
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+30
-3
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unicodedata
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, AbstractUser
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -83,8 +84,9 @@ class Image(models.Model):
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=200, default="")
|
||||
description = models.TextField(default = "")
|
||||
briefdesc = models.TextField(default = "")
|
||||
sku = models.CharField(max_length=50, unique=True, blank=True, null=True)
|
||||
description = models.TextField(default = "", max_length=5000)
|
||||
briefdesc = models.TextField(default = "", max_length=250)
|
||||
price = models.FloatField(default = 0)
|
||||
stock = models.PositiveIntegerField(default=0)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
@@ -106,6 +108,7 @@ class Product(models.Model):
|
||||
def to_dict(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"sku": self.sku,
|
||||
"description": self.description,
|
||||
"briefdesc": self.briefdesc,
|
||||
"price": self.price,
|
||||
@@ -342,8 +345,32 @@ class ShippingAddress(models.Model):
|
||||
def __str__(self):
|
||||
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):
|
||||
# Si se marca como predeterminada, desmarcar las demás del usuario
|
||||
self.full_clean()
|
||||
if self.is_default:
|
||||
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@@ -315,5 +315,6 @@ p.price {
|
||||
}
|
||||
|
||||
.texto-ajustado {
|
||||
overflow-wrap: anywhere;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
+27
-5
@@ -11,14 +11,32 @@ from .models import User, VerificationCode
|
||||
@shared_task
|
||||
def enviar_correo_bienvenida(email_usuario: str, nombre_usuario: str):
|
||||
html_content = render_to_string(
|
||||
'emails/welcome.html',
|
||||
'tienda/emails/welcome.html',
|
||||
{
|
||||
"name": nombre_usuario
|
||||
},
|
||||
using='jinja2'
|
||||
)
|
||||
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
|
||||
def enviar_correo_confirmacion(id: int):
|
||||
usuario = User.objects.get(id=id)
|
||||
@@ -33,7 +51,11 @@ def enviar_correo_confirmacion(id: int):
|
||||
|
||||
@shared_task
|
||||
def enviar_correo_recuperacion(email: str):
|
||||
usuario: User | None
|
||||
try:
|
||||
usuario = User.objects.get(email=email)
|
||||
except User.DoesNotExist as e:
|
||||
usuario = None
|
||||
if usuario is not None:
|
||||
ver_code = VerificationCode.objects.create(
|
||||
code_mode = VerificationCode.VerificationModes.RESET_PASSWORD,
|
||||
@@ -42,18 +64,18 @@ def enviar_correo_recuperacion(email: str):
|
||||
)
|
||||
ver_code.save()
|
||||
html_content = render_to_string(
|
||||
'emails/reset_pass.html',
|
||||
'tienda/emails/reset_pass.html',
|
||||
{
|
||||
"name": usuario.get_full_name(),
|
||||
"domain": settings.DOMAIN,
|
||||
"protocol": settings.PROTOCOL,
|
||||
"code": ver_code.code
|
||||
},
|
||||
using='jinja2'
|
||||
)
|
||||
|
||||
send_hemail(email, "Reset de Contraseña", html_content, "Estas reseteando la contraseña...")
|
||||
|
||||
else:
|
||||
print("User does not exist, Cancelling TASK.")
|
||||
|
||||
# Purchased items should be a list of dictionary, the dictionary must follow this tags: amount, product name, price (each)
|
||||
@shared_task
|
||||
|
||||
@@ -13,74 +13,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{{ form }}
|
||||
<!-- Botones -->
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="{% url 'mis_productos' %}" class="btn btn-secondary">Cancelar</a>
|
||||
|
||||
@@ -21,52 +21,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
<div class="d-flex gap-2">
|
||||
<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>
|
||||
@@ -80,7 +35,6 @@
|
||||
<script>
|
||||
(function () {
|
||||
const cityInput = document.getElementById('city');
|
||||
const cityValidationMessage = document.getElementById('city-validation-message');
|
||||
const form = cityInput ? cityInput.form : null;
|
||||
|
||||
if (!cityInput || !form) {
|
||||
@@ -123,8 +77,6 @@
|
||||
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
||||
cityInput.classList.add('is-invalid');
|
||||
}
|
||||
|
||||
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
|
||||
}
|
||||
|
||||
cityInput.addEventListener('input', validateTown);
|
||||
|
||||
@@ -37,18 +37,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||
<input type="text" class="form-control" id="username" value="{{ user.username }}" disabled>
|
||||
@@ -69,19 +58,7 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{% url 'cambiar_contrasena' %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ password_form.as_p }}
|
||||
<button type="submit" class="btn btn-warning">Cambiar Contraseña</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,67 +13,9 @@
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
||||
<!-- 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"
|
||||
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 -->
|
||||
<!-- Imágenes secundarias (no incluidas en el form) -->
|
||||
<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"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||
<tr>
|
||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido bloqueada</h1>
|
||||
</td>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,27 @@
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<table width="600" border="0" cellspacing="0" cellpadding="0" style="border: 1px solid #eeeeee; background-color: #ffffff;">
|
||||
<tr>
|
||||
<td align="center" style="background-color: #007bff; padding: 40px;">
|
||||
<h1 style="color: #ffffff; font-family: sans-serif; margin: 0;">Su cuenta ha sido desbloqueada</h1>
|
||||
</td>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
</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 }}" 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' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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="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>
|
||||
{{ form }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<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">
|
||||
<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>
|
||||
<form method="POST" action="{% url 'borrar_producto' producto.id %}" onsubmit="return confirm('¿Seguro que quieres borrar este producto?');">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -12,33 +12,7 @@
|
||||
<form method="post" action="{% url 'register' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{ form }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Crear Cuenta</button>
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'reset_password' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||
|
||||
@@ -11,16 +11,7 @@
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'reset_password_phase2' code %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<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>
|
||||
{{ form.as_p }}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Recuperar contraseña</button>
|
||||
|
||||
+181
@@ -14,6 +14,7 @@ from .models import (
|
||||
StockReservation, StockReservationItem, Cart, CartItem,
|
||||
Order, OrderItem, OrderMessage, SavedPaymentMethod, ShippingAddress
|
||||
)
|
||||
from .forms import UserRegisterForm, UserLoginForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||
from .vars import VAT_RATE, TRANSACTION_CODE_PREFIX
|
||||
import string
|
||||
import random
|
||||
@@ -23,6 +24,185 @@ import random
|
||||
class UserModelTests(TestCase):
|
||||
"""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):
|
||||
self.user_data = {
|
||||
"username": "testuser",
|
||||
@@ -1455,6 +1635,7 @@ class EndpointViewTests(TestCase):
|
||||
"email": "nuevo@example.com",
|
||||
"password": self.password,
|
||||
"password_confirm": self.password,
|
||||
"terms": "on",
|
||||
})
|
||||
self.assertEqual(register_response.status_code, 302)
|
||||
confirm_delay.assert_called_once()
|
||||
|
||||
@@ -18,6 +18,8 @@ urlpatterns = [
|
||||
path("venta/crear-producto/", views.crear_producto, name="crear_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/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
|
||||
path("cart/", views.view_cart, name="view_cart"),
|
||||
path("cart/add/<int:product_id>/", views.add_to_cart, name="add_to_cart"),
|
||||
|
||||
+310
-308
@@ -1,10 +1,11 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
|
||||
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
|
||||
|
||||
from django.db.utils import DataError
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode, SavedPaymentMethod
|
||||
from .forms import ProductForm, SecondaryImageForm, UserLoginForm, UserRegisterForm, ProductEditForm, EditProfileForm, ChangePasswordForm, ShippingAddressForm, ResetPasswordForm, ResetPasswordPhase2Form
|
||||
from . import tasks
|
||||
from .vars import (
|
||||
PAGE_SIZE,
|
||||
@@ -15,7 +16,7 @@ from .vars import (
|
||||
STOCK_RESERVATION_MINUTES,
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt, csrf_protect
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@@ -23,6 +24,7 @@ from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import timedelta
|
||||
import stripe
|
||||
from django.db import models, transaction
|
||||
from django.db.models import F
|
||||
from django.core.cache import cache
|
||||
import re
|
||||
import unicodedata
|
||||
@@ -85,9 +87,10 @@ def _is_almeria_city(city: str) -> bool:
|
||||
return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES
|
||||
|
||||
|
||||
def _address_form_context(direccion=None):
|
||||
def _address_form_context(direccion=None, form=None):
|
||||
return {
|
||||
"direccion": direccion,
|
||||
"form": form,
|
||||
"almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY,
|
||||
}
|
||||
|
||||
@@ -220,113 +223,129 @@ def index(request: HttpRequest):
|
||||
|
||||
def login(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("email")
|
||||
password = request.POST.get("password")
|
||||
remember = request.POST.get("remember")
|
||||
form: UserLoginForm = UserLoginForm(request.POST)
|
||||
if form.is_valid():
|
||||
email: str = form.cleaned_data["email"]
|
||||
password: str = form.cleaned_data["password"]
|
||||
remember: bool = form.cleaned_data["remember"]
|
||||
client_ip = _get_client_ip(request)
|
||||
|
||||
# Buscar usuario por email
|
||||
try:
|
||||
user_obj = User.objects.get(email=email)
|
||||
username = user_obj.username
|
||||
user: User = User.objects.get(email=email)
|
||||
username = user.username
|
||||
except User.DoesNotExist:
|
||||
audit_logger.warning(
|
||||
"LOGIN_FAILED email=%s reason=user_not_found ip=%s",
|
||||
email,
|
||||
client_ip,
|
||||
)
|
||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
return render(request, "tienda/login.html")
|
||||
audit_logger.warning("LOGIN FAILED email=%s reason=user_not_found ip=%s", email, client_ip)
|
||||
messages.error(request, "El email o la contraseña es incorrecta")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
if user.registration_status == User.RegisterStatus.BANNED:
|
||||
# Usuario baneado.
|
||||
messages.error(request, "Esta cuenta esta bloqueada.")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
|
||||
# Autenticar usuario
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is None: # Bug de error 500 en caso de fallar la contra
|
||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
return render(request, "tienda/login.html")
|
||||
user = User.objects.get(username=user.username)
|
||||
if user.registration_status == "CR":
|
||||
audit_logger.info(
|
||||
"LOGIN_FAILED email=%s reason=not_verified", email
|
||||
)
|
||||
user = authenticate(request, username = username, password=password)
|
||||
|
||||
if user is None:
|
||||
data: str = cache.get(f"tries_login_{username}")
|
||||
logins: int
|
||||
if data is None:
|
||||
logins = 0
|
||||
else:
|
||||
logins = int(data)
|
||||
|
||||
if logins >= 5:
|
||||
audit_logger.info("LOGIN FAILED email=%s reason=rate_limited", email)
|
||||
messages.error(request, "Has sufrido de Rate Limit por fallar 5 veces la contraseña")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
logins+=1
|
||||
cache.set(f"tries_login_{username}", str(logins), 600)
|
||||
messages.error(request, "El email o la contraseña es incorrecta")
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
if user.registration_status == User.RegisterStatus.CONFIRMATION_REQUIRED:
|
||||
audit_logger.info("LOGIN_FAILED email=%s reason=not_verified", email)
|
||||
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
||||
return render(request, "tienda/login.html")
|
||||
|
||||
if user is not None:
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
auth_login(request, user)
|
||||
|
||||
# Configurar duración de sesión
|
||||
if not remember:
|
||||
request.session.set_expiry(0)
|
||||
else:
|
||||
request.session.set_expiry(1209600) # 14 días en segundos
|
||||
request.session.set_expiry(1209600)
|
||||
|
||||
audit_logger.info(
|
||||
"LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s",
|
||||
user.id,
|
||||
user.email,
|
||||
client_ip,
|
||||
bool(remember),
|
||||
)
|
||||
tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name))
|
||||
# result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name)))
|
||||
audit_logger.info("LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s", user.id, user.email, client_ip, bool(remember))
|
||||
tasks.enviar_correo_bienvenida.delay(user.email, f"{user.first_name} {user.last_name}")
|
||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||
return redirect("index")
|
||||
else:
|
||||
audit_logger.warning(
|
||||
"LOGIN_FAILED email=%s reason=invalid_credentials ip=%s",
|
||||
email,
|
||||
client_ip,
|
||||
)
|
||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
return render(request, "tienda/login.html")
|
||||
|
||||
return render(request, "tienda/login.html")
|
||||
form = UserLoginForm()
|
||||
return render(request, "tienda/login.html", {"form": form})
|
||||
|
||||
#
|
||||
# if user is not None:
|
||||
# auth_login(request, user)
|
||||
#
|
||||
# # Configurar duración de sesión
|
||||
# if not remember:
|
||||
# request.session.set_expiry(0)
|
||||
# else:
|
||||
# request.session.set_expiry(1209600) # 14 días en segundos
|
||||
#
|
||||
# audit_logger.info(
|
||||
# "LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s",
|
||||
# user.id,
|
||||
# user.email,
|
||||
# client_ip,
|
||||
# bool(remember),
|
||||
# )
|
||||
# tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name))
|
||||
# # result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name)))
|
||||
# messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||
# return redirect("index")
|
||||
# else:
|
||||
# user1: User = User.objects.get(username=username)
|
||||
# if user1.registration_status == User.RegisterStatus.BANNED:
|
||||
# audit_logger.warning(
|
||||
# "LOGIN FAILED email=%s reason=user_banned ip=%s",
|
||||
# email,
|
||||
# client_ip,
|
||||
# )
|
||||
# messages.error(request, "Error, La cuenta esta bloqueada")
|
||||
# return render(request, "tienda/login.html")
|
||||
# audit_logger.warning(
|
||||
# "LOGIN_FAILED email=%s reason=invalid_credentials ip=%s",
|
||||
# email,
|
||||
# client_ip,
|
||||
# )
|
||||
# messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||
# return render(request, "tienda/login.html")
|
||||
#
|
||||
# return render(request, "tienda/login.html")
|
||||
|
||||
def register(request: HttpRequest):
|
||||
if request.user.is_authenticated:
|
||||
return redirect("index")
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
email = request.POST.get("email")
|
||||
password = request.POST.get("password")
|
||||
password_confirm = request.POST.get("password_confirm")
|
||||
form = UserRegisterForm(request.POST)
|
||||
if form.is_valid():
|
||||
name = form.cleaned_data.get("name")
|
||||
email = form.cleaned_data.get("email")
|
||||
password = form.cleaned_data.get("password")
|
||||
client_ip = _get_client_ip(request)
|
||||
|
||||
# Validaciones
|
||||
if password != password_confirm:
|
||||
audit_logger.warning("REGISTER_FAILED email=%s reason=password_mismatch ip=%s", email, client_ip)
|
||||
messages.error(request, "Las contraseñas no coinciden.")
|
||||
return render(request, "tienda/register.html")
|
||||
|
||||
if len(password) < 8:
|
||||
audit_logger.warning("REGISTER_FAILED email=%s reason=password_too_short ip=%s", email, client_ip)
|
||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||
return render(request, "tienda/register.html")
|
||||
|
||||
# Validación email
|
||||
if User.objects.filter(email=email).exists():
|
||||
audit_logger.warning("REGISTER_FAILED email=%s reason=email_exists ip=%s", email, client_ip)
|
||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||
return render(request, "tienda/register.html")
|
||||
messages.error(request, "Ya existe un usuario con este correo electrónico")
|
||||
return render(request, "tienda/register.html", {"form":form})
|
||||
|
||||
# Crear username a partir del email
|
||||
username = email.split("@")[0]
|
||||
|
||||
# Si el username ya existe, agregar un número
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
# Crear usuario
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
first_name=name
|
||||
username = username,
|
||||
email = email,
|
||||
password = password,
|
||||
first_name = name
|
||||
)
|
||||
|
||||
audit_logger.info(
|
||||
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
||||
user.id,
|
||||
@@ -339,8 +358,9 @@ def register(request: HttpRequest):
|
||||
tasks.enviar_correo_confirmacion.delay(user.id)
|
||||
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
||||
return redirect("index")
|
||||
|
||||
return render(request, "tienda/register.html")
|
||||
else:
|
||||
form = UserRegisterForm()
|
||||
return render(request, "tienda/register.html", {"form":form})
|
||||
|
||||
|
||||
def logout(request: HttpRequest):
|
||||
@@ -457,10 +477,24 @@ def _get_active_reservation_ids_for_request(request: HttpRequest):
|
||||
|
||||
|
||||
def _get_available_stock_by_product(product_ids, exclude_reservation_ids=None):
|
||||
"""Calcula stock disponible con bloqueo atómico para evitar race conditions."""
|
||||
_release_expired_stock_reservations()
|
||||
products = Product.objects.filter(id__in=product_ids)
|
||||
|
||||
if not product_ids:
|
||||
return {}
|
||||
|
||||
with transaction.atomic():
|
||||
# Bloquear productos a nivel de fila para evitar race conditions
|
||||
products = Product.objects.select_for_update().filter(id__in=product_ids)
|
||||
stocks = {product.id: product.stock for product in products}
|
||||
reserved = _get_reserved_quantities_by_product(product_ids, exclude_reservation_ids=exclude_reservation_ids)
|
||||
|
||||
# Las reservas se consultan dentro de la transacción atómica
|
||||
# _get_reserved_quantities_by_product hace una lectura consistente
|
||||
reserved = _get_reserved_quantities_by_product(
|
||||
product_ids,
|
||||
exclude_reservation_ids=exclude_reservation_ids
|
||||
)
|
||||
|
||||
return {
|
||||
product_id: max(stocks.get(product_id, 0) - reserved.get(product_id, 0), 0)
|
||||
for product_id in product_ids
|
||||
@@ -686,7 +720,7 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
||||
)
|
||||
|
||||
product_row = product_map.get(item.product_id)
|
||||
product_row.stock -= item.quantity
|
||||
product_row.stock = F('stock') - item.quantity
|
||||
product_row.save(update_fields=["stock"])
|
||||
|
||||
_invalidate_product_cache(product_ids)
|
||||
@@ -916,88 +950,32 @@ def enviar_mensaje_pedido(request: HttpRequest, item_id: int):
|
||||
|
||||
@login_required
|
||||
def crear_producto(request: HttpRequest):
|
||||
"""Crea un nuevo producto"""
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
briefdesc = request.POST.get("briefdesc")
|
||||
description = request.POST.get("description")
|
||||
price = request.POST.get("price")
|
||||
stock = request.POST.get("stock")
|
||||
category_id = request.POST.get("category")
|
||||
primary_image_file = request.FILES.get("primary_image")
|
||||
secondary_images_files = request.FILES.getlist("secondary_images")
|
||||
|
||||
# Validaciones
|
||||
if not all([name, description, price, stock, category_id]):
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
|
||||
try:
|
||||
price = float(price)
|
||||
if price < 0:
|
||||
raise ValueError("El precio no puede ser negativo")
|
||||
except ValueError:
|
||||
messages.error(request, "El precio debe ser un número válido.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
|
||||
try:
|
||||
stock = int(stock)
|
||||
if stock < 0:
|
||||
raise ValueError("El stock no puede ser negativo")
|
||||
except ValueError:
|
||||
messages.error(request, "El stock debe ser un número entero válido.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
|
||||
try:
|
||||
category = Category.objects.get(id=category_id)
|
||||
except Category.DoesNotExist:
|
||||
messages.error(request, "Categoría no válida.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
|
||||
# Crear imagen principal si se proporciona
|
||||
primary_image = None
|
||||
form = ProductForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
primary_image_file = form.cleaned_data.get("primary_image")
|
||||
image = None
|
||||
if primary_image_file:
|
||||
primary_image = Image.objects.create(
|
||||
name=f"{name}_principal",
|
||||
image=primary_image_file
|
||||
image = Image(
|
||||
name = f"{form.cleaned_data['name']}_principal",
|
||||
image = primary_image_file,
|
||||
)
|
||||
if stock > 4294967295:
|
||||
messages.error(request, "No se puede tener mas de 4294967295 existencias. Por favor, intentelo de nuevo")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
# Crear producto
|
||||
producto = Product.objects.create(
|
||||
name=name,
|
||||
briefdesc=briefdesc or "",
|
||||
description=description,
|
||||
price=price,
|
||||
stock=stock,
|
||||
category=category,
|
||||
primary_image=primary_image,
|
||||
creator=request.user
|
||||
image.save()
|
||||
producto: Product = Product(
|
||||
name = form.cleaned_data["name"],
|
||||
briefdesc = form.cleaned_data["briefdesc"],
|
||||
description = form.cleaned_data["description"],
|
||||
price = form.cleaned_data["price"],
|
||||
stock = form.cleaned_data["stock"],
|
||||
category = form.cleaned_data["category"],
|
||||
primary_image = image,
|
||||
creator = request.user
|
||||
)
|
||||
_invalidate_product_cache([producto.id])
|
||||
|
||||
# Agregar imágenes secundarias si se proporcionan
|
||||
if secondary_images_files:
|
||||
for idx, img_file in enumerate(secondary_images_files):
|
||||
secondary_img = Image.objects.create(
|
||||
name=f"{name}_secundaria_{idx+1}",
|
||||
image=img_file
|
||||
)
|
||||
producto.secondary_images.add(secondary_img)
|
||||
|
||||
messages.success(request, f"¡Producto '{name}' creado exitosamente!")
|
||||
return redirect("mis_productos")
|
||||
|
||||
# GET request - mostrar formulario
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
|
||||
producto.save()
|
||||
return redirect("/")
|
||||
else:
|
||||
form = ProductForm()
|
||||
return render(request, "tienda/crear_producto.html", {"form":form})
|
||||
|
||||
@login_required
|
||||
def editar_producto(request: HttpRequest, id: int):
|
||||
@@ -1005,74 +983,21 @@ def editar_producto(request: HttpRequest, id: int):
|
||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("name")
|
||||
briefdesc = request.POST.get("briefdesc")
|
||||
description = request.POST.get("description")
|
||||
price = request.POST.get("price")
|
||||
stock = request.POST.get("stock")
|
||||
category_id = request.POST.get("category")
|
||||
form = ProductEditForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
producto.name = form.cleaned_data["name"]
|
||||
producto.briefdesc = form.cleaned_data.get("briefdesc", "") or ""
|
||||
producto.description = form.cleaned_data["description"]
|
||||
producto.price = form.cleaned_data["price"]
|
||||
producto.stock = form.cleaned_data["stock"]
|
||||
producto.category = form.cleaned_data["category"]
|
||||
|
||||
primary_image_file = request.FILES.get("primary_image")
|
||||
secondary_images_files = request.FILES.getlist("secondary_images")
|
||||
|
||||
if not all([name, description, price, stock, category_id]):
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
try:
|
||||
price = float(price)
|
||||
if price < 0:
|
||||
raise ValueError("El precio no puede ser negativo")
|
||||
except ValueError:
|
||||
messages.error(request, "El precio debe ser un número válido.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
try:
|
||||
stock = int(stock)
|
||||
if stock < 0:
|
||||
raise ValueError("El stock no puede ser negativo")
|
||||
if stock > 4294967295:
|
||||
messages.error(request, "No se puede tener mas de 4294967295 de stock.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
except ValueError:
|
||||
messages.error(request, "El stock debe ser un número entero válido.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
try:
|
||||
category = Category.objects.get(id=category_id)
|
||||
except Category.DoesNotExist:
|
||||
messages.error(request, "Categoría no válida.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
producto.name = name
|
||||
producto.briefdesc = briefdesc or ""
|
||||
producto.description = description
|
||||
producto.price = price
|
||||
producto.stock = stock
|
||||
producto.category = category
|
||||
|
||||
if primary_image_file:
|
||||
primary_image = Image.objects.create(
|
||||
name=f"{name}_principal",
|
||||
name=f"{producto.name}_principal",
|
||||
image=primary_image_file
|
||||
)
|
||||
producto.primary_image = primary_image
|
||||
@@ -1084,17 +1009,29 @@ def editar_producto(request: HttpRequest, id: int):
|
||||
producto.secondary_images.clear()
|
||||
for idx, img_file in enumerate(secondary_images_files):
|
||||
secondary_img = Image.objects.create(
|
||||
name=f"{name}_secundaria_{idx+1}",
|
||||
name=f"{producto.name}_secundaria_{idx+1}",
|
||||
image=img_file
|
||||
)
|
||||
producto.secondary_images.add(secondary_img)
|
||||
|
||||
messages.success(request, f"¡Producto '{name}' actualizado exitosamente!")
|
||||
messages.success(request, f"¡Producto '{producto.name}' actualizado exitosamente!")
|
||||
return redirect("mis_productos")
|
||||
else:
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
else:
|
||||
initial = {
|
||||
"name": producto.name,
|
||||
"briefdesc": producto.briefdesc,
|
||||
"description": producto.description,
|
||||
"price": producto.price,
|
||||
"stock": producto.stock,
|
||||
"category": producto.category,
|
||||
}
|
||||
form = ProductEditForm(initial=initial)
|
||||
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"form": form,
|
||||
"producto": producto
|
||||
})
|
||||
|
||||
@@ -1113,6 +1050,55 @@ def borrar_producto(request: HttpRequest, id: int):
|
||||
messages.success(request, f"Producto '{nombre}' eliminado correctamente.")
|
||||
return redirect("mis_productos")
|
||||
|
||||
|
||||
@login_required
|
||||
def gestionar_imagenes(request: HttpRequest, id: int):
|
||||
"""Gestiona las imágenes secundarias de un producto"""
|
||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||
secondary_images = producto.secondary_images.all()
|
||||
form = SecondaryImageForm()
|
||||
|
||||
if request.method == "POST":
|
||||
form = SecondaryImageForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
image = Image(
|
||||
name = f"{producto.name}_secundaria_{secondary_images.count() + 1}",
|
||||
image = form.cleaned_data["image"],
|
||||
alt = form.cleaned_data.get("alt", "")
|
||||
)
|
||||
image.save()
|
||||
producto.secondary_images.add(image)
|
||||
_invalidate_product_cache([producto.id])
|
||||
messages.success(request, "Imagen añadida correctamente.")
|
||||
return redirect("gestionar_imagenes", id=producto.id)
|
||||
|
||||
return render(request, "tienda/gestionar_imagenes.html", {
|
||||
"producto": producto,
|
||||
"secondary_images": secondary_images,
|
||||
"form": form
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def eliminar_imagen_secundaria(request: HttpRequest, product_id: int, image_id: int):
|
||||
"""Elimina una imagen secundaria de un producto"""
|
||||
if request.method != "POST":
|
||||
messages.error(request, "Acción no permitida.")
|
||||
return redirect("gestionar_imagenes", id=product_id)
|
||||
|
||||
producto = get_object_or_404(Product, id=product_id, creator=request.user)
|
||||
image = get_object_or_404(Image, id=image_id)
|
||||
|
||||
if not producto.secondary_images.filter(id=image_id).exists():
|
||||
messages.error(request, "Esta imagen no pertenece al producto.")
|
||||
return redirect("gestionar_imagenes", id=product_id)
|
||||
|
||||
producto.secondary_images.remove(image)
|
||||
image.delete()
|
||||
_invalidate_product_cache([producto.id])
|
||||
messages.success(request, "Imagen eliminada correctamente.")
|
||||
return redirect("gestionar_imagenes", id=product_id)
|
||||
|
||||
@login_required
|
||||
def checkout(request: HttpRequest):
|
||||
cart = get_or_create_cart(request)
|
||||
@@ -1144,7 +1130,7 @@ def stripe_config(request):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@csrf_protect
|
||||
def create_checkout_session(request: HttpRequest):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
@@ -1483,6 +1469,7 @@ def paypal_execute(request: HttpRequest):
|
||||
# ==================== STRIPE PAYMENT INTENTS ====================
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_payment_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe PaymentIntent para el carrito actual.
|
||||
@@ -1566,6 +1553,7 @@ def crear_payment_intent(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def confirmar_pago_tarjeta(request: HttpRequest):
|
||||
"""
|
||||
Verificar que el PaymentIntent fue exitoso y crear el pedido.
|
||||
@@ -1639,6 +1627,7 @@ def confirmar_pago_tarjeta(request: HttpRequest):
|
||||
# ==================== PAYPAL ORDERS API ====================
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden de PayPal con el total del carrito actual (Orders API v2).
|
||||
@@ -1692,6 +1681,7 @@ def crear_orden_paypal(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def capturar_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Captura una orden de PayPal aprobada y crea el pedido en nuestra BD.
|
||||
@@ -1802,6 +1792,7 @@ def agregar_tarjeta(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe SetupIntent y retorna el client_secret para que el frontend
|
||||
@@ -1826,6 +1817,7 @@ def crear_setup_intent(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def confirmar_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Tras la confirmación del SetupIntent en el frontend, guarda la tarjeta.
|
||||
@@ -1927,6 +1919,7 @@ def agregar_paypal(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def crear_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden PayPal de 0.01 € para verificar/guardar la cuenta.
|
||||
@@ -1943,6 +1936,7 @@ def crear_orden_paypal_setup(request: HttpRequest):
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def capturar_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Captura la orden de verificación de PayPal y guarda la cuenta del usuario.
|
||||
@@ -2051,58 +2045,59 @@ def mis_recibos(request: HttpRequest):
|
||||
def editar_perfil(request: HttpRequest):
|
||||
"""Edita la información del perfil del usuario"""
|
||||
if request.method == "POST":
|
||||
first_name = request.POST.get("first_name", "").strip()
|
||||
last_name = request.POST.get("last_name", "").strip()
|
||||
email = request.POST.get("email", "").strip()
|
||||
form = EditProfileForm(request.POST)
|
||||
if form.is_valid():
|
||||
email = form.cleaned_data["email"]
|
||||
|
||||
# Validar email único (excepto el propio)
|
||||
if email != request.user.email and User.objects.filter(email=email).exists():
|
||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
||||
|
||||
# Actualizar usuario
|
||||
request.user.first_name = first_name
|
||||
request.user.last_name = last_name
|
||||
request.user.first_name = form.cleaned_data["first_name"]
|
||||
request.user.last_name = form.cleaned_data["last_name"]
|
||||
request.user.email = email
|
||||
request.user.save()
|
||||
|
||||
messages.success(request, "Perfil actualizado correctamente.")
|
||||
return redirect("portal_usuario")
|
||||
else:
|
||||
initial = {
|
||||
"first_name": request.user.first_name,
|
||||
"last_name": request.user.last_name,
|
||||
"email": request.user.email,
|
||||
}
|
||||
form = EditProfileForm(initial=initial)
|
||||
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
return render(request, "tienda/editar_perfil.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
def cambiar_contrasena(request: HttpRequest):
|
||||
"""Cambia la contraseña del usuario"""
|
||||
if request.method == "POST":
|
||||
current_password = request.POST.get("current_password")
|
||||
new_password = request.POST.get("new_password")
|
||||
confirm_password = request.POST.get("confirm_password")
|
||||
form = ChangePasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
current_password = form.cleaned_data["current_password"]
|
||||
new_password = form.cleaned_data["new_password"]
|
||||
|
||||
# Verificar contraseña actual
|
||||
if not request.user.check_password(current_password):
|
||||
messages.error(request, "La contraseña actual es incorrecta.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
|
||||
# Validar nueva contraseña
|
||||
if new_password != confirm_password:
|
||||
messages.error(request, "Las contraseñas nuevas no coinciden.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||
|
||||
if len(new_password) < 8:
|
||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||
return render(request, "tienda/editar_perfil.html")
|
||||
return render(request, "tienda/editar_perfil.html", {"password_form": ChangePasswordForm()})
|
||||
|
||||
# Cambiar contraseña
|
||||
request.user.set_password(new_password)
|
||||
request.user.save()
|
||||
|
||||
# Mantener la sesión activa
|
||||
auth_login(request, request.user)
|
||||
|
||||
messages.success(request, "Contraseña actualizada correctamente.")
|
||||
return redirect("portal_usuario")
|
||||
else:
|
||||
messages.error(request, "Las contraseñas nuevas no coinciden o son inválidas.")
|
||||
return render(request, "tienda/editar_perfil.html", {"password_form": form})
|
||||
|
||||
return redirect("editar_perfil")
|
||||
|
||||
@@ -2121,45 +2116,39 @@ def direcciones_usuario(request: HttpRequest):
|
||||
def crear_direccion(request: HttpRequest):
|
||||
"""Crea una nueva dirección de entrega"""
|
||||
if request.method == "POST":
|
||||
full_name = request.POST.get("full_name", "").strip()
|
||||
address_line_1 = request.POST.get("address_line_1", "").strip()
|
||||
address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||
city = request.POST.get("city", "").strip()
|
||||
postal_code = request.POST.get("postal_code", "").strip()
|
||||
country = SHIPPING_COUNTRY
|
||||
phone = request.POST.get("phone", "").strip()
|
||||
is_default = request.POST.get("is_default") == "on"
|
||||
|
||||
# Validaciones
|
||||
if not all([full_name, address_line_1, city, postal_code, phone]):
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||
form = ShippingAddressForm(request.POST)
|
||||
if form.is_valid():
|
||||
city = form.cleaned_data["city"]
|
||||
postal_code = form.cleaned_data["postal_code"]
|
||||
|
||||
if not _is_almeria_city(city):
|
||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||
|
||||
if not _is_almeria_postal_code(postal_code):
|
||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||
|
||||
# Crear dirección
|
||||
ShippingAddress.objects.create(
|
||||
user=request.user,
|
||||
full_name=full_name,
|
||||
address_line_1=address_line_1,
|
||||
address_line_2=address_line_2,
|
||||
full_name=form.cleaned_data["full_name"],
|
||||
address_line_1=form.cleaned_data["address_line_1"],
|
||||
address_line_2=form.cleaned_data.get("address_line_2", "") or "",
|
||||
city=city,
|
||||
postal_code=postal_code,
|
||||
country=country,
|
||||
phone=phone,
|
||||
is_default=is_default
|
||||
country=SHIPPING_COUNTRY,
|
||||
phone=form.cleaned_data["phone"],
|
||||
is_default=form.cleaned_data.get("is_default", False)
|
||||
)
|
||||
|
||||
messages.success(request, "Dirección creada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
else:
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
else:
|
||||
form = ShippingAddressForm()
|
||||
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context())
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(form=form))
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -2168,34 +2157,47 @@ def editar_direccion(request: HttpRequest, id: int):
|
||||
direccion = get_object_or_404(ShippingAddress, id=id, user=request.user)
|
||||
|
||||
if request.method == "POST":
|
||||
direccion.full_name = request.POST.get("full_name", "").strip()
|
||||
direccion.address_line_1 = request.POST.get("address_line_1", "").strip()
|
||||
direccion.address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||
direccion.city = request.POST.get("city", "").strip()
|
||||
direccion.postal_code = request.POST.get("postal_code", "").strip()
|
||||
direccion.country = SHIPPING_COUNTRY
|
||||
direccion.phone = request.POST.get("phone", "").strip()
|
||||
direccion.is_default = request.POST.get("is_default") == "on"
|
||||
form = ShippingAddressForm(request.POST)
|
||||
if form.is_valid():
|
||||
city = form.cleaned_data["city"]
|
||||
postal_code = form.cleaned_data["postal_code"]
|
||||
|
||||
# Validaciones
|
||||
if not all([direccion.full_name, direccion.address_line_1, direccion.city,
|
||||
direccion.postal_code, direccion.phone]):
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||
if not _is_almeria_city(city):
|
||||
messages.error(request, "El pueblo/ciudad debe pertenece a la provincia de Almería.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||
|
||||
if not _is_almeria_city(direccion.city):
|
||||
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||
|
||||
if not _is_almeria_postal_code(direccion.postal_code):
|
||||
if not _is_almeria_postal_code(postal_code):
|
||||
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||
|
||||
direccion.full_name = form.cleaned_data["full_name"]
|
||||
direccion.address_line_1 = form.cleaned_data["address_line_1"]
|
||||
direccion.address_line_2 = form.cleaned_data.get("address_line_2", "") or ""
|
||||
direccion.city = city
|
||||
direccion.postal_code = postal_code
|
||||
direccion.country = SHIPPING_COUNTRY
|
||||
direccion.phone = form.cleaned_data["phone"]
|
||||
direccion.is_default = form.cleaned_data.get("is_default", False)
|
||||
|
||||
direccion.save()
|
||||
messages.success(request, "Dirección actualizada correctamente.")
|
||||
return redirect("direcciones_usuario")
|
||||
else:
|
||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||
else:
|
||||
initial = {
|
||||
"full_name": direccion.full_name,
|
||||
"address_line_1": direccion.address_line_1,
|
||||
"address_line_2": direccion.address_line_2,
|
||||
"city": direccion.city,
|
||||
"postal_code": direccion.postal_code,
|
||||
"country": direccion.country,
|
||||
"phone": direccion.phone,
|
||||
"is_default": direccion.is_default,
|
||||
}
|
||||
form = ShippingAddressForm(initial=initial)
|
||||
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion, form=form))
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -2281,9 +2283,12 @@ def ayuda(request: HttpRequest):
|
||||
|
||||
def reset_password(request: HttpRequest):
|
||||
if request.method == "GET":
|
||||
return render(request, "tienda/reset_password.html", {})
|
||||
form = ResetPasswordForm()
|
||||
return render(request, "tienda/reset_password.html", {"form": form})
|
||||
else:
|
||||
tasks.enviar_correo_recuperacion.delay(request.POST["email"])
|
||||
form = ResetPasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
tasks.enviar_correo_recuperacion.delay(form.cleaned_data["email"])
|
||||
messages.info(request, "Si tienes una cuenta con ese correo electronico, se ha enviado un correo con un enlace")
|
||||
return render(request, "tienda/index.html", {})
|
||||
|
||||
@@ -2297,22 +2302,19 @@ def reset_password_phase2(request: HttpRequest, code: str):
|
||||
|
||||
|
||||
if request.method == "GET":
|
||||
return render(request, "tienda/reset_password_phase2.html", {
|
||||
"code": code
|
||||
})
|
||||
form = ResetPasswordPhase2Form()
|
||||
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||
elif request.method == "POST":
|
||||
password = request.POST["password"]
|
||||
vpassword = request.POST["verify_password"]
|
||||
if password != vpassword:
|
||||
messages.error(request, "Las contraseñas no coinciden")
|
||||
return render(request, "tienda/reset_password_phase2.html", {"code": code})
|
||||
|
||||
form = ResetPasswordPhase2Form(request.POST)
|
||||
if form.is_valid():
|
||||
user = ver_code.user
|
||||
user.set_password(password)
|
||||
user.set_password(form.cleaned_data["password"])
|
||||
user.save()
|
||||
ver_code.delete() # Delete Verification code after changing password
|
||||
ver_code.delete()
|
||||
messages.success(request, "Se ha cambiado la contraseña!")
|
||||
return redirect(reverse("index"))
|
||||
|
||||
else:
|
||||
messages.error(request, "Las contraseñas no coinciden")
|
||||
return render(request, "tienda/reset_password_phase2.html", {"form": form, "code": code})
|
||||
else:
|
||||
raise Http404()
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
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 = "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-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.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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-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-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.33.1" },
|
||||
{ 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 = "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.33.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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 = "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