diff --git a/essenza/.gitignore b/.gitignore similarity index 79% rename from essenza/.gitignore rename to .gitignore index 6427dc2d..e3b2dd45 100644 --- a/essenza/.gitignore +++ b/.gitignore @@ -7,16 +7,18 @@ __pycache__/ # Entorno virtual venv/ .env/ +.venv # Archivos de base de datos -db.sqlite3 # Archivos de migraciones (si quieres omitirlos) -*/migrations/__pycache__/ -*/migrations/*.py +**/migrations/__pycache__/ +**/migrations/*.py # Archivos de configuración de VSCode .vscode/ # Archivos de Django estáticos staticfiles/ + +**/*.sqlite3 \ No newline at end of file diff --git "a/docs/Iteraci\303\263n 1/Informes de seguimiento de trabajo.pdf" "b/docs/Iteraci\303\263n 1/Informes de seguimiento de trabajo.pdf" new file mode 100644 index 00000000..f518d953 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Informes de seguimiento de trabajo.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/PRODUCT BACKLOG.pdf" "b/docs/Iteraci\303\263n 1/PRODUCT BACKLOG.pdf" new file mode 100644 index 00000000..ef63c164 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/PRODUCT BACKLOG.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Pol\303\255tica de ramas y mensajes commit.pdf" "b/docs/Iteraci\303\263n 1/Pol\303\255tica de ramas y mensajes commit.pdf" new file mode 100644 index 00000000..6e22a0fb Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Pol\303\255tica de ramas y mensajes commit.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/REGISTRO DE CAMBIOS_v1.0.pdf" "b/docs/Iteraci\303\263n 1/REGISTRO DE CAMBIOS_v1.0.pdf" new file mode 100644 index 00000000..1dc549d3 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/REGISTRO DE CAMBIOS_v1.0.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/REGISTRO DE DECISIONES_v1.0.pdf" "b/docs/Iteraci\303\263n 1/REGISTRO DE DECISIONES_v1.0.pdf" new file mode 100644 index 00000000..518ac1c5 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/REGISTRO DE DECISIONES_v1.0.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/REGISTRO DE INCIDENCIAS_v1.0.pdf" "b/docs/Iteraci\303\263n 1/REGISTRO DE INCIDENCIAS_v1.0.pdf" new file mode 100644 index 00000000..ca3d1ec2 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/REGISTRO DE INCIDENCIAS_v1.0.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/SPRINT RETROSPECTIVE.pdf" "b/docs/Iteraci\303\263n 1/SPRINT RETROSPECTIVE.pdf" new file mode 100644 index 00000000..8f6ab912 Binary files /dev/null and "b/docs/Iteraci\303\263n 1/SPRINT RETROSPECTIVE.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/SPRINT REVIEW.pdf" "b/docs/Iteraci\303\263n 1/SPRINT REVIEW.pdf" new file mode 100644 index 00000000..7dcbd94f Binary files /dev/null and "b/docs/Iteraci\303\263n 1/SPRINT REVIEW.pdf" differ diff --git "a/docs/Iteraci\303\263n 1/Sprint Backlog 1.pdf" "b/docs/Iteraci\303\263n 1/Sprint Backlog 1.pdf" new file mode 100644 index 00000000..3b8079dc Binary files /dev/null and "b/docs/Iteraci\303\263n 1/Sprint Backlog 1.pdf" differ diff --git a/essenza/db.sqlite3 b/essenza/db.sqlite3 new file mode 100644 index 00000000..836a999a Binary files /dev/null and b/essenza/db.sqlite3 differ diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index a05738be..662ee0e4 100644 --- a/essenza/essenza/settings.py +++ b/essenza/essenza/settings.py @@ -37,11 +37,16 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'user', + 'product', + 'order', + 'info', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -54,11 +59,12 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ BASE_DIR / 'templates' ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', + 'django.template.context_processors.i18n', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], @@ -102,7 +108,7 @@ # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'es' TIME_ZONE = 'UTC' @@ -115,8 +121,15 @@ # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' - +STATICFILES_DIRS = [BASE_DIR / 'static'] # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# ----------------------------------------------------------------- +# SOLUCIÓN AL ERROR E304 +# Especifica que nuestro modelo 'Usuario' en la app 'user' +# es el modelo de autenticación oficial. +# ----------------------------------------------------------------- +AUTH_USER_MODEL = 'user.Usuario' \ No newline at end of file diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index 80e86799..e08e7df7 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -1,6 +1,9 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.http import HttpResponse +from info.views import info_view +from product.views import EscaparateView +import user def home(request): html = """ @@ -14,6 +17,7 @@ def home(request): text-align: center; padding-top: 100px; color: #444; + position: relative; } h1 { color: #c06b3e; @@ -24,12 +28,70 @@ def home(request): font-size: 20px; color: #555; } + .info-button { + position: absolute; + top: 20px; + left: 20px; + width: 30px; + height: 30px; + background-color: #c06b3e; + border-radius: 50%; + text-align: center; + line-height: 30px; + font-size: 18px; + font-weight: bold; + color: white; + text-decoration: none; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + transition: background-color 0.3s; + } + .info-button:hover { background-color: #a35a34; } + + .button-container { + margin-top: 30px; /* Espacio desde el texto de arriba */ + display: flex; + flex-direction: column; /* Apila los botones verticalmente */ + align-items: center; /* Centra los botones horizontalmente */ + gap: 20px; /* Espacio automático entre cada botón */ + } + + .action-button { + padding: 15px 35px; + background-color: #c06b3e; + color: white; + font-size: 15px; + font-weight: bold; + border: none; + border-radius: 10px; + cursor: pointer; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + text-decoration: none; + transition: background-color 0.3s, transform 0.2s; + + display: block; + width: 300px; /* Ancho fijo para que se vean uniformes */ + box-sizing: border-box; /* Para que el padding no afecte el ancho */ + } + + .action-button:hover { + background-color: #a35a34; + transform: scale(1.05); /* Efecto de zoom simple */ + } + + i

