diff --git a/proyecto/jinja2.py b/proyecto/jinja2.py deleted file mode 100644 index 0916e3b..0000000 --- a/proyecto/jinja2.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/proyecto/settings.py b/proyecto/settings.py index c866b51..1a9f811 100644 --- a/proyecto/settings.py +++ b/proyecto/settings.py @@ -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' @@ -429,4 +423,11 @@ CELERY_RESULT_SERIALIZER = 'json' SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") USE_X_FORWARDED_HOST = True -SECURE_REFERER_POLICY = "strict-origin-when-cross-origin" \ No newline at end of file +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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b1b1161..51c7d4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/tienda/forms.py b/tienda/forms.py new file mode 100644 index 0000000..0cb5655 --- /dev/null +++ b/tienda/forms.py @@ -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.") \ No newline at end of file diff --git a/tienda/migrations/0008_alter_product_briefdesc_alter_product_description.py b/tienda/migrations/0008_alter_product_briefdesc_alter_product_description.py new file mode 100644 index 0000000..cbef791 --- /dev/null +++ b/tienda/migrations/0008_alter_product_briefdesc_alter_product_description.py @@ -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), + ), + ] diff --git a/tienda/models.py b/tienda/models.py index 20a5d4f..d3136e1 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -86,7 +86,7 @@ class Product(models.Model): name = models.CharField(max_length=200, 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=1000) + 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) diff --git a/tienda/tasks.py b/tienda/tasks.py index 680016f..480b9d9 100644 --- a/tienda/tasks.py +++ b/tienda/tasks.py @@ -11,21 +11,19 @@ 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( - 'emails/ban.html', + 'tienda/emails/ban.html', { }, - using='jinja2' ) send_hemail(email_usuario, "Cuenta Bloqueada", html_content, "Tu cuenta ha sido bloqueada...") @@ -33,9 +31,8 @@ def banear_usuario(email_usuario: str): @shared_task def desbanear_usuario(email_usuario: str): html_content = render_to_string( - 'emails/unban.html', + 'tienda/emails/unban.html', {}, - using='jinja2' ) send_hemail(email_usuario, "Cuenta Desbloqueada", html_content, "Tu cuenta ha sido desbloqueada...") @@ -67,14 +64,13 @@ 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...") diff --git a/tienda/templates/tienda/crear_producto.html b/tienda/templates/tienda/crear_producto.html index 87a5b34..22a7a7f 100644 --- a/tienda/templates/tienda/crear_producto.html +++ b/tienda/templates/tienda/crear_producto.html @@ -13,74 +13,7 @@
{% csrf_token %} - - -
- - -
- - -
- - -
Opcional. Se mostrará en las vistas de listado de productos.
-
- - -
- - -
- - -
- -
- - -
-
- - -
- - -
Cantidad máxima que podrán comprar los clientes.
-
- - -
- - -
- - -
- - -
Opcional. Esta será la imagen destacada del producto.
-
- - -
- - -
Opcional. Puedes seleccionar múltiples imágenes adicionales.
-
- + {{ form }}
Cancelar diff --git a/tienda/templates/tienda/editar_direccion.html b/tienda/templates/tienda/editar_direccion.html index d5bc6e3..b4d4855 100644 --- a/tienda/templates/tienda/editar_direccion.html +++ b/tienda/templates/tienda/editar_direccion.html @@ -21,52 +21,7 @@
{% csrf_token %} -
- - -
-
- - -
-
- - -
-
-
- - - - {% for town in almeria_municipalities %} - - {% endfor %} - -
Selecciona o escribe un municipio de la provincia de Almería.
-
- El pueblo/ciudad debe pertenecer a la provincia de Almería. -
-
-
- - -
Solo aceptamos códigos postales de Almería (04xxx).
-
-
-
- - -
-
- - -
-
- - -
+ {{ form.as_p }}
Cancelar @@ -80,7 +35,6 @@