+
+
+
+
+
+
+
+ {% if users %}
+ {% for user in users %}
+
+
+
+
+
+
+
+ {% if user.photo %}
+

+ {% else %}
+
+ {% endif %}
+
+
+
+ {% if user.first_name %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}
+
+
{{ user.email }}
+
+ {{ user.date_joined|date:"d M Y" }}
+
+
+
+
+
+
+ {% if user.role == "admin" %}
+
+ Admin
+
+ {% else %}
+
+ Cliente
+
+ {% endif %}
+
+
+
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/essenza/user/forms.py b/essenza/user/forms.py
index ed47366..d9b17f1 100644
--- a/essenza/user/forms.py
+++ b/essenza/user/forms.py
@@ -1,7 +1,7 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
-from .models import Usuario
+from .models import Role, Usuario
class LoginForm(forms.Form):
@@ -75,3 +75,74 @@ def save(self, commit=True):
if commit:
user.save()
return user
+
+
+class UserCreationFormAdmin(RegisterForm):
+ # Añadimos SOLO lo que le falta al registro normal: Control de Rol y Estado
+ role = forms.ChoiceField(
+ choices=Role.choices,
+ label="Rol Inicial",
+ required=True,
+ initial=Role.USER, # Por defecto creamos clientes
+ )
+
+ is_active = forms.BooleanField(
+ label="Cuenta Activa",
+ required=False,
+ initial=True,
+ help_text="Desmarca si quieres crearlo pero bloquearle el acceso.",
+ )
+
+ class Meta(RegisterForm.Meta):
+ # Heredamos el modelo de RegisterForm
+ model = Usuario
+ # Añadimos los nuevos campos a los que ya tenía RegisterForm
+ fields = RegisterForm.Meta.fields + ("role", "is_active")
+
+
+class UserEditFormAdmin(forms.ModelForm):
+ first_name = forms.CharField(label="Nombre", required=False)
+ last_name = forms.CharField(label="Apellidos", required=False)
+ email = forms.EmailField(label="Correo electrónico", required=True)
+
+ role = forms.ChoiceField(
+ choices=Role.choices,
+ label="Rol de Usuario",
+ required=True,
+ widget=forms.Select(attrs={"class": "form-select"}),
+ )
+ is_active = forms.BooleanField(
+ label="Cuenta Activa",
+ required=False,
+ help_text="Desmarca esto para bloquear el acceso al usuario sin borrarlo.",
+ )
+
+ photo = forms.ImageField(
+ label="Foto de perfil", required=False, widget=forms.FileInput
+ )
+ remove_photo = forms.BooleanField(required=False, label="Eliminar foto actual")
+
+ class Meta:
+ model = Usuario
+ fields = ("first_name", "last_name", "email", "role", "is_active", "photo")
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ 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
diff --git a/essenza/user/tests.py b/essenza/user/tests.py
index 29aaece..c052e97 100644
--- a/essenza/user/tests.py
+++ b/essenza/user/tests.py
@@ -6,7 +6,8 @@
from django.test import TestCase
from django.urls import reverse
-User = get_user_model()
+# UNIFICACIÓN: Usamos 'Usuario' para todo el archivo
+Usuario = get_user_model()
class LoginViewTests(TestCase):
@@ -15,7 +16,7 @@ def setUp(self):
self.username = "user1"
self.email = "user1@example.com"
self.password = "pass1234"
- self.user = User.objects.create_user(
+ self.user = Usuario.objects.create_user(
username=self.username, email=self.email, password=self.password
)
self.login_url = reverse("login")
@@ -61,7 +62,7 @@ class RegisterViewTests(TestCase):
def setUp(self):
self.register_url = reverse("register")
self.dashboard_url = reverse("dashboard")
- self.initial_user_count = User.objects.count()
+ self.initial_user_count = Usuario.objects.count()
# Datos para un nuevo usuario de prueba
self.valid_data = {
@@ -87,16 +88,16 @@ def test_successful_registration_redirects_and_creates_user(self):
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp["Location"], self.dashboard_url)
- self.assertEqual(User.objects.count(), self.initial_user_count + 1)
+ self.assertEqual(Usuario.objects.count(), self.initial_user_count + 1)
- new_user = User.objects.get(email=data["email"])
+ new_user = Usuario.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(
+ Usuario.objects.create_user(
username="test", email=self.valid_data["email"], password="test"
) # Usuario previo creado con mismo email
data = self.valid_data.copy()
@@ -105,7 +106,7 @@ def test_registration_with_duplicate_email_shows_error(self):
) # Intento de registro con el mismo email
self.assertEqual(
- User.objects.count(), self.initial_user_count + 1
+ Usuario.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)
@@ -115,7 +116,7 @@ def test_registration_with_mismatched_passwords_shows_error(self):
data["password2"] = "diferente123"
resp = self.client.post(self.register_url, data)
- self.assertEqual(User.objects.count(), self.initial_user_count)
+ self.assertEqual(Usuario.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)
@@ -124,7 +125,7 @@ def test_registration_missing_first_name_shows_error(self):
data["first_name"] = ""
resp = self.client.post(self.register_url, data)
- self.assertEqual(User.objects.count(), self.initial_user_count)
+ self.assertEqual(Usuario.objects.count(), self.initial_user_count)
self.assertContains(resp, "Este campo es obligatorio.")
# 6. Registro con subida de foto válida
@@ -149,7 +150,7 @@ def test_registration_with_valid_photo(self):
resp = self.client.post(self.register_url, data, follow=False)
self.assertEqual(resp.status_code, 302)
- new_user = User.objects.get(email=data["email"])
+ new_user = Usuario.objects.get(email=data["email"])
self.assertTrue(new_user.photo.name.startswith("profile_pics/test_photo"))
# Elimina la foto creada
@@ -166,14 +167,14 @@ def test_registration_without_photo_is_successful(self):
resp = self.client.post(self.register_url, data, follow=False)
self.assertEqual(resp.status_code, 302)
- new_user = User.objects.get(email=data["email"])
+ new_user = Usuario.objects.get(email=data["email"])
self.assertFalse(new_user.photo)
class LogoutViewTests(TestCase):
def setUp(self):
- self.client = self.client = self.client = self.client_class()
- self.user = User.objects.create_user(
+ self.client = self.client_class()
+ self.user = Usuario.objects.create_user(
username="userlogout", email="logout@example.com", password="testlogout123"
)
self.login_url = reverse("login")
@@ -215,3 +216,180 @@ def test_logout_deletes_session_cookie(self):
def test_logout_redirects_even_if_not_authenticated(self):
response = self.client.get(self.logout_url)
self.assertRedirects(response, self.dashboard_url)
+
+
+class UserAdminViewsTests(TestCase):
+ def setUp(self):
+ # 1. Creamos un ADMIN
+ self.admin_user = Usuario.objects.create_user(
+ email="admin@test.com",
+ username="admin@test.com",
+ password="password123",
+ role="admin",
+ first_name="Admin",
+ last_name="Jefe",
+ )
+
+ # 2. Creamos un USUARIO NORMAL (Cliente)
+ self.normal_user = Usuario.objects.create_user(
+ email="user@test.com",
+ username="user@test.com",
+ password="password123",
+ role="user",
+ first_name="Cliente",
+ last_name="Uno",
+ )
+
+ # 3. Creamos un USUARIO OBJETIVO (Para editar/borrar)
+ self.target_user = Usuario.objects.create_user(
+ email="target@test.com",
+ username="target@test.com",
+ password="password123",
+ role="user",
+ first_name="Zacarias",
+ last_name="Target",
+ )
+
+ # URLs
+ self.url_list = reverse("user_list")
+ self.url_create = reverse("user_create_admin")
+ self.url_edit = reverse("user_edit_admin", args=[self.target_user.pk])
+ self.url_delete = reverse("user_delete_admin", args=[self.target_user.pk])
+ self.url_dashboard = reverse("dashboard")
+ self.url_login = reverse("login")
+
+ # ========================================================
+ # 1. PRUEBAS DE SEGURIDAD
+ # ========================================================
+
+ def test_anon_user_redirects_to_login(self):
+ """Si no estás logueado, no entras."""
+ endpoints = [self.url_list, self.url_create, self.url_edit, self.url_delete]
+ for url in endpoints:
+ resp = self.client.get(url)
+ # Redirige al login (302)
+ self.assertNotEqual(resp.status_code, 200)
+
+ def test_normal_user_redirects_to_dashboard(self):
+ """Si eres cliente, te echa al dashboard."""
+ self.client.force_login(self.normal_user)
+ endpoints = [self.url_list, self.url_create, self.url_edit, self.url_delete]
+ for url in endpoints:
+ resp = self.client.get(url)
+ self.assertRedirects(resp, self.url_dashboard)
+
+ def test_admin_can_access(self):
+ """El admin entra hasta la cocina."""
+ self.client.force_login(self.admin_user)
+ resp = self.client.get(self.url_list)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTemplateUsed(resp, "user/user_list.html")
+
+ # ========================================================
+ # 2. PRUEBAS DE LISTADO (Filtros y Orden)
+ # ========================================================
+
+ def test_list_filter_role(self):
+ self.client.force_login(self.admin_user)
+ # Filtramos solo admins
+ resp = self.client.get(self.url_list, {"role": "admin"})
+ users = resp.context["users"]
+ self.assertIn(self.admin_user, users)
+ self.assertNotIn(self.normal_user, users)
+
+ def test_list_order_by_name(self):
+ self.client.force_login(self.admin_user)
+ # Orden A-Z: 'Admin' va antes que 'Zacarias'
+ resp = self.client.get(self.url_list, {"order": "name_asc"})
+ users = list(resp.context["users"])
+
+ # Verificamos posiciones relativas
+ index_admin = users.index(self.admin_user)
+ index_target = users.index(self.target_user)
+ self.assertTrue(index_admin < index_target)
+
+ # ========================================================
+ # 3. PRUEBA DE CREAR (Sin foto)
+ # ========================================================
+
+ def test_create_user_success(self):
+ self.client.force_login(self.admin_user)
+
+ # Datos sin archivo de imagen
+ data = {
+ "email": "new@test.com",
+ "first_name": "Nuevo",
+ "last_name": "Usuario",
+ "role": "user",
+ "is_active": True,
+ "password1": "Pass12345", # Campos requeridos por tu AdminUserCreationForm
+ "password2": "Pass12345",
+ }
+
+ resp = self.client.post(self.url_create, data)
+
+ # Debe redirigir al listado
+ self.assertRedirects(resp, self.url_list)
+
+ # Verificamos que existe en DB
+ self.assertTrue(Usuario.objects.filter(email="new@test.com").exists())
+
+ # Verificamos que NO nos ha logueado con el nuevo usuario (el admin sigue siendo admin)
+ self.assertEqual(int(self.client.session["_auth_user_id"]), self.admin_user.pk)
+
+ # ========================================================
+ # 4. PRUEBA DE EDITAR (Sin foto)
+ # ========================================================
+
+ def test_update_user_success(self):
+ self.client.force_login(self.admin_user)
+
+ data = {
+ "email": "updated@test.com", # Cambiamos email
+ "first_name": "Editado",
+ "last_name": "Test",
+ "role": "admin", # Lo ascendemos a admin
+ "is_active": False, # Lo baneamos
+ }
+
+ resp = self.client.post(self.url_edit, data)
+
+ self.assertRedirects(resp, self.url_list)
+
+ # Refrescamos desde DB para comprobar cambios
+ self.target_user.refresh_from_db()
+ self.assertEqual(self.target_user.email, "updated@test.com")
+ self.assertEqual(self.target_user.role, "admin")
+ self.assertFalse(self.target_user.is_active)
+
+ # ========================================================
+ # 5. PRUEBAS DE BORRAR
+ # ========================================================
+
+ def test_delete_user_success(self):
+ self.client.force_login(self.admin_user)
+
+ # 1. GET muestra confirmación
+ resp_get = self.client.get(self.url_delete)
+ self.assertEqual(resp_get.status_code, 200)
+ self.assertTemplateUsed(resp_get, "user/confirm_delete_user_admin.html")
+
+ # 2. POST borra
+ resp_post = self.client.post(self.url_delete)
+ self.assertRedirects(resp_post, self.url_list)
+
+ # Verificamos que murió
+ self.assertFalse(Usuario.objects.filter(pk=self.target_user.pk).exists())
+
+ def test_admin_cannot_delete_self(self):
+ """El admin no puede borrarse a sí mismo."""
+ self.client.force_login(self.admin_user)
+
+ url_delete_self = reverse("user_delete_admin", args=[self.admin_user.pk])
+
+ # Intentamos borrar al admin logueado -> Debe redirigir al listado sin borrar
+ resp = self.client.post(url_delete_self)
+ self.assertRedirects(resp, self.url_list)
+
+ # Verificamos que sigue vivo
+ self.assertTrue(Usuario.objects.filter(pk=self.admin_user.pk).exists())
diff --git a/essenza/user/urls.py b/essenza/user/urls.py
index b95daf4..8a78ea8 100644
--- a/essenza/user/urls.py
+++ b/essenza/user/urls.py
@@ -9,4 +9,20 @@
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"),
+ path("list/", views.UserListView.as_view(), name="user_list"),
+ path(
+ "manage/create/",
+ views.UserCreateViewAdmin.as_view(),
+ name="user_create_admin",
+ ),
+ path(
+ "manage/edit/