5. Message Board app
- Aplicación en la que los usuarios pueden publicar y leer mensajes cortos con la ayuda de una base de datos.
- Se explorará la interfaz de administración incorporada de Django
- Se agregarán pruebas
- Se subirá a github y se desplegará en Heroku
- Django proporciona soporte incorporado para varios tipos de backends de bases de datos
- Se empezará con SQLite
- Se ejecuta a partir de un único archivo
- No requiere una instalación compleja
- Es una elección perfecta para proyectos pequeños.
5.1. Setup Inicial
- Crear un nuevo directorio para el código llamado
mb
- Instalar Django en un nuevo entorno virtual
- Crear un nuevo proyecto llamado
mb_project
- Crear una nueva aplicación
posts
- Actualizar
settings.py
$ mkdir mb
$ cd mb
$ poetry init
$ poetry add django
$ poetry shell
(mb) $ django-admin startproject mb_project .
(mb) $ python manage.py startapp posts
FICHERO: mb_project/settings.py
# mb_project/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'posts.apps.PostsConfig', # new
]
- A continuación, ejecutar el comando
migrate
para crear una base de datos inicial basada en la configuración por defecto de Django.
(mb) $ python manage.py migrate
- Ahora en el directorio habrá un fichero
db.sqlite3
que representa a la base de datos SQLite.
(mb) $ ls
db.sqlite3 mb_project manage.py
Nota.- Técnicamente se crea un fichero
db.sqlite3
la primera vez que se ejecuta una migración (migrate
) o se ejecuta el servidor (runserver
). El uso derunserver
configura una base de datos utilizando la configuración predeterminada de Django, sin embargo, la migración sincronizará la base de datos con el estado actual de cualquier modelo de base de datos contenido en el proyecto y listado enINSTALLED_APPS
. En otras palabras, para asegurar que la base de datos refleja el estado actual del proyecto se tendrá que ejecutarmigrate
(y tambiénmakemigrations
) cada vez que se actualiza un modelo. Más en breve.
- Lanzar el servidor local y comprobar el funcionamiento
(mb) $ python manage.py runserver
5.2. Crear un modelo de base de datos
- Crear un modelo de base de datos donde se pueda almacenar y mostrar los mensajes de los usuarios.
- Django convertirá este modelo en una tabla de base de datos.
FICHERO: posts/models.py
from django.db import models
-
Django importa un módulo
models
para ayudar a construir nuevos modelos de bases de datos, que "modelan" las características de los datos de la base de datos. -
Se quiere crear un modelo para almacenar el contenido textual de un mensaje en el tablero de mensajes, lo cual se puede hacer de la siguiente manera:
FICHERO: post/models.py
from django.db import models
class Post(models.Model): # new
text = models.TextField() # new
- Se ha creado un nuevo modelo de base de datos llamado
Post
que tiene el campotext
de tipoTextField()
.
5.3. Activando modelos
- Una vez creado, el modelo tiene que ser activado
- Primero se crea un archivo de migración con el comando
makemigrations
que genera los comandos SQL para las aplicaciones preinstaladas en nuestra configuración deINSTALLED_APPS
. Los archivos de migración no ejecutan esos comandos en el archivo de base de datos, sino que son una referencia de todos los cambios en los modelos. Este enfoque significa que tienen un registro de los cambios de los modelos a lo largo del tiempo. - En segundo lugar, construimos la base de datos actual con
migrate
que ejecuta la función en el archivo de migraciones.
(mb) $ python manage.py makemigrations posts
(mb) $ python manage.py migrate posts
-
No es necesario incluir un nombre después de
makemigrations
o demigrate
pero es un buen hábito para ser específico.- Si tenemos dos apps separadas en nuestro proyecto, se actualizan los modelos en ambos y luego se ejecuta
makemigrations
se genererá un archivo de migraciones que contiene datos sobre ambas modificaciones. Esto hace que la depuración sea más difícil en el futuro. Es deseable que cada archivo de migración sea lo más pequeño y aislado posible. De esta forma, si se necesita mirar las migraciones pasadas, sólo hay un cambio por migración en lugar de uno que se aplica a múltiples aplicaciones.
- Si tenemos dos apps separadas en nuestro proyecto, se actualizan los modelos en ambos y luego se ejecuta
5.4. Django Admin
- Django proporciona una robusta interfaz de administración para interactuar con la base de datos (pocos frameworks ofrecen tal cosa).
- Originado como proyecto en un periódico, los desarrolladores querían un CMS para que los periodistas pudieran escribir y editar sus historias sin tocar "código".
- Para utilizar el administrador de Django, primero necesitamos crear un superusuario que pueda iniciar sesión.
(mb) $ python manage.py createsuperuser
Username (leave blank to use 'mentecato'): mentecato
Email:
Password:
Password (again):
Superuser created successfully.
-
Reiniciar el servidor Django con
python manage.py
y en el navegador ir a http://127.0.0.1:8000/admin/. -
Necesitamos decirle explícitamente a Django qué mostrar en la página de administración.
FICHERO: post/admin.py
from django.contrib import admin
from posts.models import Post # new
admin.site.register(Post) # new
- Ahora crear el primer mensaje en el tablero de mensajes.
- Problema: La nueva entrada se llama “Post object”, lo cual no es muy útil
- Cambiamos eso añadiendo una nueva función
__str__
como sigue:
- Cambiamos eso añadiendo una nueva función
FICHERO: posts/models.py
# posts/models.py
from django.db import models
class Post(models.Model):
text = models.TextField()
def __str__(self): # new
return self.text[:50] # new
- El cometido del método
__str__
es establecer el nombre que recibirá el post. Con el código anterior lo hemos redefinido (overriden) para que su nombre se establezca como los primeros 50 caracteres del texto del propio post. - Es una buena práctica añadir métodos
__str__()
a todos los modelos para aumentar la legibilidad.
5.5. Views/Templates/URLs
- Para poder mostrar el contenido de la base de datos en la web, hay que conectar las vistas, las plantillas y las URLConfs.
5.5.1. Vista
- Django viene equipado con el
ListView
genérico basado en clases.
FICHERO: posts/views.py
# posts/views.py
from django.views.generic import ListView # new
from .models import Post # new
class HomePageView(ListView): # new
model = Post # new
template_name = 'home.html' # new
- Se importa de la colección de vistas genéricas que tiene Django la clase
ListView
para personalizarla - Se define qué modelo se va a usar
- En la vista, se deriva la clase
ListView
para especificar el nombre del modelo y la referencia a la plantilla.- Internamente
ListView
devuelve una variable de contexto llamadaobject_list
que hay que mostrar en la plantilla.
- Internamente
5.4.2. Plantilla
- Crear un directorio en el nivel del proyecto que se llame
templates
y una plantillahome.html
en él
(mb) $ mkdir templates
(mb) $ touch templates/home.html
- Actualizar el campo
DIRS
del archivosettings.py
para que Django sepa cómo acceder a la carpetatemplates
.
# settings.py
TEMPLATES = [
{
...
'DIRS': [BASE_DIR / 'templates'] # new
...
},
]
- En el archivo
home.html
se puede usar buclefor
del lenguaje de plantillas de Django para listar todos los objetos enobject_list
.
FICHERO: templates/home.html
<!-- templates/home.html --> # new
<h1>Página de Inicio del Tablón de Anuncios</h1> # new
<ul> # new
{% for post in object_list %} # new
<li>{{ post.text }}</li> # new
{% endfor %} # new
</ul> # new
- El nombre
object_list
no es muy apropiado así que pondremos uno más explícito a través del atributocontext_object_name
.
FICHERO: posts/views.py
from django.views.generic import ListView
from .models import Post
class HomePageView(ListView):
model = Post
template_name = 'home.html'
context_object_name = 'all_posts_list' # new
- ...que cambiaremos en:
FICHERO: templates/home.html
...
{% for post in all_posts_list %} # new
...
- Modificamos las urls del proyecto en:
FICHERO: mb_project/urls.py
# mb_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('posts.urls')), # new
]
- Crearemos el fichero
urls.py
a nivel de app
(mb) $ touch posts/urls.py
- ...con el siguiente contenido:
FICHERO: posts/urls.py
from django.urls import path # new
from .views import HomePageView # new
urlpatterns = [ # new
path('', HomePageView.as_view(), name='home'), # new
] # new
- Reiniciar el servidor que ahora mostrará los post del tablón de mensajes
- Añádanse más posts ;-)
- No olvidar
(mb) $ git init
(mb) $ git add -A
(mb) $ git commit -m 'Crea Commit inicial'
5.6. Tests
- Se necesita usar
TestCase
en lugar deSimpleTestCase
dado que ahora tenemos una base de datos y no solo una página estática. - Se creará una base de datos con la que se pueden hacer pruebas (tal y como se haría en una base de datos real).
- Se empezará añadiendo un mensaje de muestra al campo de la base de datos de texto para luego comprobar que se almacena correctamente.
- Es importante que todos los métodos de prueba comiencen con
test_
para que Django sepa cómo manejarlos
FICHERO: posts/test.py
# posts/tests.py
from django.test import TestCase
from .models import Post
class PostModelTest(TestCase):
def setUp(self):
Post.objects.create(text='just a test')
def test_text_content(self):
post = Post.objects.get(id=1)
expected_object_name = f'{post.text}'
self.assertEqual(expected_object_name, 'just a test')
- Importa el módulo
TestCase
que permite crear una base de datos de muestra - Importa el modelo
Post
-
Se crea una nueva clase
PostModelTest
y se le añade un métodosetUp
para crear una nueva base de datos con una sola entrada:'just a test'
-
Se ejecuta
test_text_content
para comprobar que el campo de la base de datos realmente contienejust a test
.- Se crea una variable llamada
post
que representa el primerid
del modelo Post. - Django asigna automáticamente esta identificación
- Se crea una variable llamada
- La siguiente línea usa "cadenas f", que son una adición muy interesante desde Python 3.6, y permiten poner variables directamente en las cadenas siempre y cuando estén rodeadas de corchetes
{}
- Se establece
expected_object_name
como el valor de la cadena enpost.text
que permitirá hacer la prueba - En la última línea se usa
assertEqual
para comprobar que la entrada recién creada coincide con la dispuesta al principio - Ejecutar la prueba con
python manage.py test
(mb) $ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
---------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
- A pesar de lo aparentemente complicado del asunto pronto se verá que en la mayor parte de los casos, los tests son repetitivos.
- El segundo test comprueba una sola página: la homepage. En concreto comprueba que exista (lanza una respuesta HTTP 200). Usa la vista
home
y la plantillahome.html
. - Se necesita añadir un
import
más parareverse
y una nueva claseHomePageViewTest
from django.test import TestCase
from django.urls import reverse #new
from .models import Post
class PostModelTest(TestCase):
def setUp(self):
Post.objects.create(text='just a test')
def test_text_content(self):
post=Post.objects.get(id=1)
expected_object_name = f'{post.text}'
self.assertEqual(expected_object_name, 'just a test')
class HomePageViewTest(TestCase): # new
def setUp(self): # new
Post.objects.create(text='this is another test')# new
def test_view_url_exists_at_proper_location(self): # new
resp = self.client.get('/') # new
self.assertEqual(resp.status_code, 200) # new
def test_view_url_by_name(self): # new
resp = self.client.get(reverse('home')) # new
self.assertEqual(resp.status_code, 200) # new
def test_view_uses_correct_template(self): # new
resp = self.client.get(reverse('home')) # new
self.assertEqual(resp.status_code, 200) # new
self.assertTemplateUsed(resp, 'home.html') # new
- Ejecutando el test:
(mb) $ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 4 tests in 0.036s
OK
Destroying test database for alias 'default'...
- 4 test:
test_text_content
,test_view_url_exists_at_proper_location
,test_view_url_by_name
ytest_view_uses_correct_template
. -
Cualquier función que tenga la palabra
test*
al principio y exista en un ficherotests.py
se lanzará cuando se ejecuta el comandopython manage.py test
. -
Hora de hacer commit de los cambios.
(mb) $ git add -A
(mb) $ git commit -m 'added tests'
5.7. GitHub
- Subir el proyecto...
5.8. Heroku configuration
- Hay que hacer los siguientes cambios al projecto para desplegarlo online:
- Actualizar
Pipfile.lock
- Crear
Procfile
- Instalar
gunicorn
- Actualizar
settings.py
- Actualizar
5.8.1. Actualizar Pipfile.lock
- Especificar la versión de python que se está usando
FICHERO:
Pipfile
# Pipfile
[requires]
python_version = "3.10"
Ejecutar pipenv lock
para generar el Pipfile.lock
adecuado.
(mb) $ pipenv lock
5.8.2. Crear Procfile
- Le dirá a Heroku cómo ejecutar el servidor remoto donde habita el código.
(mb) $ touch Procfile
al que le añadiremos el siguiente contenido
FICHERO: Procfile
web: gunicorn mb_project.wsgi --log-file -
5.8.3. Instalar gunicorn
-
Por ahora Heroku usa
gunicorn
como servidor de producción y mira en el ficheromb_project.wsgi
para más instrucciones. -
Luego, se instala
gunicorn
, que se usará en producción mientras se siga usando el servidor interno de Django para desarrollo local.
(mb) $ pipenv install gunicorn
5.8.4. Actualizar settings.py
- Anteriormente, establecíamos
ALLOWED_HOSTS
en*
, lo que significaba aceptar todos los hosts; esto es una atajo peligroso. Podemos, y debemos, ser más específicos. Los dos hosts locales en los que se ejecuta Django son:localhost:8000
y127.0.0.1:8000
. También sabemos que, una vez desplegado, cualquier sitio Heroku terminará con.herokuapp.com
. Podemos añadir las tres rutas a nuestra configuración.
FICHERO: mb_project/settings.py
# mb_project/settings.py
ALLOWED_HOSTS = ['.herokuapp.com', 'localhost', '127.0.0.1']
- Ahora
commit
ypush
(mb) $ git status
(mb) $ git add -A
(mb) $ git commit -m 'Actualiza el despliegue en Heroku'
(mb) $ git push -u origin master
5.9. Despliegue en Heroku
- Login
(mb) $ heroku login
- Create.- Genera un nombre aleatorio para la aplicación
(mb) $ heroku create
Creating app... done,agile-inlet-25811
https://agile-inlet-25811.herokuapp.com/ | https://git.heroku.com/agile-inlet-25811.git
- Establecer a git para usar el nombre de la nueva aplicación cuando se suba el código a Heroku.
(mb) $ heroku git:remote -a agile-inlet-25811
- Indicar a que ignore los archivos estáticos (se tratará más adelante).
(mb) $ heroku config:set DISABLE_COLLECTSTATIC=1
- Subir el código a Heroku y añadir escalado gratuito para que se ejecute realmente en línea, de lo contrario el código sólo se quedará alojado.
(mb) $ git push heroku main (ó master)
(mb) $ heroku ps:scale web=1
- Abrir el código con
heroku open
y automáticamente mostrará un navegador con la URL de la aplicación.
5.10. Conclusión
- Se ha construido, probado e implementado la primera aplicación básica basada en una base de datos.
- Faltarían formularios para interactuar con el sitio (el panel de administración no es lo adecuado).
- Se creará una aplicación de blog con formularios para que los usuarios puedan crear, editar y borrar mensajes.
- Se le añadirá estilo a través de CSS.
|\/| [- || ~|~ [- ( /\ ~|~ () ^/_ '|