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:
parent
fa10284b72
commit
e3b750bd83
35
README.md
35
README.md
@ -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`.
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
59
backend/ecommerce/migrations/0001_initial.py
Normal file
59
backend/ecommerce/migrations/0001_initial.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -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}'
|
||||||
|
44
backend/ecommerce/serializers.py
Normal file
44
backend/ecommerce/serializers.py
Normal 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
|
Loading…
Reference in New Issue
Block a user