diff --git a/README.md b/README.md index 2d7fcf0..c7dcb2e 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,21 @@ pip install djangorestframework ```py pip install django-extensions djangorestframework djangorestframework-jsonapi \ inflection python-dotenv sqlparse +``` Django utiliza SQLite3 por defecto facilitar el desarrolo, en este proyecto se utliza MariaDB, pero es opcional. -# MariaDB +```py pip install mysqlclient ``` ### Inicio del proyecto +**Creación del proyecto Django** + ```sh mkdir backend - -# Creación del proyecto Django django-admin startproject drf_course backend ``` @@ -58,7 +59,7 @@ python manage.py startapp core Archivo [./backend/drf_course/settings.py](./backend/drf_course/settings.py). -Importar `.env` usando *python-dotenv*. +Importar variables de entorno usando *python-dotenv* del archivo en `.backend/.env`. ```py from dotenv import load_dotenv @@ -120,7 +121,7 @@ REST_FRAMEWORK = { ``` En caso de utilizar MariaDB, cambiar la declaración de *DATABASES*, para usar -las variables de entorno declaradas en [./backend/env](./backend/.env) +las variables de entorno declaradas en `./backend/.env` ```py DATABASES = { @@ -151,3 +152,111 @@ urlpatterns += [ ] ``` +#### Migrar y probar aplicación + +```sh +python manage.py migrate +python manage.py runserver +``` + +### Creación del primer endoint + +Creación de endpoint de ***contacto***, para que un usuario pueda enviar su +*nombre*, *email*, y *mensaje* al backend. + +Para ello se requiere: + +- Un modelo que almacene la captura de los datos entrantes. +- Un serializador que procese los datos entrantes del usuario y envíe un +mensaje de respuesta. +- Una vista que encapsule las llamadas a los métodos REST HTTP comunes. +- Una ruta `/url` llamada `/contact/`. + + +#### Model + +[./backend/core/models.py](./backend/core/models.py) + +Utiliza modelos abstractos del modulo +*django_extensions*; `TimeStampedModel` (campos como *created*), `ActivatorModel` +(campos *status*, *activated date*, *deactivated date* ), `TitleDescriptionModel` +(campos de texto *textfield* y *charfield*). +Clase **Contact** hereda de estos modelos. Todas las tablas del proyecto tendrán +un campo *uuid* como campo id. Además de un campo *email* de Django. Y método de +representación del modelo en cadena de texto. + +**Modelo abstracto** + +Para implementar *uuid* en vez de *id* como campo identificador en todos los +modelos, se crea el modulo [model_abstracts.py](./backend/utils/model_abstracts.py) +que hereda de *models* de *django.db* y utliza el campo *id*. Utiliza el campo *UUID*. + +#### 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) +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) + +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 +varios aspectos de las normas de la API. + +#### Route & URL + +[./drf_course/urls.py](./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 +de la vista a un conjunto de URLs. + +#### Registrar app en panel de administración + +Importar modelo y registrar en [./backend/core/admin.py](./backend/core/admin.py). + +Crear las migraciones y migrar. + +```py +python manage.py makemigrations +python manage.py migrate +``` +Finalmente, crear **super usuario**. + +```py +python manage.py createsuperuser +``` + +#### Probar API + +```sh +http http://127.0.0.1:8000/contact/ name="DevFzn" message="prueba" email="devfzn@mail.com" + +HTTP/1.1 200 OK +Allow: POST, OPTIONS +Content-Length: 155 +Content-Type: application/vnd.api+json +Cross-Origin-Opener-Policy: same-origin +Date: Wed, 29 Mar 2023 20:05:32 GMT +Referrer-Policy: same-origin +Server: WSGIServer/0.2 CPython/3.10.10 +Vary: Accept, Cookie +X-Content-Type-Options: nosniff +X-Frame-Options: DENY + +{ + "data": { + "attributes": { + "email": "devfzn@mail.com", + "message": "prueba", + "name": "DevFzn" + }, + "id": "bef5e90c-821a-4d04-98ef-c0a0adde5ec1", + "type": "ContactAPIView" + } +} +``` diff --git a/backend/core/admin.py b/backend/core/admin.py index 8c38f3f..ecd233d 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from .models import Contact -# Register your models here. +@admin.register(Contact) +class ContactAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'description', 'email') diff --git a/backend/core/migrations/0001_initial.py b/backend/core/migrations/0001_initial.py new file mode 100644 index 0000000..e9b7bab --- /dev/null +++ b/backend/core/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.7 on 2023-03-29 19:55 + +from django.db import migrations, models +import django_extensions.db.fields +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Contact', + 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')), + ('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)), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ], + options={ + 'verbose_name_plural': 'Contacts', + }, + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 71a8362..05d21de 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1,3 +1,17 @@ from django.db import models +from utils.model_abstracts import Model +from django_extensions.db.models import ( + TimeStampedModel, + ActivatorModel, + TitleDescriptionModel + ) -# Create your models here. +class Contact(TimeStampedModel, ActivatorModel, TitleDescriptionModel, Model): + + class Meta: + verbose_name_plural = "Contacts" + + email = models.EmailField(verbose_name="Email") + + def __str__(self): + return f'{self.title}' diff --git a/backend/core/serializers.py b/backend/core/serializers.py new file mode 100644 index 0000000..fa36c53 --- /dev/null +++ b/backend/core/serializers.py @@ -0,0 +1,17 @@ +from . import models +from rest_framework import serializers +from rest_framework.fields import CharField, EmailField + +class ContactSerializer(serializers.ModelSerializer): + + name = CharField(source="title", required=True) + message = CharField(source="description", required=True) + email = EmailField(required=True) + + class Meta: + model = models.Contact + fields = ( + 'name', + 'email', + 'message' + ) diff --git a/backend/core/views.py b/backend/core/views.py index 91ea44a..afe1ef1 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1,3 +1,34 @@ -from django.shortcuts import render +from json import JSONDecodeError +from django.http import JsonResponse +from .serializers import ContactSerializer +from rest_framework.parsers import JSONParser +from rest_framework import views, status +from rest_framework.response import Response -# Create your views here. +class ContactAPIView(views.APIView): + """ + Simple APIView para creación de entradas de contacto. + """ + serializer_class = ContactSerializer + + def get_serializer_context(self): + return { 'request': self.request, + 'format': self.format_kwarg, + 'view': self } + + def get_serializer(self, *args, **kwargs): + kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) + + def post(self, request): + try: + data = JSONParser().parse(request) + serializer = ContactSerializer(data=data) + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response(serializer.data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except JSONDecodeError: + return JsonResponse({"resutl": "error", "message": "Json decoding error"}, + status=400) diff --git a/backend/drf_course/urls.py b/backend/drf_course/urls.py index 7e3fd92..f82f5ea 100644 --- a/backend/drf_course/urls.py +++ b/backend/drf_course/urls.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.urls import path from rest_framework import routers +from core import views as core_views router = routers.DefaultRouter() @@ -8,4 +9,5 @@ urlpatterns = router.urls urlpatterns += [ path('admin/', admin.site.urls), + path('contact/', core_views.ContactAPIView.as_view()), ] diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/model_abstracts.py b/backend/utils/model_abstracts.py new file mode 100644 index 0000000..44d14b2 --- /dev/null +++ b/backend/utils/model_abstracts.py @@ -0,0 +1,8 @@ +import uuid +from django.db import models + +class Model(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + + class Meta: + abstract = True diff --git a/backend/requirements.txt b/requirements.txt similarity index 93% rename from backend/requirements.txt rename to requirements.txt index eed58e7..6c46748 100644 --- a/backend/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ djangorestframework==3.14.0 djangorestframework-jsonapi==6.0.0 inflection==0.5.1 Markdown==3.4.3 +mysqlclient==2.1.1 Pygments==2.14.0 python-dotenv==1.0.0 pytz==2023.2