Bienvenidos a Essenza

Tu espacio online de cosmética natural, belleza y cuidado personal.

Explora nuestros productos, descubre nuevas fragancias y disfruta de la experiencia Essenza 🌸

+ +
+ Registro + Iniciar sesión + Continuar como invitado +
""" @@ -37,6 +99,8 @@ def home(request): urlpatterns = [ path('', home, name='home'), + path('info/', info_view, name='info-home'), + path("user/", include("user.urls")), path('admin/', admin.site.urls), -] - + path('escaparate/', EscaparateView.as_view(), name='escaparate') +] \ No newline at end of file diff --git a/essenza/info/__init__.py b/essenza/info/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/essenza/info/admin.py b/essenza/info/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/essenza/info/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/essenza/info/apps.py b/essenza/info/apps.py new file mode 100644 index 00000000..4d091a67 --- /dev/null +++ b/essenza/info/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InfoConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'info' diff --git a/essenza/info/models.py b/essenza/info/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/essenza/info/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/essenza/info/tests.py b/essenza/info/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/essenza/info/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/essenza/info/views.py b/essenza/info/views.py new file mode 100644 index 00000000..24295706 --- /dev/null +++ b/essenza/info/views.py @@ -0,0 +1,101 @@ +# views.py (donde se encuentra la función info_view) + +from django.http import HttpResponse +# from django.shortcuts import render # No se usa ya que el HTML está en HttpResponse + +def info_view(request): + # CIF inventado para la S.L. Essenza: B87654321 + + html = """ + + + + + Condiciones Legales e Información de Essenza + + + + +
+

Información Legal y Condiciones de Venta de Essenza

+

Volver a la Página Principal

+
+ +

1. Identificación y Aviso Legal

+

Essenza S.L. es la denominación social y el nombre comercial de la tienda online dedicada a la venta de productos de cosmética y cuidado personal, rigiéndose su actividad por la legislación española vigente. En cumplimiento del deber de información recogido en la Ley 34/2002 de Servicios de la Sociedad de la Información y Comercio Electrónico (LSSI-CE) y en el Real Decreto 85/2018 sobre productos cosméticos, a continuación se detallan los datos de identificación del titular de este sitio web:

+ + +

2. Condiciones Generales de Venta (CGV)

+

