Compare commits

..

No commits in common. "ba46c42fa78e66989ff1ae13808005f1772bcba3" and "ca9ca658c7969adbd06da8147d4cbd0161fecf59" have entirely different histories.

40 changed files with 15 additions and 2050 deletions

1065
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,285 +0,0 @@
# Recipe API
#### Caracteristicas
- Crear
- Listar
- Ver detalles
- Actualizar
- Borrar
#### Endpoints
- `/recipes/`
- `GET` Listar todas las recetas
- `POST` Crea recetas
- `/recipes/<recipe_id>`/
- `GET` Ver detalles de receta
- `PUT/PATCH` Actualizar receta
- `DELETE` Borrar receta
### APIView vs Viewsets
Una vista maneja un request a una URL, DRF usa clases con lógica reutilizable.
DRF además soporta decoradores.
`APIView` y `Viewsets` son clases base falicitadas por DRF.
### APIView
- Concentradas alrededor de los metodos HTTP
- Métodos de clase para los métodos HTTP `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
- Ofrece flexibilidad sobre toda la estuctura de las URL y la lógica usada para
procesar estas peticiones
- Util para APIs sin CRUD. Lógica a la medida, ej. auth, jobs, apis externas
### Viewsets
- Concentradas alrededor de aciones
- Retrive, list, update, partial update, destroy
- Mapea los modelos de Django
- Usa rutas para generar URLs
- Genial para operaciones CRUD en los modelos
## Test Create Recipe
[core/tests/test_models.py](./app/core/tests/test_models.py)
```py
from decimal import Decimal
...
from core import models
class ModelTests(TestCase):
...
def test_create_recipe(self):
"""Test creating a recipe is successful."""
user = get_user_model().objects.create_user(
'test@example.com',
'test123',
)
recipe = models.Recipe.objects.create(
user=user,
title='Nombre receta ejemplo',
time_minutes=5,
price=Decimal('5.50'),
decription='Descripción de la receta de ejemplo'
)
self.assertEqual(str(recipe), recipe.title)
```
## Creación del modelo
[`core/models.py`](./app/core/models.py)
```py
...
class Recipe(models.Model):
"""Recipe object."""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
time_minutes = models.IntegerField()
price = models.DecimalField(max_digits=5,decimal_places=2, blank=True)
link = models.CharField(max_length=255, blank=True)
def __str__(self):
return self.title
```
### Agregar al panel de administración
[`core/admin.py`](./app/core/admin.py)
```py
...
admin.site.register(models.Recipe)
```
### Crear migraciones
`docker compose run --rm app sh -c "python manage.py makemigrations"`
```sh
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Migrations for 'core':
core/migrations/0002_recipe.py
- Create model Recipe
```
## Creación de app recipe
`docker compose run --rm app sh -c "python manage.py startapp recipe"`
```sh
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
```
- Se eliminan, directorio `migrations`, `test.py`, `admin.py` `models.py`
- Se crean directorio `reicpes/tests/` y su respecto `__init__.py`
- Añadir app `recipe` en [settings.py](./app/app/settings.py)
```py
INSTALLED_APPS = [
...
'recipe',
]
```
### Tests recipe API
[`recipe/tests/test_recipe_api.py`](./app/recipe/tests/test_recipe_api.py)
```py
...
RECIPES_URL = reverse('recipe:recipe-list')
def create_recipe(user, **params):
"""Create and return a sample recipe."""
defaults = {
'title': 'Titulo reseta de ejemplo',
'time_minutes': 31,
'price': Decimal('5.25'),
'description': 'Descripción de ejmplo',
'link': 'https://defzn.kickto.net/blog',
}
defaults.update(params)
recipe = Recipe.objects.create(user=user, **defaults)
return recipe
class PublicRecipeApiTests(TestCase):
"""Test unauthenticated API requests."""
def setUp(self):
self.client = APIClient()
def test_auth_required(self):
"""Test auth is required to call API."""
res = self.client.get(RECIPES_URL)
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
class PrivateRecipeApiTests(TestCase):
"""Test authenticated API requests."""
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'user@example.com',
'testpass123',
)
self.client.force_authenticate(self.user)
def test_retrive_recipes(self):
"""Test retrieving a list of recipes."""
create_recipe(user=self.user)
create_recipe(user=self.user)
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.all().order_by('-id')
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)
def test_recipe_list_limited_to_user(self):
"""Test list of recipes is limited to authenticated user."""
other_user = get_user_model().objects.create_user(
'other@example.com',
'password123',
)
create_recipe(user=other_user)
create_recipe(user=self.user)
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.filter(user=self.user)
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)
```
### Serializador para Recetas
[`recipe/serializer.py`](./app/recipe/serializers.py)
```py
from rest_framework import serializers
from core.models import Recipe
class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipes."""
class Meta:
model = Recipe
fileds = ['id', 'title', 'time_minutes', 'price', 'link']
read_only_fields = ['id']
```
### Vista Recetas
[`recipe/views.py`](./app/recipe/views.py)
```py
from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from core.models import Recipe
from recipe import serializers
class RecipeViewSet(viewsets.ModelViewSet):
"""View for manage recipe APIs."""
serializer_class = serializers.RecipeSerializer
queryset = Recipe.objects.all()
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Retrieve recipes for authenticated user."""
return self.queryset.filter(user=self.request.user).order_by('-id')
```
### URLs Recetas
[`recipe/urls.py`](./app/recipe/urls.py)
```py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from recipe import views
router = DefaultRouter()
router.register('recipes', views.RecipeViewSet)
app_name = 'recipe'
urlpatterns = [
path('', include(router.urls)),
]
```
[`app/urls.py`](./app/app/urls.py)
```py
...
urlpatterns = [
...
path('api/recipe', include('recipe.urls')),
]
```

View File

@ -38,11 +38,6 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'core', 'core',
'rest_framework',
'rest_framework.authtoken',
'drf_spectacular',
'user',
'recipe',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -112,9 +107,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'es-cl' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Santiago' TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = True
@ -130,9 +125,3 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'core.User'
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

View File

@ -14,17 +14,9 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import path
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
path(
'api/docs',
SpectacularSwaggerView.as_view(url_name='api-schema'),
name='api-docs'),
path('api/user/', include('user.urls')),
path('api/recipe', include('recipe.urls')),
] ]

View File

@ -1,47 +1,3 @@
""" from django.contrib import admin # noqa
Django admin customization.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from core import models # Register your models here.
class UserAdmin(BaseUserAdmin):
"""Define the admin pages for users."""
ordering = ['id']
list_display = ['email', 'name']
fieldsets = (
(None, {'fields': ('email', 'password')}),
(
_('Permissions'),
{
'fields': (
'is_active',
'is_staff',
'is_superuser',
)
}
),
(_('Important dates'), {'fields': ('last_login',)}),
)
readonly_fields = ['last_login']
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email',
'password1',
'password2',
'name',
'is_active',
'is_staff',
'is_superuser',
)
}),
)
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Recipe)

View File

@ -1,33 +0,0 @@
# Generated by Django 4.2.5 on 2023-10-06 15:46
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='User',
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')),
('email', models.EmailField(max_length=255, unique=True)),
('name', models.CharField(max_length=255)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 4.2.5 on 2023-10-09 04:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Recipe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('time_minutes', models.IntegerField()),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=5)),
('link', models.CharField(blank=True, max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,61 +1,3 @@
""" from django.db import models # noqa
Databse models.
"""
from django.conf import settings
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
# Create your models here.
class UserManager(BaseUserManager):
"""Manager for users."""
def create_user(self, email, password=None, **extra_fields):
"""Create, save and return a new user."""
if not email:
raise ValueError('User must have an email address.')
user = self.model(email=self.normalize_email(email), **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password):
"""Create and return a new superuser."""
user = self.create_user(email, password)
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
"""User in the system."""
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
# Asignar el UserManager a esta clase User
objects = UserManager()
USERNAME_FIELD = 'email'
class Recipe(models.Model):
"""Recipe object."""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
time_minutes = models.IntegerField()
price = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
link = models.CharField(max_length=255, blank=True)
def __str__(self):
return self.title

View File

@ -1,46 +0,0 @@
"""
Tests for the Django admin modifications.
"""
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse
class AdminSiteTests(TestCase):
"""Tests for Django admin."""
def setUp(self):
"""Create user and client."""
self.client = Client()
self.admin_user = get_user_model().objects.create_superuser(
email='admin@example.com',
password='testpass123',
)
self.client.force_login(self.admin_user)
self.user = get_user_model().objects.create_user(
email='user@example.com',
password='testpass123',
name='Test User'
)
def test_users_list(self):
"""Test that users are listed on page."""
url = reverse('admin:core_user_changelist')
res = self.client.get(url)
self.assertContains(res, self.user.name)
self.assertContains(res, self.user.email)
def test_edit_user_page(self):
"""Test the edit user page works."""
url = reverse('admin:core_user_change', args=[self.user.id])
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
def test_create_user_page(self):
"""Test the create user page works."""
url = reverse('admin:core_user_add')
res = self.client.get(url)
self.assertEqual(res.status_code, 200)

View File

@ -1,66 +0,0 @@
"""
Test for models.
"""
from decimal import Decimal
from django.test import TestCase
from django.contrib.auth import get_user_model
from core import models
class ModelTests(TestCase):
"""Test models."""
def test_create_user_with_email_sucessfull(self):
"""Test creating a user with an email is sucessfull."""
email = 'test@example.com'
password = 'testpass123'
user = get_user_model().objects.create_user(
email=email,
password=password,
)
self.assertEqual(user.email, email)
self.assertTrue(user.check_password(password))
def test_new_user_email_normalized(self):
"""Test email is normalized for new users."""
sample_emails = [
['test1@EXAMPLE.com', 'test1@example.com'],
['test2@Example.com', 'test2@example.com'],
['TEST3@EXAMPLE.COM', 'TEST3@example.com'],
['test4@example.COM', 'test4@example.com'],
]
for email, expected in sample_emails:
user = get_user_model().objects.create_user(email, 'sample123')
self.assertEqual(user.email, expected)
def test_new_user_withouth_email_raises_error(self):
"""Test that creating a user withouth an email raises a ValueError."""
with self.assertRaises(ValueError):
get_user_model().objects.create_user('', 'test123')
def test_create_superuser(self):
"""Test creating a superuser."""
user = get_user_model().objects.create_superuser(
'test@example.com',
'test123',
)
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
def test_create_recipe(self):
"""Test creating a recipe is successful."""
user = get_user_model().objects.create_user(
'test@example.com',
'test123',
)
recipe = models.Recipe.objects.create(
user=user,
title='Nombre receta ejemplo',
time_minutes=5,
price=Decimal('5.50'),
description='Descripción de la receta de ejemplo'
)
self.assertEqual(str(recipe), recipe.title)

View File

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class RecipeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'recipe'

View File

@ -1,15 +0,0 @@
"""
Serializers for recipe APIs
"""
from rest_framework import serializers
from core.models import Recipe
class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipes."""
class Meta:
model = Recipe
fields = ['id', 'title', 'time_minutes', 'price', 'link']
read_only_fields = ['id']

View File

@ -1,87 +0,0 @@
"""
Test for reicpe APIs.
"""
from decimal import Decimal
from django.contrib.auth import get_user_model
from django.test import TestCase
from django. urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from core.models import Recipe
from recipe.serializers import RecipeSerializer
RECIPES_URL = reverse('recipe:recipe-list')
def create_recipe(user, **params):
"""Create and return a sample recipe."""
defaults = {
'title': 'Titulo reseta de ejemplo',
'time_minutes': 31,
'price': Decimal('5.25'),
'description': 'Descripción de ejmplo',
'link': 'https://defzn.kickto.net/blog',
}
defaults.update(params)
recipe = Recipe.objects.create(user=user, **defaults)
return recipe
class PublicRecipeApiTests(TestCase):
"""Test unauthenticated API requests."""
def setUp(self):
self.client = APIClient()
def test_auth_required(self):
"""Test auth is required to call API."""
res = self.client.get(RECIPES_URL)
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
class PrivateRecipeApiTests(TestCase):
"""Test authenticated API requests."""
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'user@example.com',
'testpass123',
)
self.client.force_authenticate(self.user)
def test_retrive_recipes(self):
"""Test retrieving a list of recipes."""
create_recipe(user=self.user)
create_recipe(user=self.user)
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.all().order_by('-id')
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)
def test_recipe_list_limited_to_user(self):
"""Test list of recipes is limited to authenticated user."""
other_user = get_user_model().objects.create_user(
'other@example.com',
'password123',
)
create_recipe(user=other_user)
create_recipe(user=self.user)
res = self.client.get(RECIPES_URL)
recipes = Recipe.objects.filter(user=self.user)
serializer = RecipeSerializer(recipes, many=True)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data, serializer.data)

View File

@ -1,18 +0,0 @@
"""
URL mappings for the recipe app.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from recipe import views
router = DefaultRouter()
router.register('recipes', views.RecipeViewSet)
app_name = 'recipe'
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -1,21 +0,0 @@
"""
Views for the recipe APIs.
"""
from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from core.models import Recipe
from recipe import serializers
class RecipeViewSet(viewsets.ModelViewSet):
"""View for manage recipe APIs."""
serializer_class = serializers.RecipeSerializer
queryset = Recipe.objects.all()
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Retrieve recipes for authenticated user."""
return self.queryset.filter(user=self.request.user).order_by('-id')

View File

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class UserConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'user'

View File

@ -1,56 +0,0 @@
"""
Seralizers for the user API View
"""
from django.contrib.auth import get_user_model, authenticate
from django.utils.translation import gettext as _ # , trim_whitespace
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
"""Seralizer for the model object."""
class Meta:
model = get_user_model()
fields = ['email', 'password', 'name']
extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
def create(self, validated_data):
"""Create and return a user with encrypted password"""
return get_user_model().objects.create_user(**validated_data)
def update(self, instance, validated_data):
"""Update and return user."""
password = validated_data.pop('password', None)
user = super().update(instance, validated_data)
if password:
user.set_password(password)
user.save()
return user
class AuthTokenSerializer(serializers.Serializer):
"""Serializer for the user auth token."""
email = serializers.EmailField()
password = serializers.CharField(
style={'input_type': 'password'},
trim_whitespace=False,
)
def validate(self, attrs):
"""Validate and authenticate the user."""
email = attrs.get('email')
password = attrs.get('password')
user = authenticate(
request=self.context.get('request'),
username=email,
password=password,
)
if not user:
msg = _('Unable to authenticate with provided credentials.')
raise serializers.ValidationError(msg, code='authorization')
attrs['user'] = user
return attrs

View File

@ -1,153 +0,0 @@
"""
Tests for the user API.
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
CREATE_USER_URL = reverse('user:create')
TOKEN_URL = reverse('user:token')
ME_URL = reverse('user:me')
def create_user(**params):
"""Create and return a new user."""
return get_user_model().objects.create_user(**params)
class PublicUserApiTest(TestCase):
"""Test the public features of the user API."""
def setUp(self):
self.client = APIClient()
def test_create_user_success(self):
"""Tests creating a user is successful."""
payload = {
'email': 'test@example.com',
'password': 'testpass123',
'name': 'TestName',
}
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
user = get_user_model().objects.get(email=payload['email'])
self.assertTrue(user.check_password(payload['password']))
self.assertNotIn('password', res.data)
def test_user_with_email_exists_error(self):
"""Test error returned if user with email exists."""
payload = {
'email': 'test@example.com',
'password': 'testpass123',
'name': 'Test Name',
}
create_user(**payload)
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_password_too_short_error(self):
"""Test an error is returned if password less than 5 chars."""
payload = {
'email': 'test@example.com',
'password': 'pw',
'name': 'Test Name',
}
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
user_exists = get_user_model().objects.filter(
email=payload['email']
).exists()
self.assertFalse(user_exists)
def test_create_token_for_user(self):
"""Test generate token for valid credentials."""
user_details = {
'name': 'Test Name',
'email': 'test@example.com',
'password': 'test-user-password123',
}
create_user(**user_details)
payload = {
'email': user_details['email'],
'password': user_details['password'],
}
res = self.client.post(TOKEN_URL, payload)
self.assertIn('token', res.data)
self.assertEqual(res.status_code, status.HTTP_200_OK)
def test_create_token_bad_credentials(self):
"""Test returns error if credentials invalid."""
create_user(email='test@example.com', password='goodpass')
payload = {'email': 'test@example.com', 'password': 'badpass'}
res = self.client.post(TOKEN_URL, payload)
self.assertNotIn('token', res.data)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_token_blank_password(self):
"""Test posting a blank password returns an error."""
payload = {'email': 'test@example.com', 'password': ''}
res = self.client.post(TOKEN_URL, payload)
self.assertNotIn('token', res.data)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_retrive_user_unauthorized(self):
"""Test authentication is required for users."""
res = self.client.get(ME_URL)
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
class PrivateUserApiTests(TestCase):
"""Test API requests that require authentication."""
def setUp(self):
self.user = create_user(
email='test@example.com',
password='testpass123',
name='Test Name',
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_retrive_profile_success(self):
"""Test retrieving profile for logged in user."""
res = self.client.get(ME_URL)
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(
res.data, {
'name': self.user.name,
'email': self.user.email,
}
)
def test_post_me_not_allowed(self):
"""Test POST is not allowed for the 'me' endpoint."""
res = self.client.post(ME_URL, {})
self.assertAlmostEqual(
res.status_code,
status.HTTP_405_METHOD_NOT_ALLOWED
)
def test_update_user_profile(self):
"""Test updating the user profile for the autenticated user."""
payload = {'name': 'Updated Name', 'password': 'newpassword123'}
res = self.client.patch(ME_URL, payload)
self.user.refresh_from_db()
self.assertEqual(self.user.name, payload['name'])
self.assertTrue(self.user.check_password(payload['password']))
self.assertEqual(res.status_code, status.HTTP_200_OK)

View File

@ -1,15 +0,0 @@
"""
URL mappings for the user API
"""
from django.urls import path
from user import views
app_name = 'user'
urlpatterns = [
path('create/', views.CreateUserView.as_view(), name='create'),
path('token/', views.CreateTokenView.as_view(), name='token'),
path('me/', views.ManageUserView.as_view(), name='me'),
]

View File

@ -1,30 +0,0 @@
"""
Views for the user API.
"""
from rest_framework import generics, authentication, permissions
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.settings import api_settings
from user.serializers import UserSerializer, AuthTokenSerializer
class CreateUserView(generics.CreateAPIView):
"""Create a new user in the system."""
serializer_class = UserSerializer
class CreateTokenView(ObtainAuthToken):
"""Create a new auth token for user."""
serializer_class = AuthTokenSerializer
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
class ManageUserView(generics.RetrieveUpdateAPIView):
"""Manage the autenticated user."""
serializer_class = UserSerializer
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get_object(self):
"""Retrieve and return the authenticated user."""
return self.request.user

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,4 +1,3 @@
Django==4.2.5 Django==4.2.5
djangorestframework==3.14.0 djangorestframework==3.14.0
psycopg2>=2.9.9 psycopg2>=2.9.9
drf-spectacular>=0.16