diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index 662ee0e..5f487b8 100644 --- a/essenza/essenza/settings.py +++ b/essenza/essenza/settings.py @@ -122,6 +122,11 @@ STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'static'] +STATIC_ROOT = BASE_DIR / 'staticfiles' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index 8ee6270..a4a755e 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -2,10 +2,15 @@ from django.urls import path, include from info.views import info_view from product.views import DashboardView +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('info/', info_view, name='info-home'), path("user/", include("user.urls")), path('admin/', admin.site.urls), path('', DashboardView.as_view(), name='dashboard') -] \ No newline at end of file +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/essenza/media/profile_pics/admin.png b/essenza/media/profile_pics/admin.png new file mode 100644 index 0000000..dff3fbd Binary files /dev/null and b/essenza/media/profile_pics/admin.png differ diff --git a/essenza/profile_pics/user1.avif b/essenza/media/profile_pics/ana_martin.jpg similarity index 100% rename from essenza/profile_pics/user1.avif rename to essenza/media/profile_pics/ana_martin.jpg diff --git a/essenza/profile_pics/user3.avif b/essenza/media/profile_pics/carla_gonzalez.jpg similarity index 100% rename from essenza/profile_pics/user3.avif rename to essenza/media/profile_pics/carla_gonzalez.jpg diff --git a/essenza/profile_pics/user2.avif b/essenza/media/profile_pics/juan_perez.jpg similarity index 100% rename from essenza/profile_pics/user2.avif rename to essenza/media/profile_pics/juan_perez.jpg diff --git a/essenza/product/models.py b/essenza/product/models.py index 12d3621..cba51f0 100644 --- a/essenza/product/models.py +++ b/essenza/product/models.py @@ -13,7 +13,7 @@ class Product(models.Model): category = models.CharField(max_length=20, choices=Category.choices) brand = models.CharField(max_length=255) price = models.DecimalField(max_digits=10, decimal_places=2) - photo = models.ImageField(upload_to='profile_pics/', null=True, blank=True) + photo = models.ImageField(upload_to='products/', null=True, blank=True) stock = models.IntegerField() is_active = models.BooleanField(default=False) diff --git a/essenza/product/sample/sample.json b/essenza/product/sample/sample.json index 2f3e04a..df6aff4 100644 --- a/essenza/product/sample/sample.json +++ b/essenza/product/sample/sample.json @@ -8,7 +8,7 @@ "category": "maquillaje", "brand": "L'Oréal", "price": 19.99, - "photo": "https://example.com/images/maquillaje_base.jpg", + "photo": "products/maquillaje_base.jpg", "stock": 50, "is_active": true } @@ -22,7 +22,7 @@ "category": "tratamiento", "brand": "Pantene", "price": 6.99, - "photo": "https://example.com/images/shampoo_reconstructivo.jpg", + "photo": "products/shampoo_reconstructivo.jpg", "stock": 100, "is_active": true } @@ -36,7 +36,7 @@ "category": "herramienta", "brand": "Braun", "price": 45.99, - "photo": "https://example.com/images/secador_pelo.jpg", + "photo": "products/secador_pelo.jpg", "stock": 30, "is_active": true } @@ -50,7 +50,7 @@ "category": "perfume", "brand": "Chanel", "price": 79.99, - "photo": "https://example.com/images/perfume_floral.jpg", + "photo": "products/perfume_floral.jpg", "stock": 20, "is_active": true } @@ -64,7 +64,7 @@ "category": "tratamiento", "brand": "Nivea", "price": 12.99, - "photo": "https://example.com/images/crema_hidratante.jpg", + "photo": "products/crema_hidratante.jpg", "stock": 150, "is_active": true } @@ -78,7 +78,7 @@ "category": "herramienta", "brand": "Remington", "price": 29.99, - "photo": "https://example.com/images/rizador_pelo.jpg", + "photo": "products/rizador_pelo.jpg", "stock": 40, "is_active": true } @@ -92,7 +92,7 @@ "category": "tratamiento", "brand": "Dettol", "price": 4.99, - "photo": "https://example.com/images/gel_antibacterial.jpg", + "photo": "products/gel_antibacterial.jpg", "stock": 200, "is_active": true } @@ -106,7 +106,7 @@ "category": "tratamiento", "brand": "Head & Shoulders", "price": 7.99, - "photo": "https://example.com/images/shampoo_anticaspa.jpg", + "photo": "products/shampoo_anticaspa.jpg", "stock": 90, "is_active": true } @@ -120,7 +120,7 @@ "category": "tratamiento", "brand": "Argan Oil", "price": 15.99, - "photo": "https://example.com/images/aceite_capilar.jpg", + "photo": "products/aceite_capilar.jpg", "stock": 60, "is_active": true } @@ -134,7 +134,7 @@ "category": "cabello", "brand": "Garnier", "price": 8.99, - "photo": "https://example.com/images/tinte_cabello.jpg", + "photo": "products/tinte_cabello.jpg", "stock": 110, "is_active": true } @@ -148,7 +148,7 @@ "category": "tratamiento", "brand": "L'Oréal", "price": 18.99, - "photo": "https://example.com/images/mascarilla_facial.jpg", + "photo": "products/mascarilla_facial.jpg", "stock": 80, "is_active": true } @@ -162,7 +162,7 @@ "category": "tratamiento", "brand": "TRESemmé", "price": 5.99, - "photo": "https://example.com/images/shampoo_voluminizador.jpg", + "photo": "products/shampoo_voluminizador.jpg", "stock": 120, "is_active": true } @@ -176,7 +176,7 @@ "category": "cabello", "brand": "Schwarzkopf", "price": 10.99, - "photo": "https://example.com/images/laca_pelo.jpg", + "photo": "products/laca_pelo.jpg", "stock": 70, "is_active": true } @@ -190,7 +190,7 @@ "category": "tratamiento", "brand": "Hawaiian Tropic", "price": 14.99, - "photo": "https://example.com/images/crema_solar.jpg", + "photo": "products/crema_solar.jpg", "stock": 40, "is_active": true } @@ -204,7 +204,7 @@ "category": "tratamiento", "brand": "Olay", "price": 25.99, - "photo": "https://example.com/images/crema_antiedad.jpg", + "photo": "products/crema_antiedad.jpg", "stock": 30, "is_active": true } @@ -218,7 +218,7 @@ "category": "tratamiento", "brand": "Dove", "price": 3.99, - "photo": "https://example.com/images/desodorante.jpg", + "photo": "products/desodorante.jpg", "stock": 150, "is_active": true } @@ -232,7 +232,7 @@ "category": "tratamiento", "brand": "Neutrogena", "price": 4.49, - "photo": "https://example.com/images/toallitas_desmaquillantes.jpg", + "photo": "products/toallitas_desmaquillantes.jpg", "stock": 90, "is_active": true } @@ -246,7 +246,7 @@ "category": "maquillaje", "brand": "Real Techniques", "price": 12.99, - "photo": "https://example.com/images/pincel_maquillaje.jpg", + "photo": "products/pincel_maquillaje.jpg", "stock": 110, "is_active": true } @@ -260,7 +260,7 @@ "category": "tratamiento", "brand": "Eucerin", "price": 9.99, - "photo": "https://example.com/images/crema_pies.jpg", + "photo": "products/crema_pies.jpg", "stock": 80, "is_active": true } @@ -274,7 +274,7 @@ "category": "tratamiento", "brand": "Neutrogena", "price": 7.49, - "photo": "https://example.com/images/limpieza_facial.jpg", + "photo": "products/limpieza_facial.jpg", "stock": 130, "is_active": true } diff --git a/essenza/profile_pics/img1.jpg b/essenza/profile_pics/img1.jpg deleted file mode 100644 index 64ca02d..0000000 Binary files a/essenza/profile_pics/img1.jpg and /dev/null differ diff --git a/essenza/requirements.txt b/essenza/requirements.txt new file mode 100644 index 0000000..e81440b --- /dev/null +++ b/essenza/requirements.txt @@ -0,0 +1,5 @@ +asgiref==3.10.0 +Django==5.2.8 +pillow==12.0.0 +sqlparse==0.5.3 +tzdata==2025.2 diff --git a/essenza/static/images/default_user.png b/essenza/static/images/default_user.png new file mode 100644 index 0000000..ada81e2 Binary files /dev/null and b/essenza/static/images/default_user.png differ diff --git a/essenza/templates/product/dashboard.html b/essenza/templates/product/dashboard.html index 9022f47..5472c98 100644 --- a/essenza/templates/product/dashboard.html +++ b/essenza/templates/product/dashboard.html @@ -200,7 +200,7 @@ {% if user.is_authenticated %} - Mi perfil + Mi perfil
{% csrf_token %} diff --git a/essenza/templates/user/confirm_delete_profile.html b/essenza/templates/user/confirm_delete_profile.html new file mode 100644 index 0000000..1538942 --- /dev/null +++ b/essenza/templates/user/confirm_delete_profile.html @@ -0,0 +1,101 @@ +{% load static %} + + + + + Eliminar Cuenta · Essenza + + + + + +
+