Estas condiciones regulan la relación contractual de compraventa entre Essenza y usted desde el momento en que realiza un pedido en nuestra web. La formalización de un pedido implica la lectura, comprensión y aceptación expresa de estas Condiciones Generales de Venta en su totalidad, siendo de obligado cumplimiento para ambas partes.

+ +

2.1. Información, Trazabilidad y Calidad del Producto

+

Essenza garantiza que todos sus productos han pasado rigurosos controles de calidad y cumplen con los requisitos de seguridad establecidos por la normativa europea (Reglamento CE 1223/2009). La información detallada de ingredientes, modo de uso, precauciones y el Periodo Después de la Apertura (PAO) se encuentra de forma clara y accesible en la ficha de cada producto y en su etiquetado. Mantenemos un estricto control de trazabilidad para garantizar la seguridad de todos los artículos de cosmética que comercializamos.

+ +

2.2. Proceso de Compra y Precio

+

El proceso de compra se considera finalizado y vinculante una vez que el pago ha sido confirmado. Todos los precios mostrados en el sitio web están expresados en euros (€) e incluyen el Impuesto sobre el Valor Añadido (I.V.A.) legalmente aplicable. Los gastos de envío serán calculados en base al peso y la dirección de entrega, siendo detallados y aceptados por el Cliente antes de la confirmación final de la compra.

+ +

2.3. Envíos, Plazos y Riesgos

+ + +

2.4. Política de Desistimiento y Devoluciones

+

De acuerdo con la Ley General para la Defensa de los Consumidores y Usuarios, el Cliente dispone de 14 días naturales desde la recepción del pedido para ejercer su derecho de desistimiento. Condición Específica para Cosmética: Por estrictas razones de higiene, seguridad y protección de la salud, no se admitirá la devolución de productos cosméticos que hayan sido abiertos, desprecintados o usados. En caso de desistimiento válido, los costes directos de la devolución (envío de vuelta) correrán a cargo del cliente, salvo si la causa es un producto defectuoso o un error de Essenza.

+ +

3. Política de Privacidad (RGPD)

+

En Essenza, su privacidad es nuestra prioridad. Los datos personales recabados (nombre, dirección, email, datos de pago) a través de la web son tratados bajo la legitimación de la ejecución de un contrato (para gestionar su pedido) o el consentimiento (para el newsletter). Nos comprometemos a no ceder sus datos a terceros, salvo obligación legal. Usted puede ejercer en todo momento sus derechos de Acceso, Rectificación, Supresión, Limitación, Portabilidad y Oposición (ARSLOP) enviando una solicitud a info@essenza.com.

