diff --git a/README.md b/README.md index 736b72a..53f643c 100644 --- a/README.md +++ b/README.md @@ -194,14 +194,14 @@ que hereda de *models* de *django.db* y utliza el campo *id*. Utiliza el campo * ### Serializer Para convertir los datos de entrada *json* en tipos de datos de python, y -viceversa, se crea el archivo [./backend/core/serializer.py](./backend/core/serializer.py) +viceversa, se crea el archivo [./backend/core/serializers.py](./backend/core/serializers.py) en la app *core*. Este hereda de la clase *serializers* del modulo *rest_framework* e implementa sus campos (*CharField* *EmailField*). ### View [./backend/core/views.py](./backend/core/views.py) - +backend El uso de la clase *APIView* es muy similar al una vista regular, la petición entrante es enviada a un manejador apropiado para el método, como `.get()` o `.post()`. Además se pueden establecer otros atributos en la clase que controla @@ -209,7 +209,7 @@ varios aspectos de las normas de la API. ### Route & URL -[./drf_course/urls.py](./drf_course/urls.py) +[./backend/drf_course/urls.py](./backend/drf_course/urls.py) El framework REST añade soporte para ruteo automático de URLs a Django y provee al programador de una simple, rápida y consistente forma de enlazar la lógica @@ -366,7 +366,7 @@ Creación de app *ecommerce* ./manage.py startapp ecommerce ``` -Modificar [settings](./drf_course/settings.py) del sitio, reemplazando el +Modificar [settings](./backend/drf_course/settings.py) del sitio, editando `REST_FRAMEWORK` con el siguiente código. > notar el nuevo `DEFAULT_AUTHENTICATION_CLASSES`. @@ -463,7 +463,8 @@ cree un nuevo token para este. ./manage.py createsuperuser ``` -Visitar [http:127.0.0.8/admin](http:127.0.0.8/admin), y verificar creación del token. +Visitar [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin), y verificar +creación del token. Probar retorno del token a traves de la API. @@ -471,13 +472,13 @@ ej. **curl** ```sh curl -XPOST -F 'username=' -F 'password=' \ - 'http://192.168.0.9:8000/api-token-auth/' + 'http://127.0.0.1:8000/api-token-auth/' ``` ej. **HTTPie** ```sh -http post http://192.168.0.9:8000/api-token-auth/ username= \ +http post http://127.0.0.1:8000/api-token-auth/ username= \ password= ``` @@ -497,3 +498,23 @@ X-Frame-Options: DENY "token": "2f076a6310a244283c6902a73e07a0febc59649c" } ``` + +## Ecommerce Model + +Esta app hace uso obligatorio del token de autentificación. Solo usuarios +autentificados pueden acceder a este endpoint. + +La app ecommerce se construye con un endpoint **item** y otro **order**. Los +usuarios podrán recuperar elementos de la base de datos, hacer un pedido y +recuperar la información del pedido. + +Se necesitan modelos, enrutadores, serializadores y vistas. (models, routers, +serializers & view/sets api/view). + +Creación de [modelos](./backend/ecommerce/models.py) Item y Order. Creación +de [serializers](./backend/ecommerce/serializers.py). + +Registro de app en el panel de [administración](./backend/ecommerce/admin.py). + +Migraciones `./manage.py makemigrations` y `./manage.py migrate`. + diff --git a/backend/ecommerce/admin.py b/backend/ecommerce/admin.py index 8c38f3f..7640576 100644 --- a/backend/ecommerce/admin.py +++ b/backend/ecommerce/admin.py @@ -1,3 +1,12 @@ from django.contrib import admin +from . import models -# Register your models here. + +@admin.register(models.Item) +class ItemAdmin(admin.ModelAdmin): + list_display = ('id', 'title') + + +@admin.register(models.Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'item') diff --git a/backend/ecommerce/migrations/0001_initial.py b/backend/ecommerce/migrations/0001_initial.py new file mode 100644 index 0000000..42dc91f --- /dev/null +++ b/backend/ecommerce/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 4.1.7 on 2023-03-30 18:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, populate_from='title', verbose_name='slug')), + ('status', models.IntegerField(choices=[(0, 'Inactive'), (1, 'Active')], default=1, verbose_name='status')), + ('activate_date', models.DateTimeField(blank=True, help_text='keep empty for an immediate activation', null=True)), + ('deactivate_date', models.DateTimeField(blank=True, help_text='keep empty for indefinite activation', null=True)), + ('stock', models.IntegerField(default=1)), + ('price', models.IntegerField(default=0)), + ], + options={ + 'verbose_name': 'Item', + 'verbose_name_plural': 'Items', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('status', models.IntegerField(choices=[(0, 'Inactive'), (1, 'Active')], default=1, verbose_name='status')), + ('activate_date', models.DateTimeField(blank=True, help_text='keep empty for an immediate activation', null=True)), + ('deactivate_date', models.DateTimeField(blank=True, help_text='keep empty for indefinite activation', null=True)), + ('quantity', models.IntegerField(default=0)), + ('item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='ecommerce.item')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Order', + 'verbose_name_plural': 'Orders', + 'ordering': ['id'], + }, + ), + ] diff --git a/backend/ecommerce/models.py b/backend/ecommerce/models.py index 71a8362..06d7af5 100644 --- a/backend/ecommerce/models.py +++ b/backend/ecommerce/models.py @@ -1,3 +1,85 @@ from django.db import models +from django.contrib.auth.models import User +from utils.model_abstracts import Model +from django_extensions.db.models import ( + TimeStampedModel, + ActivatorModel, + TitleSlugDescriptionModel +) -# Create your models here. + +class Item(TimeStampedModel, ActivatorModel, TitleSlugDescriptionModel, Model): + """ + ecommerce.Item + Almacena una entrada de item para la tienda + """ + class Meta: + verbose_name = 'Item' + verbose_name_plural = 'Items' + ordering = ["id"] + + def __str__(self): + return self.title + + stock = models.IntegerField(default=1) + price = models.IntegerField(default=0) + + def amount(self): + """ + Converte el precio de centavos a libras + """ + amount = float(self.price / 100) + return amount + + def manage_stock(self, qty): + """ + Reduce stock según `qty` + """ + new_stock = self.stock - int(qty) + self.stock = new_stock + self.save() + + def check_stock(self, qty): + """ + Comprueba si la cantidad en order excede el stock + """ + if int(qty) > self.stock: + return False + return True + + def place_order(self, user, qty): + """ + Realiza un order (pedido) + """ + if self.check_stock(qty): + order = Order.objects.create( + item = self, + quantity = qty, + user= user) + self.manage_stock(qty) + return order + else: + return None + + +class Order(TimeStampedModel, ActivatorModel, Model): + """ + ecommerce.Order + Almacena una entrada de order, relacionada con :model:`ecommerce.Item` y + :model:`auth.User`. + """ + class Meta: + verbose_name = 'Order' + verbose_name_plural = 'Orders' + ordering = ["id"] + + user = models.ForeignKey( User, on_delete=models.CASCADE, + null=True, blank=True ) + item = models.ForeignKey( Item, + null=True, + blank=True, + on_delete=models.CASCADE ) + quantity = models.IntegerField(default=0) + + def __str__(self): + return f'{self.user.username} - {self.item.title}' diff --git a/backend/ecommerce/serializers.py b/backend/ecommerce/serializers.py new file mode 100644 index 0000000..b36a8f9 --- /dev/null +++ b/backend/ecommerce/serializers.py @@ -0,0 +1,44 @@ +from collections import OrderedDict +from .models import Item, Order +from rest_framework_json_api import serializers +from rest_framework import status +from rest_framework.exceptions import APIException + + +class NotEnoughStockException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = 'Sin stock suficiente' + default_code = 'invalido' + + +class ItemSerializer(serializers.ModelSerializer): + + class Meta: + model = Item + fields = ( + 'title', + 'stock', + 'price', + ) + + +class OrderSerializer(serializers.ModelSerializer): + + item = serializers.PrimaryKeyRelatedField(queryset = Item.objects.all(), many=False) + + class Meta: + model = Order + fields = ( + 'item', + 'quantity', + ) + + def validate(self, res: OrderedDict): + ''' + Utilizado para validar niveles de stock del Item + ''' + item = res.get("item") + quantity = res.get("quantity") + if not item.check_stock(quantity): + raise NotEnoughStockException + return res