From f8b26fa63d7025d79f387ea8b2a03de873e94d55 Mon Sep 17 00:00:00 2001 From: Yessica Chuctaya Date: Tue, 17 Sep 2024 20:35:24 -0500 Subject: [PATCH 1/3] Agregando proyecto Back end --- .gitignore | 51 +++++ animal_shelter/api/__init__.py | 0 animal_shelter/api/apps.py | 6 + animal_shelter/api/migrations/__init__.py | 0 animal_shelter/api/shelter/__init__.py | 0 animal_shelter/api/shelter/permissions.py | 44 ++++ animal_shelter/api/shelter/routers.py | 9 + animal_shelter/api/shelter/serializers.py | 80 ++++++++ animal_shelter/api/shelter/views.py | 90 +++++++++ animal_shelter/api/tests.py | 3 + animal_shelter/api/urls.py | 18 ++ animal_shelter/apps/__init__.py | 0 animal_shelter/apps/shelter/__init__.py | 0 animal_shelter/apps/shelter/admin.py | 17 ++ animal_shelter/apps/shelter/apps.py | 5 + .../apps/shelter/migrations/0001_initial.py | 80 ++++++++ .../apps/shelter/migrations/__init__.py | 0 animal_shelter/apps/shelter/models.py | 191 ++++++++++++++++++ animal_shelter/apps/shelter/permissions .py | 38 ++++ animal_shelter/apps/shelter/tests.py | 3 + animal_shelter/config/__init__.py | 0 animal_shelter/config/asgi.py | 16 ++ animal_shelter/config/settings.py | 175 ++++++++++++++++ animal_shelter/config/urls.py | 24 +++ animal_shelter/config/wsgi.py | 16 ++ animal_shelter/manage.py | 22 ++ animal_shelter/requirements.txt | 9 + 27 files changed, 897 insertions(+) create mode 100644 .gitignore create mode 100644 animal_shelter/api/__init__.py create mode 100644 animal_shelter/api/apps.py create mode 100644 animal_shelter/api/migrations/__init__.py create mode 100644 animal_shelter/api/shelter/__init__.py create mode 100644 animal_shelter/api/shelter/permissions.py create mode 100644 animal_shelter/api/shelter/routers.py create mode 100644 animal_shelter/api/shelter/serializers.py create mode 100644 animal_shelter/api/shelter/views.py create mode 100644 animal_shelter/api/tests.py create mode 100644 animal_shelter/api/urls.py create mode 100644 animal_shelter/apps/__init__.py create mode 100644 animal_shelter/apps/shelter/__init__.py create mode 100644 animal_shelter/apps/shelter/admin.py create mode 100644 animal_shelter/apps/shelter/apps.py create mode 100644 animal_shelter/apps/shelter/migrations/0001_initial.py create mode 100644 animal_shelter/apps/shelter/migrations/__init__.py create mode 100644 animal_shelter/apps/shelter/models.py create mode 100644 animal_shelter/apps/shelter/permissions .py create mode 100644 animal_shelter/apps/shelter/tests.py create mode 100644 animal_shelter/config/__init__.py create mode 100644 animal_shelter/config/asgi.py create mode 100644 animal_shelter/config/settings.py create mode 100644 animal_shelter/config/urls.py create mode 100644 animal_shelter/config/wsgi.py create mode 100644 animal_shelter/manage.py create mode 100644 animal_shelter/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1feae6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# TEMPs +*.DS_Store +*.bak +*.egg-info +*.old +*.orig +*.pyc +*~ +.*.swp +.*~ + +# IDEs +*.code-workspace +*.log* +*.nja +*.sublime-* +*.~* +.hgignore +.idea/* +.komodoproject +.komodotools +.project +.pydevproject +animal_shelter/.settings/* + +# PROJECT +.env +.profiles/* +credentials.json +local_settings.py +animal_shelter/.venv/* +animal_shelter/.vscode/* +animal_shelter/bin/* +animal_shelter/browsermob* +animal_shelter/data*.csv +animal_shelter/data/* +animal_shelter/env/* +animal_shelter/media/* +animal_shelter/project/settings_* +animal_shelter/static_files/* + +# DBs +*.dat +*.db +*.db-journal +*.sqlite +*.sqlite3 +data/* + +# +__pycache__/ diff --git a/animal_shelter/api/__init__.py b/animal_shelter/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/api/apps.py b/animal_shelter/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/animal_shelter/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/animal_shelter/api/migrations/__init__.py b/animal_shelter/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/api/shelter/__init__.py b/animal_shelter/api/shelter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/api/shelter/permissions.py b/animal_shelter/api/shelter/permissions.py new file mode 100644 index 0000000..68af34a --- /dev/null +++ b/animal_shelter/api/shelter/permissions.py @@ -0,0 +1,44 @@ +from rest_framework.permissions import BasePermission + +class IsAdmin(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_authenticated and request.user.is_superuser + +class IsVolunteer(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_authenticated and (request.user.is_superuser or request.user.is_volunteer) + +class IsAdoptant(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_authenticated and (request.user.is_superuser or request.user.is_adoptant) + +class IsOwnResource(BasePermission): + def has_object_permission(self, request, view, obj): + if request.user.is_volunteer: + # Los voluntarios solo pueden acceder a su propio registro + return obj.id == request.user.id and request.user.is_volunteer + if request.user.is_adoptant: + # Los adoptantes solo pueden acceder a su propio registro + return obj.id == request.user.id and request.user.is_adoptant + return False + + def has_permission(self, request, view): + # Permite listar y consultar para todos los usuarios autenticados + if request.method in ['GET']: + return True + + if request.user.is_volunteer: + # Los voluntarios no pueden crear, actualizar ni eliminar otros usuarios + if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: + return False + # Los voluntarios pueden ver a otros adoptantes (GET requests) + if view.basename == 'adoptant': + return True + + if request.user.is_adoptant: + # Los adoptantes pueden ver y modificar solo su propio perfil + if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: + return request.user.is_adoptant and view.kwargs.get('pk') == str(request.user.id) + return True + + return False diff --git a/animal_shelter/api/shelter/routers.py b/animal_shelter/api/shelter/routers.py new file mode 100644 index 0000000..71406c1 --- /dev/null +++ b/animal_shelter/api/shelter/routers.py @@ -0,0 +1,9 @@ +from rest_framework.routers import DefaultRouter +from .views import AnimalViewSet, AdoptionViewSet, VolunteerViewSet, AdoptantViewSet + +router = DefaultRouter() +# Registra los ViewSets con nombres base únicos +router.register(r'volunteers', VolunteerViewSet, basename='volunteer') +router.register(r'adoptants', AdoptantViewSet, basename='adoptant') +router.register(r'animals', AnimalViewSet, basename='animal') +router.register(r'adoptions', AdoptionViewSet, basename='adoption') \ No newline at end of file diff --git a/animal_shelter/api/shelter/serializers.py b/animal_shelter/api/shelter/serializers.py new file mode 100644 index 0000000..0ebabe2 --- /dev/null +++ b/animal_shelter/api/shelter/serializers.py @@ -0,0 +1,80 @@ +from rest_framework import serializers +from apps.shelter.models import ShelterUser, Animal, Adoption + + +#-----Voluntarios +class VolunteerSerializer(serializers.ModelSerializer): + class Meta: + model = ShelterUser + fields = ('id', 'first_name', 'last_name', 'email', 'is_volunteer', 'is_active') + +class VolunteerCreationSerializer(serializers.ModelSerializer): + password1 = serializers.CharField(write_only=True) + password2 = serializers.CharField(write_only=True) + + class Meta: + model = ShelterUser + fields = ('first_name', 'last_name', 'email', 'password1', 'password2', 'is_volunteer') + + def validate(self, data): + if data['password1'] != data['password2']: + raise serializers.ValidationError({"password": "Passwords must match"}) + return data + + def create(self, validated_data): + user = ShelterUser( + first_name=validated_data['first_name'], + last_name=validated_data['last_name'], + email=validated_data['email'], + is_volunteer=True + ) + user.set_password(validated_data['password1']) + user.save() + return user + +#-----Adoptant +class AdoptantSerializer(serializers.ModelSerializer): + class Meta: + model = ShelterUser + fields = ('id', 'first_name', 'last_name', 'email', 'is_adoptant', 'is_active') + +class AdoptantCreationSerializer(serializers.ModelSerializer): + password1 = serializers.CharField(write_only=True) + password2 = serializers.CharField(write_only=True) + + class Meta: + model = ShelterUser + fields = ('first_name', 'last_name', 'email', 'password1', 'password2', 'is_adoptant') + + def validate(self, data): + if data['password1'] != data['password2']: + raise serializers.ValidationError({"password": "Passwords must match"}) + return data + + def create(self, validated_data): + user = ShelterUser( + first_name=validated_data['first_name'], + last_name=validated_data['last_name'], + email=validated_data['email'], + is_adoptant=True + ) + user.set_password(validated_data['password1']) + user.save() + return user + + +# -------Animals +class AnimalSerializer(serializers.ModelSerializer): + class Meta: + model = Animal + fields = ['id', 'name', 'age', 'breed', 'type', 'status'] + +#--------Adoption +class AdoptionSerializer(serializers.ModelSerializer): + animal = AnimalSerializer(read_only=True) + volunteer = VolunteerSerializer(read_only=True) + adopter = AdoptantSerializer(read_only=True) + + class Meta: + model = Adoption + fields = ['id', 'animal', 'volunteer', 'adopter', 'adoption_date', 'status'] diff --git a/animal_shelter/api/shelter/views.py b/animal_shelter/api/shelter/views.py new file mode 100644 index 0000000..fe6817f --- /dev/null +++ b/animal_shelter/api/shelter/views.py @@ -0,0 +1,90 @@ +from rest_framework import viewsets, permissions +from apps.shelter.models import ShelterUser, Animal, Adoption +from .serializers import AnimalSerializer, AdoptionSerializer +from .serializers import VolunteerSerializer, AdoptantSerializer, \ + VolunteerCreationSerializer, AdoptantCreationSerializer +from .permissions import IsAdmin, IsAdoptant, IsVolunteer, IsOwnResource +from rest_framework.permissions import IsAuthenticated, exceptions + +class VolunteerViewSet(viewsets.ModelViewSet): + queryset = ShelterUser.objects.filter(is_volunteer=True) + permission_classes = [IsAuthenticated, IsOwnResource] + + def get_queryset(self): + if self.request.user.is_volunteer: + return ShelterUser.objects.filter(id=self.request.user.id, is_volunteer=True) + elif self.request.user.is_superuser: + return ShelterUser.objects.filter(is_volunteer=True) + return ShelterUser.objects.none() + + def get_serializer_class(self): + if self.request.method == 'POST': + return VolunteerCreationSerializer + return VolunteerSerializer + +class AdoptantViewSet(viewsets.ModelViewSet): + queryset = ShelterUser.objects.filter(is_adoptant=True) + permission_classes = [IsAuthenticated, IsOwnResource] + + def get_queryset(self): + # Un adoptante solo puede ver su propio perfil + if self.request.user.is_adoptant: + return ShelterUser.objects.filter(id=self.request.user.id, is_adoptant=True) + # Los voluntarios pueden ver todos los adoptantes + elif self.request.user.is_volunteer or self.request.user.is_superuser: + return ShelterUser.objects.filter(is_adoptant=True) + # Si no es adoptante ni voluntario, no tiene acceso + return ShelterUser.objects.none() + + def get_serializer_class(self): + if self.request.method == 'POST': + return AdoptantCreationSerializer + return AdoptantSerializer + +class AnimalViewSet(viewsets.ModelViewSet): + # queryset = Animal.objects.all() + serializer_class = AnimalSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_adoptant: + return Animal.objects.filter(status=Animal.STATUS_AVAILABLE) + elif self.request.user.is_volunteer: + return Animal.objects.all() + elif self.request.user.is_superuser: + return Animal.objects.all() + else: + return Animal.objects.none() + + def perform_update(self, serializer): + if self.request.user.is_volunteer or self.request.user.is_superuser: + serializer.save() + else: + raise exceptions.PermissionDenied("No tienes permiso para modificar el estado del animal.") + # raise permissions.PermissionDenied("No tienes permiso para modificar el estado del animal") + + # def get_permissions(self): + # if self.request.method in ['POST', 'PUT', 'PATCH']: + # if not (self.request.user.is_volunteer or self.request.user.is_superuser): + # return [permissions.Denied("No tienes permiso para modificar animales.")] + # return super().get_permissions() + +class AdoptionViewSet(viewsets.ModelViewSet): + queryset = Adoption.objects.all() + serializer_class = AdoptionSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_adoptant: + return Adoption.objects.filter(adopter=self.request.user) + elif self.request.user.is_volunteer or self.request.user.is_superuser: + return Adoption.objects.all() + else: + return Adoption.objects.none() + + def perform_create(self, serializer): + if self.request.user.is_adoptant: + serializer.save(adopter=self.request.user) + else: + raise permissions.PermissionDenied("No tienes permiso para solicitar una adopción") + diff --git a/animal_shelter/api/tests.py b/animal_shelter/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/animal_shelter/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/animal_shelter/api/urls.py b/animal_shelter/api/urls.py new file mode 100644 index 0000000..b16533a --- /dev/null +++ b/animal_shelter/api/urls.py @@ -0,0 +1,18 @@ + +from django.urls import path, include +from api.shelter.routers import router as RouterShelter +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from rest_framework.permissions import IsAdminUser + +urlpatterns = [ + + # API Endpoints + path('api/', include(RouterShelter.urls)), + # Tokens + path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + # Swagger + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), +] \ No newline at end of file diff --git a/animal_shelter/apps/__init__.py b/animal_shelter/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/apps/shelter/__init__.py b/animal_shelter/apps/shelter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/apps/shelter/admin.py b/animal_shelter/apps/shelter/admin.py new file mode 100644 index 0000000..0361af2 --- /dev/null +++ b/animal_shelter/apps/shelter/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import ShelterUser, Animal, Adoption + +class ShelterUserAdmin(UserAdmin): + model = ShelterUser + fieldsets = UserAdmin.fieldsets + ( + (None, {'fields': ('is_volunteer', 'is_adoptant')}), + ) + add_fieldsets = UserAdmin.add_fieldsets + ( + (None, {'fields': ('is_volunteer', 'is_adoptant')}), + ) + +admin.site.register(ShelterUser, ShelterUserAdmin) +admin.site.register(Animal) +admin.site.register(Adoption) + diff --git a/animal_shelter/apps/shelter/apps.py b/animal_shelter/apps/shelter/apps.py new file mode 100644 index 0000000..455b5a2 --- /dev/null +++ b/animal_shelter/apps/shelter/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class ShelterConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.shelter' diff --git a/animal_shelter/apps/shelter/migrations/0001_initial.py b/animal_shelter/apps/shelter/migrations/0001_initial.py new file mode 100644 index 0000000..85c69f8 --- /dev/null +++ b/animal_shelter/apps/shelter/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# Generated by Django 5.1.1 on 2024-09-17 15:30 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Animal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Creado')), + ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Actualizado')), + ('name', models.CharField(blank=True, max_length=50, verbose_name='Nombre')), + ('age', models.PositiveIntegerField(blank=True, null=True, verbose_name='Edad')), + ('breed', models.CharField(blank=True, max_length=50, verbose_name='Raza')), + ('type', models.CharField(blank=True, choices=[('dog', 'Perro'), ('cat', 'Gato')], max_length=3, verbose_name='Tipo')), + ('status', models.CharField(choices=[('available', 'Disponible'), ('adopted', 'Adoptado'), ('in_process', 'En Proceso'), ('waiting', 'En Espera')], default='available', max_length=20, verbose_name='Estado')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ShelterUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('is_volunteer', models.BooleanField(default=False)), + ('is_adoptant', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(blank=True, related_name='shelter_users', to='auth.group')), + ('user_permissions', models.ManyToManyField(blank=True, related_name='shelter_users', to='auth.permission')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Adoption', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Creado')), + ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Actualizado')), + ('adoption_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Fecha')), + ('status', models.CharField(choices=[('completed', 'Finalizado'), ('in_process', 'En Proceso')], default='in_process', max_length=20, verbose_name='Estado')), + ('adopter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adopted_animals', to=settings.AUTH_USER_MODEL, verbose_name='Adoptante')), + ('volunteer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_adoptions', to=settings.AUTH_USER_MODEL, verbose_name='Voluntario')), + ('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adoptions', to='shelter.animal', verbose_name='Animal')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/animal_shelter/apps/shelter/migrations/__init__.py b/animal_shelter/apps/shelter/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/apps/shelter/models.py b/animal_shelter/apps/shelter/models.py new file mode 100644 index 0000000..389d2f3 --- /dev/null +++ b/animal_shelter/apps/shelter/models.py @@ -0,0 +1,191 @@ +from django.db import models +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.utils import timezone +from django.contrib.auth.models import AbstractUser + + +# class CustomUserManager(BaseUserManager): +# def create_user(self, email, password=None, **extra_fields): +# if not email: +# raise ValueError('The Email field must be set') +# email = self.normalize_email(email) +# user = self.model(email=email, **extra_fields) +# user.set_password(password) +# user.save(using=self._db) +# return user + +# def create_superuser(self, email, password=None, **extra_fields): +# extra_fields.setdefault('is_staff', True) +# extra_fields.setdefault('is_superuser', True) +# return self.create_user(email, password, **extra_fields) + +# class CustomUser(AbstractBaseUser): +# STATUS_CHOICES = ( +# ('active', 'Activo'), +# ('inactive', 'Inactivo'), +# ) + +# USER_TYPE_CHOICES = ( +# ('volunteer', 'Voluntario'), +# ('adopter', 'Adoptante'), +# ('admin', 'Administrador'), +# ) + +# email = models.EmailField(unique=True, verbose_name='Correo Electrónico') +# first_name = models.CharField(max_length=30, verbose_name='Nombre') +# last_name = models.CharField(max_length=30, verbose_name='Apellido') +# status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='active', verbose_name='Estado') +# user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES) +# is_staff = models.BooleanField(default=False) +# is_active = models.BooleanField(default=True) +# is_superuser = models.BooleanField(default=False) + +# objects = CustomUserManager() + +# USERNAME_FIELD = 'email' +# REQUIRED_FIELDS = ['user_type'] + +# def __str__(self): +# return self.email + +# def has_perm(self, perm, obj=None): +# """Devuelve True si el usuario tiene el permiso especificado.""" +# return self.is_superuser + +# def has_module_perms(self, app_label): +# """Devuelve True si el usuario tiene permisos para ver las apps del módulo especificado.""" +# return self.is_superuser + +# # groups = models.ManyToManyField( +# # 'auth.Group', +# # related_name='customuser_set', +# # blank=True, +# # ) +# # user_permissions = models.ManyToManyField( +# # 'auth.Permission', +# # related_name='customuser_set', +# # blank=True, +# # ) + +class ShelterUser(AbstractUser): + is_volunteer = models.BooleanField(default=False) + is_adoptant = models.BooleanField(default=False) + + # Cambiar los `related_name` para evitar conflictos + groups = models.ManyToManyField( + Group, + related_name='shelter_users', + blank=True, + ) + user_permissions = models.ManyToManyField( + Permission, + related_name='shelter_users', + blank=True, + ) + +# Create your models here. +class BaseDataModel(models.Model): + created_at = models.DateTimeField( + verbose_name='Creado', + auto_now_add=True, + null=True, + editable=False, + ) + updated_at = models.DateTimeField( + verbose_name='Actualizado', + auto_now=True, + null=True, + editable=False, + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if not self.updated_at: + self.updated_at = timezone.now() + super().save(*args, **kwargs) + + +# Animal Model +class Animal(BaseDataModel): + TYPE_DOG = 'dog' + TYPE_CAT = 'cat' + STATUS_AVAILABLE = 'available' + STATUS_ADOPTED = 'adopted' + STATUS_IN_PROCESS = 'in_process' + STATUS_WAITING = 'waiting' + + TYPE_CHOICES = ( + (TYPE_DOG, 'Perro'), + (TYPE_CAT, 'Gato'), + ) + + STATUS_CHOICES = ( + (STATUS_AVAILABLE, 'Disponible'), + (STATUS_ADOPTED, 'Adoptado'), + (STATUS_IN_PROCESS, 'En Proceso'), + (STATUS_WAITING, 'En Espera'), + ) + + name = models.CharField( + max_length=50, + verbose_name='Nombre', + blank=True + ) + age = models.PositiveIntegerField( + verbose_name='Edad', + blank=True, + null=True + ) + breed = models.CharField( + max_length=50, + verbose_name='Raza', + blank=True + ) + type = models.CharField( + max_length=3, + choices=TYPE_CHOICES, + verbose_name='Tipo', + blank=True + ) + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='available', + verbose_name='Estado' + ) + + def __str__(self): + return f"{self.name or 'Sin Nombre'} ({self.get_type_display()})" + + def get_name(self): + return self.name or "No proporcionado" + + def get_age(self): + return self.age if self.age is not None else "No proporcionada" + + def get_breed(self): + return self.breed or "No proporcionada" + + def get_status(self): + return self.get_status_display() + + + +# Adoption Model +class Adoption(BaseDataModel): + STATUS_CHOICES = ( + ('completed', 'Finalizado'), + ('in_process', 'En Proceso'), + ) + animal = models.ForeignKey(Animal, on_delete=models.CASCADE,related_name='adoptions', verbose_name='Animal') + volunteer = models.ForeignKey(ShelterUser, on_delete=models.CASCADE, related_name='volunteer_adoptions', verbose_name='Voluntario') + adopter = models.ForeignKey(ShelterUser, on_delete=models.CASCADE, related_name='adopted_animals', verbose_name='Adoptante') + adoption_date = models.DateTimeField(default=timezone.now, verbose_name='Fecha') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='in_process', verbose_name='Estado') + + def __str__(self): + return f"Adopción de {self.animal.name} por {self.adopter.first_name} {self.adopter.last_name}" diff --git a/animal_shelter/apps/shelter/permissions .py b/animal_shelter/apps/shelter/permissions .py new file mode 100644 index 0000000..08b5975 --- /dev/null +++ b/animal_shelter/apps/shelter/permissions .py @@ -0,0 +1,38 @@ +from rest_framework import permissions +from rest_framework import permissions + +class IsAdminOrReadOnly(permissions.BasePermission): + """ + Permiso para permitir solo a los administradores realizar cambios. + """ + def has_permission(self, request, view): + # Solo los admins pueden hacer POST, PUT o DELETE + if request.method in permissions.SAFE_METHODS: + return True + return request.user and request.user.is_staff + +class IsVolunteerOrReadOnly(permissions.BasePermission): + """ + Permiso para permitir solo a los voluntarios acceder a ciertas vistas. + """ + def has_permission(self, request, view): + if request.user.user_type == 'volunteer': + return True + return request.method in permissions.SAFE_METHODS + + +class IsAdminUser(permissions.BasePermission): + """ + Permiso para permitir solo a los administradores acceder a ciertas vistas. + """ + def has_permission(self, request, view): + return request.user and request.user.is_staff + +class IsVolunteerOrReadOnly(permissions.BasePermission): + """ + Permiso para permitir a los voluntarios cambiar el estado de un animal. + """ + def has_permission(self, request, view): + if request.user and request.user.user_type == 'volunteer': + return True + return request.method in permissions.SAFE_METHODS diff --git a/animal_shelter/apps/shelter/tests.py b/animal_shelter/apps/shelter/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/animal_shelter/apps/shelter/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/animal_shelter/config/__init__.py b/animal_shelter/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/animal_shelter/config/asgi.py b/animal_shelter/config/asgi.py new file mode 100644 index 0000000..400e9e3 --- /dev/null +++ b/animal_shelter/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/animal_shelter/config/settings.py b/animal_shelter/config/settings.py new file mode 100644 index 0000000..4729a5e --- /dev/null +++ b/animal_shelter/config/settings.py @@ -0,0 +1,175 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 5.1.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-%9@mk^aa&aro(uk!!-!=^u%8ifcr2dex#whf^c1btdf74r!fvm' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'drf_spectacular', + 'api', + 'apps.shelter', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + # Otros middlewares personalizados o adicionales +] + + +CORS_ALLOWED_ORIGINS = [ + 'http://localhost:8000', + 'http://127.0.0.1:8000', +] + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +# SIMPLE_JWT = { +# 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), +# 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), +# } +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=2), +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'Shelter API', + 'DESCRIPTION': 'API documentation for the Shelter application', + 'VERSION': 'v1', + 'CONTACT': { + 'email': 'contact@shelter.local', + }, + 'LICENSE': { + 'name': 'BSD License', + }, + 'SERVE_INCLUDE_SCHEMA': False, # Esto evita mostrar el esquema dentro del Swagger UI + 'COMPONENT_SPLIT_REQUEST': True, + 'SERVERS': [ + {'url': 'http://localhost:8000', 'description': 'Development server'}, + ], +} + + + +AUTH_USER_MODEL = 'shelter.ShelterUser' + + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/animal_shelter/config/urls.py b/animal_shelter/config/urls.py new file mode 100644 index 0000000..ace4e54 --- /dev/null +++ b/animal_shelter/config/urls.py @@ -0,0 +1,24 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include("api.urls")), +] diff --git a/animal_shelter/config/wsgi.py b/animal_shelter/config/wsgi.py new file mode 100644 index 0000000..e232e56 --- /dev/null +++ b/animal_shelter/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/animal_shelter/manage.py b/animal_shelter/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/animal_shelter/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/animal_shelter/requirements.txt b/animal_shelter/requirements.txt new file mode 100644 index 0000000..b715ce8 --- /dev/null +++ b/animal_shelter/requirements.txt @@ -0,0 +1,9 @@ +asgiref==3.8.1 +Django==5.1.1 +django-cors-headers==4.4.0 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +psycopg2-binary==2.9.9 +PyJWT==2.9.0 +sqlparse==0.5.1 +tzdata==2024.1 From afb1e2a27908d955a329791daa6aef0231e6d21c Mon Sep 17 00:00:00 2001 From: Yessica Chuctaya Date: Tue, 17 Sep 2024 22:26:54 -0500 Subject: [PATCH 2/3] [FIX] Agregando Readme y test --- README.md | 191 ++++++++++++++++------ animal_shelter/api/shelter/permissions.py | 4 + animal_shelter/api/shelter/views.py | 6 + animal_shelter/api/tests.py | 127 +++++++++++++- animal_shelter/apps/shelter/models.py | 69 +------- animal_shelter/requirements.txt | 1 + 6 files changed, 277 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index dbb4cbb..48fcd26 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,154 @@ -# FULLSTACK TECHNICAL TEST -## Creación de un API REST con Django Rest Framework y un client web con React +Proyecto Animal Shelter (Shelter API) +================= -La prueba consiste en crear un API REST para un único recurso, la cual será consumida desde un cliente web. La generación de todos los archivos necesarios y el manejo de una óptima estructura de archivos debe ser realizado por el/la postulante. Genere un fork de este repositorio. Utilice versiones con soporte de Python, Django y React. +## Guidelines -# Objetivos de la prueba -* Evaluar el pensamiento computacional y el enfoque empleado al resolver problemas. -* Evaluar el conocimiento general del lenguaje Python y de React. -* Evaluar el conocimiento en arquitecturas web (MVC, REST). -* Evaluar buenas prácticas en el código. -* Evaluar el conocimiento de buenas prácticas de seguridad. - -## Requerimientos generales +------ -1. El API REST debe ser desarrollado con Django Rest Framework. -2. El cliente web debe ser desarrollado con React + Typescript. -3. El cliente web debe consumir el API REST. -4. Se debe utilizar el ORM de Django para el modelo de la base de datos. -5. Se debe crear un archivo `requirements.txt` con las dependencias necesarias para ejecutar el proyecto. -6. Se debe crear un archivo `README.md` en el cual se explique el funcionamiento del proyecto, así como las instrucciones para ejecutarlo. -7. Se debe crear un archivo `.env` que contenga las variables de entorno necesarias para la ejecución del proyecto. +API para administrar un albergue de animales rescatados, permitiendo gestionar animales (perros y gatos), voluntarios, adoptantes, y las adopciones. Esta API ofrece operaciones CRUD para cada entidad y está protegida mediante autenticación JWT. -## Requerimientos backend -1. Generar un API para administrar un albergue de animales rescatados (perros y gatos). Se debe poder llevar un control de los animales en albergue: nombre, edad, raza, tipo (perro o gato), estado (adoptado, en adopción, en espera de adopción, etc). Se debe poder llevar un control de los voluntarios que trabajan en el albergue: nombre, apellido, correo electrónico, contraseña, estado (activo, inactivo, etc). Se debe poder llevar un control de las personas que adoptan animales: nombre, apellido, correo electrónico, contraseña, estado (activo, inactivo, etc). Se debe poder llevar un control de las adopciones, es decir, el registro de cuándo y a quién se le entregó un animal: fecha, animal, voluntario, persona que adopta, estado (finalizado, en proceso, etc). +## Índice - **Nota:** El endpoint debe permitir realizar las siguientes operaciones: - * Listar todos los animales en albergue. [GET] - * Listar todos los voluntarios. [GET] - * Listar todos los adoptantes. [GET] - * Listar todas las adopciones. [GET] - * Crear un animal en albergue. [POST] - * Crear un voluntario. [POST] - * Crear un adoptante. [POST] - * Crear una adopción. [POST] - * Obtener un animal en albergue. [GET by ID] - * Obtener una adopción. [GET by ID] - * Actualizar un animal en albergue. [POST/PATCH] - * Actualizar un voluntario. [POST/PATCH] - * Actualizar un adoptante. [POST/PATCH] - * Actualizar una adopción. [POST/PATCH] - * Eliminar un animal en albergue. [DELETE] - * Eliminar un voluntario. [DELETE] - * Eliminar un adoptante. [DELETE] - * Eliminar una adopción. [DELETE] +1. [Descripción](#descripción) +2. [Características](#características) +3. [Requerimientos](#requerimientos) +4. [Instalación](#instalación) +5. [Uso](#uso) +6. [Autenticación](#autenticación) +7. [Documentación de la API](#documentación-de-la-api) +8. [Contribuciones](#contribuciones) +9. [Licencia](#licencia) -2. El modelo de la base de datos queda a elección del postulante; sin embargo, esta debe tener todo lo necesario para plasmar lo explicado en el requerimiento anterior. +## Descripción -3. Se desea validar que los requests cumplan las restricciones establecidas en el modelo de la base de datos. Los errores deben mostrarse al usuario en el response en caso de haberlos. +La **Shelter API** es una aplicación diseñada para administrar un albergue de animales, permitiendo llevar el control de: +- Animales en el albergue (nombre, raza, edad, tipo, estado de adopción). +- Voluntarios (información personal y estado). +- Adoptantes (información personal y estado). +- Adopciones (registro de adopciones realizadas). -4. Se desea que el API sea restringido mediante algún método de autenticación. Queda a elección del postulante el approach de autenticación a seguir. El nivel de seguridad será evaluado. +## Características -5. Se desea que el usuario se autentifique mediante json web tokens, para ello se utilizará la librería Simple JWT para Django rest framework. (ACCESS + REFRESH TOKEN) +- Autenticación mediante JSON Web Tokens (JWT). +- Operaciones CRUD completas para las entidades: + - Animales + - Voluntarios + - Adoptantes + - Adopciones +- Control de acceso mediante autenticación JWT (utilizando la librería **Simple JWT** para Django REST Framework). -## Requerimientos frontend +## Requerimientos -1. Debe utilizar React + Typescript junto al framework [Mantine UI](https://mantine.dev/). +- Python 3.8+ +- Django 4.0+ +- Django REST Framework 3.12+ +- Simple JWT -2. Debe contar con un solo registro para los voluntarios y adoptantes. Los administradores (superusers) solo pueden ser creados desde Django admin. Para terminos prácticos puede asumir que los roles son excluyentes. +## Instalación -3. Debe contar con un Login unificado para los voluntarios, adoptantes y administradores. +### 1. Clonar el repositorio: -4. Los administradores pueden acceder al CRUD de los animales en albergue, de los voluntarios, de los adoptantes y de las adopciones. +```bash +git clone https://github.com/usuario/shelter-api.git +cd shelter-api +``` -5. Los adoptantes solo pueden visualizar los animales disponibles para adopción y solicitar la adopción de un animal. - -7. Los voluntarios pueden visualizar los animales del albergue, los adoptantes y las adopciones. Además, pueden cambiar el estado de un animal en albergue a "adoptado" o "en adopción". +### 2. Crear y activar un entorno virtual: +```bash +python -m venv venv +source venv/bin/activate # En Windows: venv\Scripts\activate +``` -## Adicionales -1. Deployar el cliente web y servidor backend en la capa gratuita de algún servicio cloud. -2. Realizar un esquema de testing para el backend, basado en probar los endpoints, y todo lo que ello implica. +### 3. Instalar dependencias: + +```bash +pip install -r requirements.txt +``` + +### 4. Configurar las variables de entorno: + +Crea un archivo `.env` en la raíz del proyecto con las siguientes variables: + +```bash +SECRET_KEY=your_secret_key +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 +DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/DB_NAME +``` + +### 5. Migraciones: + +Ejecuta las migraciones para configurar la base de datos: + +```bash +python manage.py migrate +``` + +### 6. Crear superusuario (opcional para el admin): + +```bash +python manage.py createsuperuser +``` + +### 7. Iniciar el servidor de desarrollo: + +```bash +python manage.py runserver +``` + +## Uso + +### Endpoints principales + +- **Adoptantes**: Gestión de las personas que adoptan animales. + - `GET /api/adoptants/`: Listar todos los adoptantes. + - `POST /api/adoptants/`: Crear un adoptante. + - `GET /api/adoptants/{id}/`: Obtener información de un adoptante. + - `PUT /api/adoptants/{id}/`: Actualizar adoptante. + - `PATCH /api/adoptants/{id}/`: Actualizar parcialmente un adoptante. + - `DELETE /api/adoptants/{id}/`: Eliminar adoptante. + +- **Animales**: Gestión de los animales rescatados. + - `GET /api/animals/`: Listar todos los animales. + - `POST /api/animals/`: Crear un nuevo animal. + - `GET /api/animals/{id}/`: Obtener información de un animal. + - `PUT /api/animals/{id}/`: Actualizar un animal. + - `PATCH /api/animals/{id}/`: Actualizar parcialmente un animal. + - `DELETE /api/animals/{id}/`: Eliminar un animal. + +- **Adopciones**: Gestión de las adopciones. + - `GET /api/adoptions/`: Listar todas las adopciones. + - `POST /api/adoptions/`: Registrar una nueva adopción. + - `GET /api/adoptions/{id}/`: Obtener detalles de una adopción. + - `PUT /api/adoptions/{id}/`: Actualizar una adopción. + - `PATCH /api/adoptions/{id}/`: Actualizar parcialmente una adopción. + - `DELETE /api/adoptions/{id}/`: Eliminar una adopción. + +- **Voluntarios**: Gestión de los voluntarios del albergue. + - `GET /api/volunteers/`: Listar todos los voluntarios. + - `POST /api/volunteers/`: Crear un voluntario. + - `GET /api/volunteers/{id}/`: Obtener información de un voluntario. + - `PUT /api/volunteers/{id}/`: Actualizar voluntario. + - `PATCH /api/volunteers/{id}/`: Actualizar parcialmente un voluntario. + - `DELETE /api/volunteers/{id}/`: Eliminar voluntario. + +## Autenticación + +La API utiliza autenticación basada en JSON Web Tokens (JWT). Para obtener un token de acceso, realiza una solicitud POST a: + +```bash +POST /api/token/ +``` + +Con las credenciales correctas, recibirás un `access_token` y un `refresh_token` que podrás usar para autenticarte en los demás endpoints. + +Para refrescar el token: + +```bash +POST /api/token/refresh/ +``` + +## Documentación de la API + +La documentación detallada de la API se puede consultar en `/api/schema/swagger-ui/` or cuando el servidor de desarrollo esté en ejecución. \ No newline at end of file diff --git a/animal_shelter/api/shelter/permissions.py b/animal_shelter/api/shelter/permissions.py index 68af34a..0beac62 100644 --- a/animal_shelter/api/shelter/permissions.py +++ b/animal_shelter/api/shelter/permissions.py @@ -20,6 +20,8 @@ def has_object_permission(self, request, view, obj): if request.user.is_adoptant: # Los adoptantes solo pueden acceder a su propio registro return obj.id == request.user.id and request.user.is_adoptant + if request.user.is_superuser: + return True return False def has_permission(self, request, view): @@ -40,5 +42,7 @@ def has_permission(self, request, view): if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: return request.user.is_adoptant and view.kwargs.get('pk') == str(request.user.id) return True + if request.user.is_superuser: + return True return False diff --git a/animal_shelter/api/shelter/views.py b/animal_shelter/api/shelter/views.py index fe6817f..6b3d578 100644 --- a/animal_shelter/api/shelter/views.py +++ b/animal_shelter/api/shelter/views.py @@ -40,6 +40,12 @@ def get_serializer_class(self): if self.request.method == 'POST': return AdoptantCreationSerializer return AdoptantSerializer + + def perform_create(self, serializer): + if self.request.user.is_superuser: + serializer.save() + else: + raise permissions.PermissionDenied("No tienes permiso para crear adoptantes") class AnimalViewSet(viewsets.ModelViewSet): # queryset = Animal.objects.all() diff --git a/animal_shelter/api/tests.py b/animal_shelter/api/tests.py index 7ce503c..5470191 100644 --- a/animal_shelter/api/tests.py +++ b/animal_shelter/api/tests.py @@ -1,3 +1,126 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from apps.shelter.models import ShelterUser, Animal, Adoption +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from django.contrib.auth import get_user_model +from apps.shelter.models import Animal, Adoption +from rest_framework_simplejwt.tokens import RefreshToken -# Create your tests here. +User = get_user_model() + +class VolunteerViewSetTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username='volunteer', password='password', is_volunteer=True) + self.superuser = User.objects.create_superuser(username='superuser', password='password') + self.url = reverse('volunteer-list') # Asumiendo que el nombre de tu vista es 'volunteer-list' + + def authenticate(self, user): + self.client.force_authenticate(user=user) + + def test_list_volunteers(self): + self.authenticate(self.superuser) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_volunteer(self): + self.authenticate(self.superuser) + data = {'username': 'new_volunteer', 'password': 'password', 'is_volunteer': True} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_volunteer_as_non_superuser(self): + self.authenticate(self.user) + data = {'username': 'new_volunteer', 'password': 'password', 'is_volunteer': True} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class AdoptantViewSetTests(APITestCase): + def setUp(self): + self.adoptant = User.objects.create_user(username='adoptant', password='password', is_adoptant=True) + self.volunteer = User.objects.create_user(username='volunteer', password='password', is_volunteer=True) + self.superuser = User.objects.create_superuser(username='superuser', password='password') + self.url = reverse('adoptant-list') # Asumiendo que el nombre de tu vista es 'adoptant-list' + + def authenticate(self, user): + self.client.force_authenticate(user=user) + + def test_list_adoptants(self): + self.authenticate(self.superuser) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_adoptant(self): + self.authenticate(self.superuser) + data = {'username': 'new_adoptant', 'password': 'password', 'is_adoptant': True} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_adoptant_as_non_superuser(self): + self.authenticate(self.adoptant) + data = {'username': 'new_adoptant', 'password': 'password', 'is_adoptant': True} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class AnimalViewSetTests(APITestCase): + def setUp(self): + self.adoptant = User.objects.create_user(username='adoptant', password='password', is_adoptant=True) + self.volunteer = User.objects.create_user(username='volunteer', password='password', is_volunteer=True) + self.superuser = User.objects.create_superuser(username='superuser', password='password') + self.animal = Animal.objects.create(name='Buddy', status=Animal.STATUS_AVAILABLE) + self.url = reverse('animal-list') # Asumiendo que el nombre de tu vista es 'animal-list' + + def authenticate(self, user): + self.client.force_authenticate(user=user) + + def test_list_animals(self): + self.authenticate(self.volunteer) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_animal(self): + self.authenticate(self.volunteer) + data = {'name': 'Charlie', 'status': Animal.STATUS_AVAILABLE} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_update_animal(self): + self.authenticate(self.volunteer) + data = {'name': 'Buddy', 'status': Animal.STATUS_ADOPTED} + response = self.client.patch(reverse('animal-detail', args=[self.animal.id]), data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_update_animal_as_non_volunteer(self): + self.authenticate(self.adoptant) + data = {'name': 'Buddy', 'status': Animal.STATUS_ADOPTED} + response = self.client.patch(reverse('animal-detail', args=[self.animal.id]), data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class AdoptionViewSetTests(APITestCase): + def setUp(self): + self.adoptant = User.objects.create_user(username='adoptant', password='password', is_adoptant=True) + self.volunteer = User.objects.create_user(username='volunteer', password='password', is_volunteer=True) + self.superuser = User.objects.create_superuser(username='superuser', password='password') + self.animal = Animal.objects.create(name='Buddy', status=Animal.STATUS_AVAILABLE) + self.adoption = Adoption.objects.create(adopter=self.adoptant, animal=self.animal, status=Adoption.SA) + self.url = reverse('adoption-list') # Asumiendo que el nombre de tu vista es 'adoption-list' + + def authenticate(self, user): + self.client.force_authenticate(user=user) + + def test_create_adoption(self): + self.authenticate(self.adoptant) + data = {'animal': self.animal.id} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_adoption_as_non_adoptant(self): + self.authenticate(self.volunteer) + data = {'animal': self.animal.id} + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/animal_shelter/apps/shelter/models.py b/animal_shelter/apps/shelter/models.py index 389d2f3..4d313c4 100644 --- a/animal_shelter/apps/shelter/models.py +++ b/animal_shelter/apps/shelter/models.py @@ -6,69 +6,6 @@ from django.contrib.auth.models import AbstractUser -# class CustomUserManager(BaseUserManager): -# def create_user(self, email, password=None, **extra_fields): -# if not email: -# raise ValueError('The Email field must be set') -# email = self.normalize_email(email) -# user = self.model(email=email, **extra_fields) -# user.set_password(password) -# user.save(using=self._db) -# return user - -# def create_superuser(self, email, password=None, **extra_fields): -# extra_fields.setdefault('is_staff', True) -# extra_fields.setdefault('is_superuser', True) -# return self.create_user(email, password, **extra_fields) - -# class CustomUser(AbstractBaseUser): -# STATUS_CHOICES = ( -# ('active', 'Activo'), -# ('inactive', 'Inactivo'), -# ) - -# USER_TYPE_CHOICES = ( -# ('volunteer', 'Voluntario'), -# ('adopter', 'Adoptante'), -# ('admin', 'Administrador'), -# ) - -# email = models.EmailField(unique=True, verbose_name='Correo Electrónico') -# first_name = models.CharField(max_length=30, verbose_name='Nombre') -# last_name = models.CharField(max_length=30, verbose_name='Apellido') -# status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='active', verbose_name='Estado') -# user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES) -# is_staff = models.BooleanField(default=False) -# is_active = models.BooleanField(default=True) -# is_superuser = models.BooleanField(default=False) - -# objects = CustomUserManager() - -# USERNAME_FIELD = 'email' -# REQUIRED_FIELDS = ['user_type'] - -# def __str__(self): -# return self.email - -# def has_perm(self, perm, obj=None): -# """Devuelve True si el usuario tiene el permiso especificado.""" -# return self.is_superuser - -# def has_module_perms(self, app_label): -# """Devuelve True si el usuario tiene permisos para ver las apps del módulo especificado.""" -# return self.is_superuser - -# # groups = models.ManyToManyField( -# # 'auth.Group', -# # related_name='customuser_set', -# # blank=True, -# # ) -# # user_permissions = models.ManyToManyField( -# # 'auth.Permission', -# # related_name='customuser_set', -# # blank=True, -# # ) - class ShelterUser(AbstractUser): is_volunteer = models.BooleanField(default=False) is_adoptant = models.BooleanField(default=False) @@ -177,9 +114,11 @@ def get_status(self): # Adoption Model class Adoption(BaseDataModel): + STATUS_COMPLETED = 'completed' + STATUS_IN_PROCESS = 'in_process' STATUS_CHOICES = ( - ('completed', 'Finalizado'), - ('in_process', 'En Proceso'), + (STATUS_COMPLETED, 'Finalizado'), + (STATUS_IN_PROCESS, 'En Proceso'), ) animal = models.ForeignKey(Animal, on_delete=models.CASCADE,related_name='adoptions', verbose_name='Animal') volunteer = models.ForeignKey(ShelterUser, on_delete=models.CASCADE, related_name='volunteer_adoptions', verbose_name='Voluntario') diff --git a/animal_shelter/requirements.txt b/animal_shelter/requirements.txt index b715ce8..b3dce3c 100644 --- a/animal_shelter/requirements.txt +++ b/animal_shelter/requirements.txt @@ -7,3 +7,4 @@ psycopg2-binary==2.9.9 PyJWT==2.9.0 sqlparse==0.5.1 tzdata==2024.1 +drf-spectacular==0.27.2 \ No newline at end of file From dc8d0bb8b1c1c9b7660e42aa6b55f106a4fd1efa Mon Sep 17 00:00:00 2001 From: Yessica Chuctaya Date: Tue, 17 Sep 2024 22:32:02 -0500 Subject: [PATCH 3/3] [Fix] Ruta del repositorio --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48fcd26..af88e93 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ La **Shelter API** es una aplicación diseñada para administrar un albergue de ### 1. Clonar el repositorio: ```bash -git clone https://github.com/usuario/shelter-api.git -cd shelter-api +git clone git@github.com:ELSA-LA/fullstack-technical-test.git +cd fullstack-technical-test/ ``` ### 2. Crear y activar un entorno virtual: