Creación modelos Item y Order para app Ecommerce

Clase Item con atributos stock y precio; metodos amount,
manage_stock, check_stock, place_order.
Clase Order con atributos user, item, quantity.
Registro en panel de administración.
Serializadores de Item y Order + excepción APIException del framework.
This commit is contained in:
devfzn 2023-03-30 15:24:14 -03:00
parent fa10284b72
commit e3b750bd83
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
5 changed files with 224 additions and 9 deletions

View File

@ -194,14 +194,14 @@ que hereda de *models* de *django.db* y utliza el campo *id*. Utiliza el campo *
### Serializer ### Serializer
Para convertir los datos de entrada *json* en tipos de datos de python, y 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* en la app *core*. Este hereda de la clase *serializers* del modulo *rest_framework*
e implementa sus campos (*CharField* *EmailField*). e implementa sus campos (*CharField* *EmailField*).
### View ### View
[./backend/core/views.py](./backend/core/views.py) [./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 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 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 `.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 ### 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 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 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 ./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. `REST_FRAMEWORK` con el siguiente código.
> notar el nuevo `DEFAULT_AUTHENTICATION_CLASSES`. > notar el nuevo `DEFAULT_AUTHENTICATION_CLASSES`.
@ -463,7 +463,8 @@ cree un nuevo token para este.
./manage.py createsuperuser ./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. Probar retorno del token a traves de la API.
@ -471,13 +472,13 @@ ej. **curl**
```sh ```sh
curl -XPOST -F 'username=<tu-usuario>' -F 'password=<tu-password>' \ curl -XPOST -F 'username=<tu-usuario>' -F 'password=<tu-password>' \
'http://192.168.0.9:8000/api-token-auth/' 'http://127.0.0.1:8000/api-token-auth/'
``` ```
ej. **HTTPie** ej. **HTTPie**
```sh ```sh
http post http://192.168.0.9:8000/api-token-auth/ username=<tu-usuario> \ http post http://127.0.0.1:8000/api-token-auth/ username=<tu-usuario> \
password=<tu-password> password=<tu-password>
``` ```
@ -497,3 +498,23 @@ X-Frame-Options: DENY
"token": "2f076a6310a244283c6902a73e07a0febc59649c" "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`.

View File

@ -1,3 +1,12 @@
from django.contrib import admin 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')

View File

@ -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'],
},
),
]

View File

@ -1,3 +1,85 @@
from django.db import models 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}'

View File

@ -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