diff --git a/tienda_ilusion/don_confiao/api/customers.py b/tienda_ilusion/don_confiao/api/customers.py index 533e0ad..4b0e182 100644 --- a/tienda_ilusion/don_confiao/api/customers.py +++ b/tienda_ilusion/don_confiao/api/customers.py @@ -5,7 +5,7 @@ from rest_framework.permissions import IsAuthenticated from ..models.customers import Customer from ..serializers import CustomerSerializer -from ..permissions import IsAdministrator +from ..permissions import IsAdministrator, IsNotPublico from ..services.tryton.customers import CustomerTrytonService from ..services.tryton.client import get_tryton_client @@ -13,7 +13,7 @@ from ..services.tryton.client import get_tryton_client class CustomerView(viewsets.ModelViewSet): queryset = Customer.objects.all() serializer_class = CustomerSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsNotPublico] class CustomersFromTrytonView(APIView): diff --git a/tienda_ilusion/don_confiao/api/products.py b/tienda_ilusion/don_confiao/api/products.py index 7387924..ddb0867 100644 --- a/tienda_ilusion/don_confiao/api/products.py +++ b/tienda_ilusion/don_confiao/api/products.py @@ -5,7 +5,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from ..models.products import Product from ..serializers import ProductSerializer -from ..permissions import IsAdministrator +from ..permissions import IsAdministrator, IsNotPublico from ..services.tryton.products import ProductTrytonService from ..services.tryton.client import get_tryton_client @@ -17,7 +17,7 @@ class ProductView(viewsets.ModelViewSet): def get_permissions(self): if self.action in ("list", "retrieve"): return [AllowAny()] - return [IsAuthenticated()] + return [IsNotPublico()] def get_queryset(self): """ diff --git a/tienda_ilusion/don_confiao/api/sales.py b/tienda_ilusion/don_confiao/api/sales.py index 085ca29..752e941 100644 --- a/tienda_ilusion/don_confiao/api/sales.py +++ b/tienda_ilusion/don_confiao/api/sales.py @@ -14,7 +14,7 @@ from ..serializers import ( SaleSummarySerializer, CatalogSaleSummarySerializer, ) -from ..permissions import IsAdministrator +from ..permissions import IsAdministrator, IsNotPublico from ..services.tryton.sales import SaleTrytonService from ..services.tryton.client import get_tryton_client from ..views import sales_to_tryton_csv @@ -23,7 +23,7 @@ from ..views import sales_to_tryton_csv class SaleView(viewsets.ModelViewSet): queryset = Sale.objects.all() serializer_class = SaleSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsNotPublico] def create(self, request): data = request.data @@ -59,11 +59,15 @@ class SaleView(viewsets.ModelViewSet): class CatalogSaleView(viewsets.ModelViewSet): queryset = CatalogSale.objects.all() serializer_class = CatalogSaleSerializer - permission_classes = [IsAuthenticated] + + def get_permissions(self): + if self.action == "create": + return [IsAuthenticated()] + return [IsNotPublico()] class SaleSummary(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsNotPublico] def get(self, request, id): sale = Sale.objects.get(pk=id) @@ -72,7 +76,7 @@ class SaleSummary(APIView): class CatalogSaleSummary(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsNotPublico] def get(self, request, id): catalog_sale = CatalogSale.objects.get(pk=id) diff --git a/tienda_ilusion/don_confiao/permissions.py b/tienda_ilusion/don_confiao/permissions.py index d36562a..9285936 100644 --- a/tienda_ilusion/don_confiao/permissions.py +++ b/tienda_ilusion/don_confiao/permissions.py @@ -1,6 +1,26 @@ from rest_framework.permissions import BasePermission +def _get_user_type(user): + if not user or not user.is_authenticated: + return None + profile = getattr(user, "profile", None) + if profile: + return profile.user_type + return "administrator" if user.is_staff else "user" + + class IsAdministrator(BasePermission): def has_permission(self, request, view): - return request.user and request.user.is_staff + return _get_user_type(request.user) == "administrator" + + +class IsNotPublico(BasePermission): + def has_permission(self, request, view): + user_type = _get_user_type(request.user) + return user_type is not None and user_type != "publico" + + +class IsPublico(BasePermission): + def has_permission(self, request, view): + return _get_user_type(request.user) == "publico" diff --git a/tienda_ilusion/users/admin.py b/tienda_ilusion/users/admin.py index 8c38f3f..79a685a 100644 --- a/tienda_ilusion/users/admin.py +++ b/tienda_ilusion/users/admin.py @@ -1,3 +1,23 @@ from django.contrib import admin +from django.contrib.auth.models import User +from django.contrib.auth.admin import UserAdmin -# Register your models here. +from .models import UserProfile + + +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + + +class CustomUserAdmin(UserAdmin): + inlines = [UserProfileInline] + + def get_inline_instances(self, request, obj=None): + if not obj: + return [] + return super().get_inline_instances(request, obj) + + +admin.site.unregister(User) +admin.site.register(User, CustomUserAdmin) diff --git a/tienda_ilusion/users/apps.py b/tienda_ilusion/users/apps.py index 4ce1fab..27b4056 100644 --- a/tienda_ilusion/users/apps.py +++ b/tienda_ilusion/users/apps.py @@ -3,3 +3,6 @@ from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' + + def ready(self): + import users.signals # noqa diff --git a/tienda_ilusion/users/migrations/0001_initial.py b/tienda_ilusion/users/migrations/0001_initial.py new file mode 100644 index 0000000..9a8374c --- /dev/null +++ b/tienda_ilusion/users/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.6 on 2026-06-22 17:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_type', models.CharField(choices=[('publico', 'Público'), ('user', 'Usuario'), ('administrator', 'Administrador')], default='user', max_length=20)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tienda_ilusion/users/migrations/0002_backfill_user_profiles.py b/tienda_ilusion/users/migrations/0002_backfill_user_profiles.py new file mode 100644 index 0000000..bb41551 --- /dev/null +++ b/tienda_ilusion/users/migrations/0002_backfill_user_profiles.py @@ -0,0 +1,24 @@ +from django.db import migrations + + +def backfill_user_profiles(apps, schema_editor): + User = apps.get_model("auth", "User") + UserProfile = apps.get_model("users", "UserProfile") + for user in User.objects.all(): + UserProfile.objects.get_or_create( + user=user, + defaults={ + "user_type": "administrator" if user.is_staff else "user", + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.RunPython(backfill_user_profiles, migrations.RunPython.noop), + ] diff --git a/tienda_ilusion/users/models.py b/tienda_ilusion/users/models.py index 71a8362..0621e53 100644 --- a/tienda_ilusion/users/models.py +++ b/tienda_ilusion/users/models.py @@ -1,3 +1,19 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. + +class UserProfile(models.Model): + USER_TYPE_CHOICES = [ + ("publico", "Público"), + ("user", "Usuario"), + ("administrator", "Administrador"), + ] + user = models.OneToOneField( + User, on_delete=models.CASCADE, related_name="profile" + ) + user_type = models.CharField( + max_length=20, choices=USER_TYPE_CHOICES, default="user" + ) + + def __str__(self): + return f"{self.user.username} - {self.user_type}" diff --git a/tienda_ilusion/users/serializers.py b/tienda_ilusion/users/serializers.py index 1a09ef3..2e7f342 100644 --- a/tienda_ilusion/users/serializers.py +++ b/tienda_ilusion/users/serializers.py @@ -1,13 +1,25 @@ from django.contrib.auth.models import User from rest_framework import serializers +from .models import UserProfile + class UserSerializer(serializers.ModelSerializer): role = serializers.SerializerMethodField() class Meta: model = User - fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role') + fields = ( + "id", + "username", + "email", + "first_name", + "last_name", + "role", + ) def get_role(self, obj): - return 'administrator' if obj.is_staff else 'user' + try: + return obj.profile.user_type + except UserProfile.DoesNotExist: + return "administrator" if obj.is_staff else "user" diff --git a/tienda_ilusion/users/signals.py b/tienda_ilusion/users/signals.py new file mode 100644 index 0000000..d1d0616 --- /dev/null +++ b/tienda_ilusion/users/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth.models import User + +from .models import UserProfile + + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + user_type = "administrator" if instance.is_staff else "user" + UserProfile.objects.create(user=instance, user_type=user_type) diff --git a/tienda_ilusion/users/tests.py b/tienda_ilusion/users/tests.py index 5b4da6d..c96d524 100644 --- a/tienda_ilusion/users/tests.py +++ b/tienda_ilusion/users/tests.py @@ -4,6 +4,8 @@ from django.contrib.auth.models import User from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken +from .models import UserProfile + class MeEndpointTests(TestCase): def setUp(self): @@ -49,7 +51,6 @@ class MeEndpointTests(TestCase): username='regular', email='regular@example.com', password='regularpass', - is_staff=False ) refresh = RefreshToken.for_user(regular_user) @@ -87,6 +88,31 @@ class MeEndpointTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['role'], 'administrator') + def test_publico_user_role_is_publico(self): + """ + Verifica que un usuario con user_type='publico' recibe role 'publico'. + """ + publico_user = User.objects.create_user( + username='publico', + email='publico@example.com', + password='publicopass', + ) + profile = UserProfile.objects.get(user=publico_user) + profile.user_type = 'publico' + profile.save() + + refresh = RefreshToken.for_user(publico_user) + access_token = str(refresh.access_token) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f'Bearer {access_token}') + + url = reverse('current-user') + response = client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['role'], 'publico') + def test_me_endpoint_requires_authentication(self): """ Sin token el endpoint debe devolver 401 Unauthorized.