+ + + + + + """ + return HttpResponse(html) \ No newline at end of file diff --git a/essenza/order/__init__.py b/essenza/order/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/essenza/order/admin.py b/essenza/order/admin.py new file mode 100644 index 00000000..54957e86 --- /dev/null +++ b/essenza/order/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Order, OrderProduct, Status +# Register your models here. + +admin.site.register(Order) +admin.site.register(OrderProduct) diff --git a/essenza/order/apps.py b/essenza/order/apps.py new file mode 100644 index 00000000..42888e45 --- /dev/null +++ b/essenza/order/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'order' diff --git a/essenza/order/models.py b/essenza/order/models.py new file mode 100644 index 00000000..d623635f --- /dev/null +++ b/essenza/order/models.py @@ -0,0 +1,27 @@ +from django.db import models + +# Create your models here. +class Status(models.TextChoices): + PENDING = 'pending', 'Pending' + PAID = 'paid', 'Paid' + SHIPPED = 'shipped', 'Shipped' + +class Order(models.Model): + user = models.ForeignKey('user.Usuario', on_delete=models.CASCADE, related_name='orders') + adress = models.CharField(max_length=255) + placed_at = models.DateTimeField(auto_now=True) + total_price = models.DecimalField(max_digits=10, decimal_places=2) + status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING) + + def __str__(self): + return f"Order {self.id} by {self.user.email}" + + +class OrderProduct(models.Model): + order = models.ForeignKey('order.Order', on_delete=models.CASCADE, related_name='order_products') + product = models.ForeignKey('product.Product', on_delete=models.CASCADE, related_name='product_orders') + quantity = models.IntegerField() + unity_price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} of {self.product.name} in order {self.order.id}" diff --git a/essenza/order/tests.py b/essenza/order/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/essenza/order/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/essenza/order/views.py b/essenza/order/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/essenza/order/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/essenza/product/__init__.py b/essenza/product/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/essenza/product/admin.py b/essenza/product/admin.py new file mode 100644 index 00000000..3617350c --- /dev/null +++ b/essenza/product/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Category, Product + +# Register your models here. +admin.site.register(Product) diff --git a/essenza/product/apps.py b/essenza/product/apps.py new file mode 100644 index 00000000..235a3339 --- /dev/null +++ b/essenza/product/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProductConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'product' diff --git a/essenza/product/models.py b/essenza/product/models.py new file mode 100644 index 00000000..b2cfaafa --- /dev/null +++ b/essenza/product/models.py @@ -0,0 +1,22 @@ +from django.db import models + +# Create your models here. +class Category(models.TextChoices): + MAQUILLAJE = 'maquillaje', 'Maquillaje' + TRATAMIENTO = 'tratamiento', 'Tratamiento' + CABELLO = 'cabello', 'Cabello' + PERFUME = 'perfume', 'Perfume' + +class Product(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + categoria = models.CharField(max_length=20, choices=Category.choices) + brand = models.CharField(max_length=255) + price = models.DecimalField(max_digits=10, decimal_places=2) + foto = models.ImageField(upload_to='profile_pics/', null=True, blank=True) + stock = models.IntegerField() + is_active = models.BooleanField(default=False) + + def __str__(self): + return self.name + diff --git a/essenza/product/tests.py b/essenza/product/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/essenza/product/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/essenza/product/views.py b/essenza/product/views.py new file mode 100644 index 00000000..a99cebae --- /dev/null +++ b/essenza/product/views.py @@ -0,0 +1,6 @@ +from django.shortcuts import render +from django.views import View + +class EscaparateView(View): + def get(self, request): + return render(request, 'product/escaparate.html') diff --git a/essenza/profile_pics/img1.jpg b/essenza/profile_pics/img1.jpg new file mode 100644 index 00000000..64ca02d7 Binary files /dev/null and b/essenza/profile_pics/img1.jpg differ diff --git a/essenza/profile_pics/user1.avif b/essenza/profile_pics/user1.avif new file mode 100644 index 00000000..9c6e5cd3 Binary files /dev/null and b/essenza/profile_pics/user1.avif differ diff --git a/essenza/profile_pics/user2.avif b/essenza/profile_pics/user2.avif new file mode 100644 index 00000000..319b52c6 Binary files /dev/null and b/essenza/profile_pics/user2.avif differ diff --git a/essenza/profile_pics/user3.avif b/essenza/profile_pics/user3.avif new file mode 100644 index 00000000..3e2938c5 Binary files /dev/null and b/essenza/profile_pics/user3.avif differ diff --git a/essenza/static/images/img2.avif b/essenza/static/images/img2.avif new file mode 100644 index 00000000..5df7da0b Binary files /dev/null and b/essenza/static/images/img2.avif differ diff --git a/essenza/templates/product/escaparate.html b/essenza/templates/product/escaparate.html new file mode 100644 index 00000000..5fbbd77b --- /dev/null +++ b/essenza/templates/product/escaparate.html @@ -0,0 +1,130 @@ +{% load static %} + + + + + Escaparate · Essenza + + + + + +
+
ESSENZA
+ + + + {% if user.is_authenticated %} + +
+ {% csrf_token %} + +
+ + {% else %} + + + Log in + + + {% endif %} +
+ \ No newline at end of file diff --git a/essenza/templates/user/login.html b/essenza/templates/user/login.html new file mode 100644 index 00000000..5afa3d00 --- /dev/null +++ b/essenza/templates/user/login.html @@ -0,0 +1,95 @@ +{% load static %} + + + + + Iniciar sesión · Essenza + + + + + + + +
+

ESSENZA

+ +
+
+ {% csrf_token %} + + + {{ form.email }} + + + {{ form.password }} + + {% if form.non_field_errors %} +

{{ form.non_field_errors.0 }}

+ {% endif %} + + + + +
+
+
+ + + diff --git a/essenza/templates/user/register.html b/essenza/templates/user/register.html new file mode 100644 index 00000000..1ff83b22 --- /dev/null +++ b/essenza/templates/user/register.html @@ -0,0 +1,176 @@ +{% load static %} + + + + + Registro · Essenza + + + + +
+

ESSENZA

+
+
+ {% csrf_token %} + + + {{ form.first_name }} + + + {{ form.last_name }} + + + {{ form.username }} + + + {{ form.email }} + + + {{ form.password1 }} + + + {{ form.password2 }} + + + {{ form.foto }} + + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + + + + +
+
+
+ + \ No newline at end of file diff --git a/essenza/user/__init__.py b/essenza/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/essenza/user/admin.py b/essenza/user/admin.py new file mode 100644 index 00000000..34516ebb --- /dev/null +++ b/essenza/user/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Role, Usuario +# Register your models here. + +admin.site.register(Usuario) diff --git a/essenza/user/apps.py b/essenza/user/apps.py new file mode 100644 index 00000000..36cce4c8 --- /dev/null +++ b/essenza/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user' diff --git a/essenza/user/forms.py b/essenza/user/forms.py new file mode 100644 index 00000000..70b77faf --- /dev/null +++ b/essenza/user/forms.py @@ -0,0 +1,38 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm +from .models import Usuario + +class LoginForm(forms.Form): + email = forms.CharField( + label="Correo electrónico o usuario", + widget=forms.TextInput(attrs={"placeholder": "Introduce tu correo electrónico"}) + ) + password = forms.CharField( + label="Contraseña", + widget=forms.PasswordInput(attrs={"placeholder": "Introduce tu contraseña"}) + ) + +class RegisterForm(UserCreationForm): + + first_name = forms.CharField( + label="Nombre", + required=True + ) + last_name = forms.CharField( + label="Apellidos", + required=True + ) + email = forms.EmailField( + label="Correo electrónico", + required=True + ) + foto = forms.ImageField( + label="Foto (Opcional)", + required=False + ) + + class Meta(UserCreationForm.Meta): + + model = Usuario + + fields = UserCreationForm.Meta.fields + ('first_name', 'last_name', 'email', 'foto') diff --git a/essenza/user/models.py b/essenza/user/models.py new file mode 100644 index 00000000..7a92bfbf --- /dev/null +++ b/essenza/user/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + +class Role(models.TextChoices): + ADMIN = 'admin', 'Admin' + USER = 'user', 'User' + +class Usuario(AbstractUser): + foto = models.ImageField(upload_to='images/', null=True, blank=True) + role = models.CharField(max_length=10, choices=Role.choices, default=Role.USER) + + email = models.EmailField(unique=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + def __str__(self): + return self.email \ No newline at end of file diff --git a/essenza/user/tests.py b/essenza/user/tests.py new file mode 100644 index 00000000..a65bb48e --- /dev/null +++ b/essenza/user/tests.py @@ -0,0 +1,218 @@ +import tempfile +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth import get_user_model +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +import io +import os + + +User = get_user_model() + +class LoginViewTests(TestCase): + def setUp(self): + # usuario de prueba + self.username = "user1" + self.email = "user1@example.com" + self.password = "pass1234" + self.user = User.objects.create_user( + username=self.username, + email=self.email, + password=self.password + ) + self.login_url = reverse("login") + self.home_url = reverse("home") + self.escaparate_url = reverse("escaparate") + + #1. comprueba que la pagina de login carga correctamente + def test_get_login_page_returns_200(self): + resp = self.client.get(self.login_url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Iniciar sesión") + self.assertContains(resp, "ESSENZA") + + #2. si email y contraseña validas redirige al escaparate + def test_login_with_valid_email_redirects_escaparate(self): + data = {"email": self.email, "password": self.password} + resp = self.client.post(self.login_url, data, follow=False) + self.assertEqual(resp.status_code, 302, resp.content) + self.assertEqual(resp["Location"], self.escaparate_url) + + #3. si email y contraseña no validas muestra error + def test_login_with_invalid_passwordAndEmail_shows_error(self): + data = {"email": "wrong", "password": "wrong"} + resp = self.client.post(self.login_url, data) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Usuario o contraseña incorrectos") + + #4. si email no es valido muestra error + def test_login_with_invalid_email_shows_error(self): + data = {"email": "wrong", "password": self.password} + resp = self.client.post(self.login_url, data) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Usuario o contraseña incorrectos") + + #5. si contraseña no es valida muestra error + def test_login_with_invalid_password_shows_error(self): + data = {"email": self.email, "password": "wrong"} + resp = self.client.post(self.login_url, data) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Usuario o contraseña incorrectos") + + +class RegisterViewTests(TestCase): + def setUp(self): + self.register_url = reverse("register") + self.escaparate_url = reverse("escaparate") + self.initial_user_count = User.objects.count() + + # Datos para un nuevo usuario de prueba + self.valid_data = { + 'first_name': 'Juan', + 'last_name': 'Perez', + 'username': 'nuevo_usuario', + 'email': 'nuevo@ejemplo.com', + 'password1': 'PasswordSeguro123', + 'password2': 'PasswordSeguro123', + } + + #1. Comprueba que la página de registro carga correctamente + def test_get_register_page_returns_200(self): + resp = self.client.get(self.register_url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Crear cuenta") + self.assertContains(resp, "ESSENZA") + + #2. Registro con datos válidos y redirige al escaparate (302) + def test_successful_registration_redirects_and_creates_user(self): + data = self.valid_data.copy() + resp = self.client.post(self.register_url, data, follow=False) + + self.assertEqual(resp.status_code, 302) + self.assertEqual(resp["Location"], self.escaparate_url) + self.assertEqual(User.objects.count(), self.initial_user_count + 1) + + new_user = User.objects.get(email=data['email']) + self.assertTrue(new_user.check_password(data['password1'])) # La contraseña está hasheada + + + #3. Registro con email duplicado muestra error + def test_registration_with_duplicate_email_shows_error(self): + User.objects.create_user(username='test', email=self.valid_data['email'], password='test') # Usuario previo creado con mismo email + data = self.valid_data.copy() + resp = self.client.post(self.register_url, data) #Intento de registro con el mismo email + + self.assertEqual(User.objects.count(), self.initial_user_count + 1) #Solo se añade el usuario creado antes + self.assertContains(resp, "Ya existe Usuario con este Email.", html=True) + + + #4. Registro con contraseñas que no coinciden muestra error + def test_registration_with_mismatched_passwords_shows_error(self): + data = self.valid_data.copy() + data['password2'] = 'diferente123' + resp = self.client.post(self.register_url, data) + + self.assertEqual(User.objects.count(), self.initial_user_count) + self.assertContains(resp, "Los dos campos de contraseña no coinciden.") + + + #5. Registro con campo 'first_name' vacío muestra error (required=True) + def test_registration_missing_first_name_shows_error(self): + data = self.valid_data.copy() + data['first_name'] = '' + resp = self.client.post(self.register_url, data) + + self.assertEqual(User.objects.count(), self.initial_user_count) + self.assertContains(resp, "Este campo es obligatorio.") + + + #6. Registro con subida de foto válida + def test_registration_with_valid_photo(self): + # Creo una foto JPEG en memoria + try: + from PIL import Image + buf = io.BytesIO() + img = Image.new('RGB', (1, 1), color=(255, 0, 0)) + img.save(buf, format='JPEG') + image_data = buf.getvalue() + except Exception: + self.skipTest('Pillow is required to create a test JPEG image') + + photo = SimpleUploadedFile( + name='test_photo.jpg', + content=image_data, + content_type='image/jpeg' + ) + + data = self.valid_data.copy() + data['foto'] = photo + resp = self.client.post(self.register_url, data, follow=False) + + self.assertEqual(resp.status_code, 302) + new_user = User.objects.get(email=data['email']) + self.assertTrue(new_user.foto.name.startswith('images/test_photo')) + + # Elimina la foto creada + if new_user.foto: + if os.path.exists(new_user.foto.path): + os.remove(new_user.foto.path) + + + #7. Registro sin campo 'foto' (opcional) es exitoso + def test_registration_without_photo_is_successful(self): + data = self.valid_data.copy() + if 'foto' in data: + del data['foto'] + + resp = self.client.post(self.register_url, data, follow=False) + + self.assertEqual(resp.status_code, 302) + new_user = User.objects.get(email=data['email']) + self.assertFalse(new_user.foto) + +class LogoutViewTests(TestCase): + def setUp(self): + self.client = self.client = self.client = self.client_class() + self.user = User.objects.create_user( + username='userlogout', + email='logout@example.com', + password='testlogout123' + ) + self.login_url = reverse('login') + self.logout_url = reverse('logout') + self.home_url = reverse('home') + + # 1. Comprobar que un usuario logueado se desloguea y redirige correctamente + def test_logout_redirects_to_home_and_clears_session(self): + # Iniciar sesión + self.client.login(username='logout@example.com', password='testlogout123') + + # Verificar que la sesión está activa + self.assertIn('_auth_user_id', self.client.session) + + # Hacer logout + response = self.client.get(self.logout_url) + + # Verificar redirección al home + self.assertRedirects(response, self.home_url) + + # Verificar que se ha cerrado la sesión + self.assertNotIn('_auth_user_id', self.client.session) + + # 2. Comprobar que el logout borra la cookie de sesión + def test_logout_deletes_session_cookie(self): + """El logout deja la cookie de sesión vacía y expirada.""" + self.client.login(username='logout@example.com', password='testlogout123') + response = self.client.get(self.logout_url) + + # Django deja la cookie 'sessionid', pero vacía o marcada para expirar + self.assertIn('sessionid', response.cookies) + cookie = response.cookies['sessionid'] + self.assertTrue(cookie.value == '' or cookie['max-age'] == 0 or cookie['expires']) + self.assertRedirects(response, self.home_url) + + # 3. Comprobar que un usuario no autenticado también redirige correctamente + def test_logout_redirects_even_if_not_authenticated(self): + response = self.client.get(self.logout_url) + self.assertRedirects(response, self.home_url) diff --git a/essenza/user/urls.py b/essenza/user/urls.py new file mode 100644 index 00000000..26825a40 --- /dev/null +++ b/essenza/user/urls.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.urls import include, path +from django.http import HttpResponse +import user.views as views + + +urlpatterns = [ + path('register/', views.RegisterView.as_view(), name='register'), + path('login/', views.LoginView.as_view(), name='login'), + path('logout/', views.LogoutView.as_view(), name='logout'), +] + diff --git a/essenza/user/views.py b/essenza/user/views.py new file mode 100644 index 00000000..25774b65 --- /dev/null +++ b/essenza/user/views.py @@ -0,0 +1,66 @@ +from django.shortcuts import render, redirect +from django.views import View +from django.contrib.auth import authenticate, login, logout +from .forms import LoginForm, RegisterForm +from .models import Usuario + +class LoginView(View): + form_class = LoginForm + template_name = 'user/login.html' + + def get(self, request, *args, **kwargs): + # Si el usuario ya está autenticado, lo mandamos a escaparate + logout(request) + if request.user.is_authenticated: + return redirect('escaparate') + # Si no está autenticado, renderiza el formulario de login + return render(request, self.template_name, {'form': self.form_class()}) + + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST) + if form.is_valid(): + email = form.cleaned_data["email"] + password = form.cleaned_data["password"] + + # Autenticamos al usuario + user = authenticate(request, username=email, password=password) + + if user is not None: + login(request, user) + # Redirige al escaparate después del login + return redirect('escaparate') + else: + # Si falla el login, muestra error en el formulario + form.add_error(None, "Usuario o contraseña incorrectos") + + return render(request, self.template_name, {'form': form}) + +class LogoutView(View): + def get(self, request): + logout(request) + response = redirect('home') + response.delete_cookie('sessionid') + return response + + def post(self, request): + logout(request) + response = redirect('home') + response.delete_cookie('sessionid') + return response + +class RegisterView(View): + form_class = RegisterForm + template_name = 'user/register.html' + + def get(self, request, *args, **kwargs): + form = self.form_class() + return render(request, self.template_name, {'form': form}) + + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST, request.FILES) + + if form.is_valid(): + user = form.save() + return redirect('escaparate') + + return render(request, self.template_name, {'form': form}) \ No newline at end of file