ESSENZA

+
+

Eliminar Cuenta

+

+ ¿Estás seguro de que quieres eliminar tu cuenta permanentemente? +

+

+ Toda tu información será borrada y esta acción no se puede deshacer. +

+ + + {% csrf_token %} + + + + + +
+
+ + \ No newline at end of file diff --git a/essenza/templates/user/edit_profile.html b/essenza/templates/user/edit_profile.html new file mode 100644 index 0000000..75d157c --- /dev/null +++ b/essenza/templates/user/edit_profile.html @@ -0,0 +1,254 @@ +{% load static %} + + + + + Editar Perfil · Essenza + + + + +
+

ESSENZA

+
+
+ {% csrf_token %} + + + {{ form.first_name }} + + + {{ form.last_name }} + + + {{ form.email }} + + + {{ form.photo }} + + {% if not form.remove_photo.is_hidden %} +
+ {{ form.remove_photo }} + +
+ {% endif %} + + {% 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 %} + + {% if form.errors %} +
+

¡El formulario tiene errores!

+ {% for field in form %} + {% if field.errors %} +
+ {{ field.label }}: +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + {% endfor %} + {% for error in form.non_field_errors %} +
+ Error general: {{ error }} +
+ {% endfor %} +
+ {% endif %} + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/essenza/templates/user/login.html b/essenza/templates/user/login.html index 5afa3d0..f23dea3 100644 --- a/essenza/templates/user/login.html +++ b/essenza/templates/user/login.html @@ -12,11 +12,13 @@ font-family:'Segoe UI', Arial, sans-serif; } - .page{ + .page{ min-height:100dvh; - display:grid; - place-items:start center; - padding-top:56px; + display:flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap:2rem; } .brand{ @@ -57,10 +59,23 @@ background-color: #a35a34; } - .forgot{ margin-top:10px; text-align:left; } - .forgot a{ color:#333; text-decoration:underline; font-size:12.5px; } + /* Link para "Aún no tienes cuenta?" */ + .login-link { + margin-top: 15px; + text-align: center; + font-size: 13px; + } + + .login-link a { + color: #c06b3e; + text-decoration: underline; + } - .form-error{ color:#b00020; } + .form-error { + color: #b00020; + font-size: 13px; + margin: 6px 0; + } @@ -86,10 +101,13 @@

ESSENZA

+ + - diff --git a/essenza/templates/user/profile.html b/essenza/templates/user/profile.html new file mode 100644 index 0000000..43a9b58 --- /dev/null +++ b/essenza/templates/user/profile.html @@ -0,0 +1,147 @@ +{% load static %} + + + + + Mi Perfil · Essenza + + + + + + + Volver + +
+

ESSENZA

+ +
+ + {% if user.photo %} + Foto de perfil + {% else %} + Foto de perfil por defecto + {% endif %} + +
+

Nombre: {{ user.first_name }}

+

Apellidos: {{ user.last_name }}

+

Email: {{ user.email }}

+
+ + Editar mis datos + +
+
+ + \ No newline at end of file diff --git a/essenza/user/forms.py b/essenza/user/forms.py index 66c8d85..821a573 100644 --- a/essenza/user/forms.py +++ b/essenza/user/forms.py @@ -43,3 +43,56 @@ def save(self, commit=True): if commit: user.save() return user + +class ProfileEditForm(forms.ModelForm): + + 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 + ) + photo = forms.ImageField( + label="Foto (Opcional)", + required=False, + widget=forms.FileInput + ) + remove_photo = forms.BooleanField( + required=False, + label="Eliminar foto de perfil actual" + ) + + class Meta: + model = Usuario + fields = ('first_name', 'last_name', 'email', 'photo') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['email'].disabled = True + self.fields['email'].help_text = 'El correo electrónico no se puede modificar.' + + # Ocultar checkbox si no hay foto + if not self.instance or not self.instance.photo: + self.fields['remove_photo'].widget = forms.HiddenInput() + + def save(self, commit=True): + user = super().save(commit=False) + + if self.cleaned_data.get('remove_photo') and not self.files.get('photo'): + try: + if user.photo: + user.photo.delete(save=False) + except Exception: + pass + user.photo = None + + if commit: + user.save() + return user \ No newline at end of file diff --git a/essenza/user/models.py b/essenza/user/models.py index 40ab05e..1183d51 100644 --- a/essenza/user/models.py +++ b/essenza/user/models.py @@ -6,7 +6,7 @@ class Role(models.TextChoices): USER = 'user', 'User' class Usuario(AbstractUser): - photo = models.ImageField(upload_to='images/', null=True, blank=True) + photo = models.ImageField(upload_to='profile_pics/', null=True, blank=True) role = models.CharField(max_length=10, choices=Role.choices, default=Role.USER) email = models.EmailField(unique=True) diff --git a/essenza/user/sample/sample.json b/essenza/user/sample/sample.json index 843a364..180043e 100644 --- a/essenza/user/sample/sample.json +++ b/essenza/user/sample/sample.json @@ -5,7 +5,7 @@ "fields": { "email": "juan.perez@example.com", "username": "juan", - "photo": "https://example.com/images/juan_perez.jpg", + "photo": "profile_pics/juan_perez.jpg", "role": "user", "password": "pbkdf2_sha256$1000000$6D2dFePeqIu7UN3iIkhrmg$jkb15BhOjo5s/2vveGH1YPZX2Eu5abDhXUpn6rHi61c=" } @@ -16,7 +16,7 @@ "fields": { "email": "ana.martin@example.com", "username": "ana", - "photo": "https://example.com/images/ana_martin.jpg", + "photo": "profile_pics/ana_martin.jpg", "role": "user", "password": "pbkdf2_sha256$1000000$sjZji3r8LgZ1Scf2lR8sDA$zwsaj6xrhTxpZqfLqPZtHD8w5obxhP5RKh5RUY8vcB0=" } @@ -27,7 +27,7 @@ "fields": { "email": "carla.gonzalez@example.com", "username": "carla", - "photo": "https://example.com/images/carla_gonzalez.jpg", + "photo": "profile_pics/carla_gonzalez.jpg", "role": "user", "password": "pbkdf2_sha256$1000000$X4rPLmVEgJfrZJ3gVwUwKg$alBh41D6b/3X9GiA2fo2X8X5Q8aHT91Jk5mwY6GLgdE=" } @@ -41,7 +41,7 @@ "is_superuser": true, "is_staff": true, "is_active": true, - "photo": "https://example.com/images/admin.jpg", + "photo": "profile_pics/admin.png", "role": "admin", "password": "pbkdf2_sha256$1000000$sazfnRvfJ4niYZE6ixBVKR$pboaUb8AWvEXwuJZkWoh3xwfYq++7nik9p2e0TxNPws=" } diff --git a/essenza/user/tests.py b/essenza/user/tests.py index 7948294..cdb055a 100644 --- a/essenza/user/tests.py +++ b/essenza/user/tests.py @@ -150,7 +150,7 @@ def test_registration_with_valid_photo(self): self.assertEqual(resp.status_code, 302) new_user = User.objects.get(email=data['email']) - self.assertTrue(new_user.photo.name.startswith('images/test_photo')) + self.assertTrue(new_user.photo.name.startswith('profile_pics/test_photo')) # Elimina la foto creada if new_user.photo: diff --git a/essenza/user/urls.py b/essenza/user/urls.py index 6c353e8..b5e3856 100644 --- a/essenza/user/urls.py +++ b/essenza/user/urls.py @@ -6,5 +6,8 @@ path('register/', views.RegisterView.as_view(), name='register'), path('login/', views.LoginView.as_view(), name='login'), path('logout/', views.LogoutView.as_view(), name='logout'), + path('profile/', views.ProfileView.as_view(), name='profile'), + path('profile/edit/', views.ProfileEditView.as_view(), name='profile_edit'), + path('profile/delete/', views.ProfileDeleteView.as_view(), name='profile_delete'), ] diff --git a/essenza/user/views.py b/essenza/user/views.py index 9e4765e..e01fd4c 100644 --- a/essenza/user/views.py +++ b/essenza/user/views.py @@ -1,7 +1,8 @@ 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 .forms import LoginForm, RegisterForm, ProfileEditForm +from django.contrib.auth.mixins import LoginRequiredMixin # Para proteger vistas class LoginView(View): form_class = LoginForm @@ -9,7 +10,6 @@ class LoginView(View): def get(self, request, *args, **kwargs): # Si el usuario ya está autenticado, lo mandamos a dashboard - logout(request) if request.user.is_authenticated: return redirect('dashboard') # Si no está autenticado, renderiza el formulario de login @@ -62,4 +62,69 @@ def post(self, request, *args, **kwargs): login(request, user) return redirect('dashboard') - return render(request, self.template_name, {'form': form}) \ No newline at end of file + return render(request, self.template_name, {'form': form}) + +class ProfileView(LoginRequiredMixin, View): + template_name = 'user/profile.html' + + def get(self, request, *args, **kwargs): + return render(request, self.template_name) + + +class ProfileEditView(LoginRequiredMixin, View): + form_class = ProfileEditForm + template_name = 'user/edit_profile.html' + + def get(self, request, *args, **kwargs): + # Rellena el formulario con los datos actuales del usuario + form = self.form_class(instance=request.user) + return render(request, self.template_name, {'form': form}) + + def post(self, request, *args, **kwargs): + + # Guarda la foto antigua para borrarla si se ha cambiado + try: + old_photo = request.user.photo + except AttributeError: + old_photo = None + + # Rellena el formulario con los datos enviados + form = self.form_class(request.POST, request.FILES, instance=request.user) + + # Si el formulario es válido, se redirige a la vista de perfil + if form.is_valid(): + new_user = form.save() + # Si había una foto antigua y es distinta a la nueva, la borramos del sistema + if old_photo and old_photo != new_user.photo: + old_photo.delete(save=False) + return redirect('profile') + + # Si el formulario no es válido, se vuelve a mostrar con errores + return render(request, self.template_name, {'form': form}) + + +class ProfileDeleteView(LoginRequiredMixin, View): + template_name = 'user/confirm_delete_profile.html' + + def get(self, request, *args, **kwargs): + # Muestra la página de confirmación + return render(request, self.template_name) + + def post(self, request, *args, **kwargs): + + # Guarda la foto antigua para borrarla + try: + photo_to_delete = request.user.photo + except AttributeError: + photo_to_delete = None + user = request.user + + # Cierra la sesión ANTES de borrar al usuario para evitar errores + logout(request) + # Borra el usuario de la base de datos + user.delete() + # Borra, si la hay, la foto del sistema de archivos + if photo_to_delete: + photo_to_delete.delete(save=False) + + return redirect('dashboard') \ No newline at end of file