domingo, 18 de febrero de 2024

28.- Plataforma de E-learning (fixtures, modelos polimórficos, campos de modelos personalizados.)

En este post, comenzaremos un nuevo proyecto de Django que consistirá en una plataforma de e-learning con nuestro propio sistema de gestión de contenido (CMS). Las plataformas de aprendizaje en línea son un gran ejemplo de aplicaciones donde necesitas proporcionar herramientas para generar contenido teniendo en cuenta la flexibilidad de los contenidos que se ofrecen.

En este capítulo, aprenderás cómo:

- Crear modelos para el CMS
- Crear fixtures para tus modelos y aplicarlos
- Usar la herencia de modelos para crear modelos de datos para contenido polimórfico
- Crear campos de modelo personalizados
- Ordenar contenidos y módulos del curso
- Construir vistas de autenticación para el CMS


Configuración del proyecto de e-learning


Tu proyecto práctico final será una plataforma de e-learning. Primero, crea un entorno virtual para tu nuevo proyecto dentro del directorio env/ con el siguiente comando:

```
python -m venv env/educa
```

Si estás usando Linux o macOS, ejecuta el siguiente comando para activar tu entorno virtual:

```
source env/educa/bin/activate
```

Si estás usando Windows, utiliza el siguiente comando en su lugar:

```
.\env\educa\Scripts\activate
```

Instala Django en tu entorno virtual con el siguiente comando:

```
pip install Django
```

Vas a gestionar la carga de imágenes en tu proyecto, así que también necesitas instalar Pillow con el siguiente comando:

```
pip install Pillow
```

Crea un nuevo proyecto utilizando el siguiente comando:

```
django-admin startproject educa
```

Ingresa al nuevo directorio educa y crea una nueva aplicación utilizando los siguientes comandos:

```
cd educa
django-admin startapp courses
```

Edita el archivo settings.py del proyecto educa y agrega courses a las aplicaciones instaladas (INSTALLED_APPS), de la siguiente manera. La nueva línea está resaltada en azul:

```
INSTALLED_APPS = [
    ...
    'courses',
]
```

Sirviendo archivos multimedia


Antes de crear los modelos para los cursos y su contenido, prepararemos el proyecto para servir archivos multimedia. Los profesores de los cursos podrán subir archivos multimedia al contenido del curso utilizando el CMS que construiremos. Por lo tanto, configuraremos el proyecto para servir archivos multimedia.

Edita el archivo settings.py del proyecto y agrega las siguientes líneas:

MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'

Esto permitirá a Django gestionar las subidas de archivos y servir archivos multimedia. MEDIA_URL es la URL base utilizada para servir los archivos multimedia subidos por los usuarios. MEDIA_ROOT es la ruta local donde se encuentran. Las rutas y URLs de los archivos se construyen dinámicamente anteponiendo la ruta del proyecto o la URL de medios a ellos para una mayor portabilidad.

Ahora, edita el archivo urls.py principal del proyecto educa y modifica el código de la siguiente manera. Las líneas nuevas están resaltadas en negrita:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Hemos agregado la función de ayuda static() para servir archivos multimedia con el servidor de desarrollo de Django durante el desarrollo (es decir, cuando la configuración DEBUG está establecida en True).

Ten en cuenta que la función auxiliar static() es adecuada para el desarrollo, pero no para la producción. Django es muy ineficiente para servir archivos estáticos. Nunca sirvas tus archivos estáticos con Django en un entorno de producción. Aprenderemos cómo servir archivos estáticos en un entorno de producción más adelante.

Aquí hay algunos puntos importantes adicionales:

  • Los archivos estáticos son cosas como archivos CSS, JavaScript e imágenes que no cambian con frecuencia y no requieren procesamiento dinámico por parte de Django.
  • En producción, es mejor servir archivos estáticos usando un servidor web dedicado como Nginx o Apache, ya que están optimizados para servir archivos estáticos de manera eficiente.
  • Usar Django para servir archivos estáticos en producción puede ralentizar tu aplicación y aumentar la carga del servidor.

Construyendo los modelos de cursos


Tu plataforma de e-learning ofrecerá cursos sobre diversos temas. Cada curso se dividirá en un número configurable de módulos, y cada módulo contendrá un número configurable de contenidos. Los contenidos serán de varios tipos: texto, archivos, imágenes o vídeos. El siguiente ejemplo muestra cómo será la estructura de datos de tu catálogo de cursos:

Asignatura 1 (subject)     Curso 1         Módulo 1             Contenido 1 (imagen)             Contenido 2 (texto)         Módulo 2             Contenido 3 (texto)             Contenido 4 (archivo)             Contenido 5 (video) ...

Construyamos los modelos de cursos. Edita el archivo models.py de la aplicación courses y agrega el siguiente código:

from django.db import models
from django.contrib.auth.models import User

class Subject(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)

    class Meta:
        ordering = ['title']

    def __str__(self):
        return self.title

class Course(models.Model):
    owner = models.ForeignKey(User,
        related_name='courses_created',
        on_delete=models.CASCADE)
    subject = models.ForeignKey(Subject,
        related_name='courses',
        on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    overview = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created']

    def __str__(self):
        return self.title

class Module(models.Model):
    course = models.ForeignKey(Course,
        related_name='modules',
        on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)

    def __str__(self):
        return self.title

Estos son los modelos iniciales de Subject (asignatura), Course (Curso) y Module (Módulo). Los campos del modelo Course son los siguientes:

  • owner: El profesor que creó este curso.
  • subject: La materia o asignatura a la que pertenece este curso. Es un campo ForeignKey que apunta al modelo Subject.
  • title: El título del curso.
  • slug: El slug del curso. Se utilizará en las URLs más adelante.
  • overview: Una columna TextField para almacenar una descripción general del curso.
  • created: La fecha y hora en que se creó el curso. Django lo establecerá automáticamente al crear nuevos objetos debido a auto_now_add=True.

Cada curso se divide en varios módulos. Por lo tanto, el modelo Module contiene un campo ForeignKey que apunta al modelo Course.


Abre el shell y ejecuta el siguiente comando:

python manage.py makemigrations

Verás la siguiente salida:

Migrations for 'courses':
courses/migrations/0001_initial.py:
- Crear modelo Curso
- Crear modelo Módulo
- Crear modelo Tema
- Agregar campo tema al curso

Luego, ejecuta el siguiente comando para aplicar todas las migraciones a la base de datos:

python manage.py migrate

Registrando los modelos en el panel de administración


Agreguemos los modelos de cursos al panel de administración. Edita el archivo admin.py dentro del directorio de la aplicación courses y agrega el siguiente código:

from django.contrib import admin
from .models import Subject, Course, Module

@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
    list_display = ['title', 'slug']
    prepopulated_fields = {'slug': ('title',)}

class ModuleInline(admin.StackedInline):
    model = Module

@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    list_display = ['title', 'subject', 'created']
    list_filter = ['created', 'subject']
    search_fields = ['title', 'overview']
    prepopulated_fields = {'slug': ('title',)}
    inlines = [ModuleInline]

Explicación:

  • Este código registra los modelos Subject, Course y Module en el panel de administración de Django.
  • La función @admin.register() decora cada clase y se usa para registrar el modelo correspondiente.
  • list_display define las columnas que se muestran en la lista de objetos de cada modelo.
  • prepopulated_fields establece que el campo slug se genere automáticamente a partir del título.
  • La clase ModuleInline define una relación "inline" entre los modelos Course y Module, lo que permite editar módulos directamente desde la página del curso.
  • list_filter y search_fields permiten filtrar y buscar objetos en la lista.

Ahora los modelos de la aplicación courses están registrados en el panel de administración.


Usando fixtures para proporcionar datos iniciales a los modelos


A veces, es posible que desees rellenar previamente tu base de datos con datos ya registrados. Esto es útil para incluir automáticamente datos iniciales en la configuración del proyecto, en lugar de tener que agregarlos manualmente. Django viene con una forma simple de cargar y volcar datos desde la base de datos a archivos llamados fixtures. Django admite fixtures en formatos JSON, XML o YAML.

Va a crear un fixture para incluir varios objetos Subject (asignaturas) iniciales para el proyecto.

Pasos:

  1. Crea un superusuario: Para comenzar, ejecute el siguiente comando en su terminal para crear un superusuario que pueda acceder al panel de administración:
python manage.py createsuperuser
  1. Inicia el servidor de desarrollo: Luego, ejecuta el siguiente comando para iniciar el servidor de desarrollo de Django:
python manage.py runserver
  1. Accede al panel de administración: Abra la siguiente URL en su navegador para acceder al panel de administración de Django:
http://127.0.0.1:8000/admin/courses/subject/
  1. Crea Asignaturas: Utiliza el panel de administración para crear varios objetos Subject iniciales para el proyecto.

  2. Observa la lista de cambios: Verás la lista de cambios de las asignaturas que has creado.

The subject change list view on the administration site

Ejecuta el siguiente comando:

python manage.py dumpdata courses --indent=2

Explicación:

  • Ejecuta este comando en la terminal para extraer datos de la base de datos a un archivo JSON.
  • dumpdata es el comando utilizado para exportar datos.
  • courses indica que solo se extraerán datos de la aplicación courses.
  • --indent=2 agrega sangría para mejorar la legibilidad del archivo JSON.

Salida esperada:

Verás una salida similar a la siguiente:

JSON
[
  {
    "model": "courses.subject",
    "pk": 1,
    "fields": {
      "title": "Física",
      "slug": "fisica"
    }
  },
  {
    "model": "courses.subject",
    "pk": 2,
    "fields": {
      "title": "Matemáticas",
      "slug": "matematicas"
    }
  },
  // ... (otras asignaturas que creaste)
]

Explicación de la salida:

  • La salida es un array JSON que contiene un objeto por cada sujeto que has creado.
  • Cada objeto tiene tres propiedades:
    • model: nombre del modelo (ej. "courses.subject").
    • pk: clave primaria del objeto en la base de datos.
    • fields: diccionario con los campos del modelo y sus valores.

Siguiente paso:

Vamos a guardar esta salida en un archivo JSON dentro de una nueva carpeta llamadafixtures que crearemos dentrode la aplicación courses. Te proporcionaré los comandos para hacerlo a continuación.

>>> mkdir courses/fixtures

>>> python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json

Ahora vuelve a ejecutar el servidor de desarrollo y usa el panel de administración para borrar todas las asignaturas (subjects) que hayas creado, tal como se muestra en la siguiente imagen:


borrando todas las asignaturas existentes

Depués de borrarlas todas, vuelve a cargarlas en la base de datos de nuevo usando el siguiente comando:

    python manage.py loaddata subjects.json

Como verás todas la asignaturas guardadas en el archivo subjects.json dentro del directorio fixtures han sido cargadas de nuevo en la base de datos de la aplicación.

Subjects from the fixture are now loaded into the database

Por defecto, Django busca estos archivos dentro del directorio fixtures que pueda haber en cada aplicación, pero puedes especificar otro directorio. Para ello debes usar el argumento FIXTURE_DIRS dentro del settings.py del proyecto para decirle a Django donde debe buscar estos archivos.

Estos archivos "fixtures" no solo son útiles para cargar datos iniciales sino también para proporcionar o proporcionar datos para hacer futuros text del modelo.

Puedes leer sobre cómo usar fixtures para testing en https://docs.djangoproject.com/en/5.0/ topics/testing/tools/#fixture-loading.

Si quieres cargar fixtures en las migraciones de modelos, consulta la documentación de Django sobre migraciones de datos. Puedes encontrar la documentación para migrar datos en https://docs.djangoproject.com/en/5.0/topics/migrations/#data-migrations.

Hasta aquí, hemos creado los modelos para gestionar las asignaturas, cursos y módulos de los cursos. A continuación, crearemos modelos para gestionar diferentes tipos de contenidos de módulos.


Creación de modelos para contenido polimórfico


Tenemos en mente agregar diferentes tipos de contenido a los módulos del curso, como texto, imágenes, archivos y videos.

El polimorfismo es la provisión de una sola interfaz para entidades de diferentes tipos. Necesitas un modelo de datos versátil que te permita almacenar contenido diverso que sea accesible a través de una sola interfaz. En post anteriores vimos la conveniencia de usar relaciones genéricas para crear claves externas que puedan apuntar a los objetos de cualquier modelo. Vamos a crear un modelo Content que represente el contenido de los módulos y definir una relación genérica para asociar cualquier objeto con el objeto del contenido.

Edita el archivo models.py de la aplicación de course (cursos) y agrega las siguientes importaciones:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

Luego, agrega el siguiente código al final del archivo:

class Content(models.Model):
    module = models.ForeignKey(Module,
        related_name='contents',
        on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
        on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')

Explicación:

  • Importaciones:
    • ContentType: Este modelo de Django almacena información sobre todos los modelos registrados en la aplicación.
    • GenericForeignKey: Este campo permite crear relaciones con objetos de cualquier modelo registrado.
  • Modelo "Content":
    • module: Campo ForeignKey que relaciona cada contenido con un módulo específico.
    • content_type: Campo ForeignKey que referencia el modelo al que pertenece el contenido específico (texto, imagen, video, etc.).
    • object_id: Campo que almacena la clave primaria del objeto relacionado (por ejemplo, la ID de un texto específico).
    • item: Campo GenericForeignKey que combina content_type y object_id para acceder al objeto relacionado de forma genérica.

El modelo "Content"

Este modelo representa el contenido de un módulo. Dado que un módulo puede contener distintos tipos de contenido (texto, imágenes, videos, etc.), se define un campo ForeignKey que apunta al modelo Module. Además, se establece una relación genérica para asociar objetos de diferentes modelos que representan estos tipos de contenido.

Relación genérica:

Para establecer una relación genérica se necesitan tres campos:

  • content_type: Campo ForeignKey al modelo ContentType. Este modelo almacena información sobre todos los modelos registrados en la aplicación.
  • object_id: Campo PositiveIntegerField que almacena la clave primaria del objeto relacionado.
  • item: Campo GenericForeignKey al objeto relacionado, combinando los dos campos anteriores.

Almacenamiento en la base de datos:

Solo los campos content_type y object_id tienen una columna correspondiente en la tabla de la base de datos de este modelo. El campo item permite recuperar o establecer directamente el objeto relacionado, y su funcionalidad se basa en los otros dos campos.

Modelos específicos para cada tipo de contenido:

Se utilizará un modelo diferente para cada tipo de contenido. Los modelos Content tendrán algunos campos comunes, pero diferirán en los datos que pueden almacenar. De esta manera, se crea una interfaz única para manejar diferentes tipos de contenido.

Explicación adicional:

  • Un modelo genérico como "Content" es útil cuando la misma funcionalidad se aplica a diferentes tipos de datos. En este caso, todos los tipos de contenido comparten características como estar dentro de un módulo, pero tienen características específicas (texto, imagen, etc.) que se gestionan en modelos separados.
  • El uso de un campo GenericForeignKey brinda flexibilidad y evita la necesidad de definir relaciones específicas con cada modelo de contenido.


Utilizando la herencia de modelos


Django soporta la herencia de modelos. Funciona de manera similar a la herencia de clases estándar en Python.

Django ofrece las siguientes tres opciones para usar la herencia de modelos:

Modelos abstractos: Útiles cuando quieres incluir alguna información común en varios modelos.

Herencia de modelos con múltiples tablas: Aplicable cuando cada modelo en la jerarquía es considerado un modelo completo por sí mismo.

Modelos proxy: Útiles cuando necesitas cambiar el comportamiento de un modelo, por ejemplo, incluyendo métodos adicionales, cambiando el gestor predeterminado o utilizando diferentes opciones meta.

Vamos a examinar más de cerca cada una de ellas.

Modelos abstractos

Un modelo abstracto es una clase base en la cual defines los campos que quieres incluir en todos los modelos hijos.

Django no crea ninguna tabla de base de datos para los modelos abstractos. Una tabla de base de datos es creada para cada modelo hijo, incluyendo los campos heredados de la clase abstracta y los definidos en el modelo hijo.

Para marcar un modelo como abstracto, necesitas incluir abstract=True en su clase Meta. Django reconocerá que es un modelo abstracto y no creará una tabla de base de datos para él. Para crear modelos hijos, simplemente necesitas subclasear el modelo abstracto.

El siguiente ejemplo muestra un modelo Content abstracto y un modelo hijo Text:

from django.db import models class BaseContent(models.Model): title = models.CharField(max_length=100) created = models.DateTimeField(auto_now_add=True) class Meta: abstract = True class Text(BaseContent): body = models.TextField()


En este caso, Django crearía una tabla únicamente para el modelo Text, incluyendo los campos title, created y body.


Herencia de modelos con múltiples tablas

En la herencia de modelos con múltiples tablas, cada modelo corresponde a una tabla de base de datos. Django crea un campo OneToOneField para la relación entre el modelo hijo y su modelo padre. Para usar la herencia de modelos con múltiples tablas, debes subclasificar un modelo existente. Django creará una tabla de base de datos tanto para el modelo original como para el submodelo. El siguiente ejemplo muestra la herencia de modelos con múltiples tablas:

from django.db import models class BaseContent(models.Model): title = models.CharField(max_length=100) created = models.DateTimeField(auto_now_add=True) class Meta: abstract = True class Text(BaseContent): body = models.TextField()

Django incluirá automáticamente un campo OneToOneField generado en el modelo Text y creará una tabla de base de datos para cada modelo.


Modelos proxy

Un modelo proxy cambia el comportamiento de un modelo. Ambos modelos operan en la tabla de base de datos del modelo original. Para crear un modelo proxy, agrega proxy=True a la clase Meta del modelo. El siguiente ejemplo ilustra cómo crear un modelo proxy:

from django.db import models
from django.utils import timezone

class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)

class OrderedContent(BaseContent):
    class Meta:
        proxy = True
        ordering = ['created']

    def created_delta(self):
        return timezone.now() - self.created
  • Este código define dos modelos:
    • BaseContent: El modelo base que ya hemos visto anteriormente, con los campos title y created.
    • OrderedContent: Un modelo proxy que hereda de BaseContent. Los modelos proxy no crean nuevas tablas en la base de datos, sino que actúan como una vista diferente de la tabla del modelo base.

Características del modelo "OrderedContent":

  • proxy = True: Indica que es un modelo proxy.
  • ordering = ['created']: Establece el orden predeterminado para las consultas (QuerySets) del modelo, ordenando por la fecha de creación (created).
  • created_delta(): Define un método adicional que calcula el tiempo transcurrido desde la creación del contenido.

Beneficios de usar un modelo proxy:

  • Permite definir órdenes predeterminados para consultas específicas sin modificar el modelo base.
  • Agrega métodos personalizados al modelo sin afectar el modelo base.
  • Mantiene la sincronización con la base de datos del modelo base.

Importante:

  • Los objetos de OrderedContent y BaseContent representan datos en la misma tabla de la base de datos.
  • Puedes acceder a los objetos a través del ORM utilizando cualquiera de los dos modelos.


Creando los modelos de Contenido


El modelo de Contenido (Content) de tu aplicación de cursos contiene una relación genérica para asociar diferentes tipos de contenido con él. Crearemos un modelo diferente para cada tipo de contenido. Todos los modelos de Contenido tendrán algunos campos en común y campos adicionales para almacenar datos personalizados. Vas a crear un modelo abstracto que proporcione los campos comunes para todos los modelos de Contenido.

Edita el archivo models.py de la aplicación de cursos y agrega el siguiente código:

class ItemBase(models.Model):
    owner = models.ForeignKey(User,
        related_name='%(class)s_related',
        on_delete=models.CASCADE)
    title = models.CharField(max_length=250)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    def __str__(self):
        return self.title

class Text(ItemBase):
    content = models.TextField()

class File(ItemBase):
    file = models.FileField(upload_to='files')

class Image(ItemBase):
    file = models.FileField(upload_to='images')

class Video(ItemBase):
    url = models.URLField()

Explicación:

  • Este código define varios modelos:
    • ItemBase: Un modelo abstracto que define campos comunes para todos los tipos de contenido (propietario, título, fecha de creación y actualización). Al ser abstracto, este modelo no se utiliza directamente para crear tablas en la base de datos.
    • TextFileImageVideo: Modelos heredados de ItemBase, cada uno con un campo específico para su tipo de contenido:
      • Text: Campo content para texto.
      • File: Campo file para archivos genéricos.
      • Image: Campo file para imágenes.
      • Video: Campo url para enlaces a videos externos.

Campos en "ItemBase":

  • owner: Campo ForeignKey que relaciona el contenido con el usuario que lo creó.
  • title: Campo CharField para el título del contenido.
  • created: Campo DateTimeField para la fecha de creación del contenido.
  • updated: Campo DateTimeField para la fecha de actualización del contenido.

Uso de "related_name" con modelos heredados:

  • El campo owner está definido en un modelo abstracto, por lo que necesita un related_name diferente para cada modelo heredado.
  • El placeholder %(class)s en related_name permite generar automáticamente el nombre en base al nombre de la clase heredada. Ejemplo: Text tendrá text_related como related_name.

Resumen:

  • Este código crea un modelo base para contenido y modelos específicos para diferentes tipos de contenido (texto, archivos, imágenes, videos).
  • Los modelos heredados comparten campos comunes y agregan campos específicos para su tipo de contenido.
  • El uso de un modelo base y related_name con placeholder facilita la organización y flexibilidad del código.

El campo owner te permite almacenar qué usuario creó el contenido. Dado que este campo está definido en una clase abstracta, necesitas un related_name diferente para cada submodelo. Django te permite especificar un marcador de posición para el nombre de la clase del modelo en el atributo related_name como %(class)s. Al hacerlo, el related_name para cada modelo hijo se generará automáticamente. Dado que estás usando '%(class)s_related' como el related_name, la relación inversa para los modelos hijos será text_related, file_related, image_related y video_related, respectivamente.

Has definido cuatro modelos de Contenido diferentes que heredan del modelo abstracto ItemBase. Son los siguientes:

• Text: Para almacenar contenido de texto

• File: Para almacenar archivos, como PDFs

• Image: Para almacenar archivos de imagen

• Video: Para almacenar videos; utilizas un campo URLField para proporcionar una URL de video con el fin de incrustarlo

Cada modelo hijo contiene los campos definidos en la clase ItemBase además de sus propios campos. Se creará una tabla de base de datos para los modelos Text, File, Image y Video, respectivamente. No habrá tabla de base de datos asociada con el modelo ItemBase ya que es un modelo abstracto.

Edita el modelo de Contenido (Content) que creaste anteriormente y modifica su campo content_type, de la siguiente manera:

Edita el modelo de Contenido que creaste anteriormente y modifica su campo content_type de la siguiente manera:


código a modificar en el archivo


Agregas un argumento limit_choices_to para limitar los objetos ContentType que pueden ser utilizados para la relación genérica. Utilizas la búsqueda de campo model__in para filtrar la consulta a los objetos ContentType con un atributo model que sea 'text', 'video', 'image' o 'file'.

Ahora, creemos una migración para incluir los nuevos modelos que has agregado. Ejecuta el siguiente comando desde la línea de comandos:

python manage.py makemigrations

Verás la siguiente salida:

Migraciones para 'courses':
courses/migrations/0002_video_text_image_file_content.py
- Crear modelo Video
- Crear modelo Text
- Crear modelo Image
- Crear modelo File
- Crear modelo Content


Luego, ejecuta el siguiente comando para aplicar la nueva migración:

python manage.py migrate


La salida que ves debería terminar con la siguiente línea:


Aplicando courses.0002_video_text_image_file_content... OK


Has creado modelos que son adecuados para agregar un contenido variado a los módulos del curso. Sin embargo, todavía falta algo en tus modelos: los módulos del curso y los contenidos deberían seguir un orden determinado. Necesitas un campo que te permita ordenarlos fácilmente.

Creando campos de modelo personalizados


Django te brinda una colección completa de campos de modelo que puedes usar para construir tus modelos. Sin embargo, también puedes crear tus propios campos de modelo para almacenar datos personalizados o modificar el comportamiento de los campos existentes.

Necesidad de un campo de orden:

  • Necesitas un campo que permita definir un orden para los objetos de tu modelo.
  • Una forma sencilla de especificar un orden utilizando campos de Django existentes es agregando un PositiveIntegerField a tus modelos.
  • Mediante números enteros, puedes especificar fácilmente el orden de los objetos.

Campo de orden personalizado:

  • Puedes crear un campo de orden personalizado que herede de PositiveIntegerField y proporcione un comportamiento adicional.

  • Este campo tendrá dos funcionalidades clave:

    1. Asignación automática de orden:

      • Al guardar un nuevo objeto sin un orden específico, tu campo debe asignar automáticamente el número que sigue al último objeto ordenado existente.
      • Ejemplo: Si hay dos objetos con orden 1 y 2 respectivamente, al guardar un tercer objeto, debe asignarse automáticamente el orden 3 si no se ha proporcionado un orden específico.
    2. Ordenar objetos con respecto a otros campos:

      • Los módulos de un curso se ordenarán con respecto al curso al que pertenecen, y el contenido de los módulos se ordenará con respecto al módulo al que pertenecen.

Instrucciones:

  1. Crea un nuevo archivo fields.py dentro del directorio de la aplicación courses.
  2. Agrega el siguiente código al archivo fields.py:
from django.db import models
from django.core.exceptions import ObjectDoesNotExist

class OrderField(models.PositiveIntegerField):
    def __init__(self, for_fields=None, *args, **kwargs):
        self.for_fields = for_fields
        super().__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        if getattr(model_instance, self.attname) is None:
            # Sin valor actual
            try:
                qs = self.model.objects.all()
                if self.for_fields:
                    # Filtrar por objetos con los mismos valores de campo
                    # para los campos en "for_fields"
                    query = {field: getattr(model_instance, field) for field in self.for_fields}
                    qs = qs.filter(**query)
                    # Obtener el orden del último elemento
                    last_item = qs.latest(self.attname)
                    value = last_item.order + 1
                except ObjectDoesNotExist:
                    value = 0
                setattr(model_instance, self.attname, value)
                return value
            else:
                return super().pre_save(model_instance, add)


Este es el OrderField personalizado. Hereda del campo PositiveIntegerField proporcionado por Django.

Tu campo OrderField toma un parámetro opcional for_fields, que te permite indicar los campos utilizados para ordenar los datos.

Tu campo sobrescribe el método pre_save() del campo PositiveIntegerField, que se ejecuta antes de guardar el campo en la base de datos. En este método, realizas las siguientes acciones:

1.) Verificas si ya existe un valor para este campo en la instancia del modelo. Utilizas self.attname, que es el nombre de atributo dado al campo en el modelo. Si el valor del atributo es diferente de None, calculas el orden que debes darle de la siguiente manera:

   1. Construyes un QuerySet para recuperar todos los objetos del modelo del campo. Recuperas la clase del modelo al que pertenece el campo accediendo a self.model.

   2. Si hay algún nombre de campo en el atributo for_fields del campo, filtras el QuerySet por el valor actual de los campos del modelo en for_fields. De esta manera, calculas el orden con respecto a los campos dados.

   3. Recuperas el objeto con el orden más alto con last_item = qs.latest(self.attname) de la base de datos. Si no se encuentra ningún objeto, asumes que este objeto es el primero y le asignas el orden 0.

   4. Si se encuentra un objeto, sumas 1 al orden más alto encontrado.

   5. Asignas el orden calculado al valor del campo en la instancia del modelo usando setattr() y lo devuelves.

2.) Si la instancia del modelo tiene un valor para el campo actual, lo utilizas en lugar de calcularlo.

Cuando crees campos de modelo personalizados, hazlos genéricos. Evita codificar datos que dependan de un modelo o campo específico. Tu campo debería funcionar en cualquier modelo.

Puedes encontrar más información en https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/.


Agregando orden a los objetos módulo y contenido


Vamos a añadir el nuevo campo a tus modelos. Edita el archivo models.py de la aplicación courses, e importa la clase OrderField y un campo al modelo Module, de la siguiente manera:

from .fields import OrderField

class
Module(models.Model):
# ...
    order = OrderField(blank=True, for_fields=['course'])

Nombras el nuevo campo como order y especificas que el orden se calcula con respecto al curso al establecer for_fields=['course']. Esto significa que el orden para un nuevo módulo se asignará agregando 1 al último módulo del mismo objeto Course.

Ahora, puedes editar el método str() del modelo Module para incluir su orden, de la siguiente manera:

class Module(models.Model):
        # ...
        def __str__(self):
            return f'{self.order}. {self.title}'

Los contenidos del módulo también necesitan seguir un orden particular. Agrega un campo OrderField al modelo Content, de la siguiente manera:

class Content(models.Model):
    # ...
    order = OrderField(blank=True, for_fields=['module'])

Esta vez, especificas que el orden se calcula con respecto al campo module.

Finalmente, vamos a añadir un orden predeterminado para ambos modelos. Agrega la siguiente clase Meta a los modelos Module y Content:


class Module(models.Model):
    # ...
    class Meta: ordering = ['order']

class
Content(models.Model):
    # ...
    class Meta: ordering = ['order']

Cuando creas campos de modelo personalizados, hazlos genéricos. Evita codificar datos que dependan de un modelo o campo específico. Tu campo debería funcionar en cualquier modelo.

Los modelos "Module" y "Content" ahora deberían tener el siguiente aspecto:

class Module(models.Model):
    course = models.ForeignKey(Course,
                              related_name='modules',
                              on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    order = OrderField(blank=True, for_fields=['course'])

    class Meta:
        ordering = ['order']

    def __str__(self):
        return f'{self.order}. {self.title}'

class Content(models.Model):
    module = models.ForeignKey(Module,
                              related_name='contents',
                              on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
                                     on_delete=models.CASCADE,
                                     limit_choices_to={'model__in': (
                                         'text',
                                         'video',
                                         'image',
                                         'file'
                                     )})
    object_id = models.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')
    order = OrderField(blank=True, for_fields=['module'])

    class Meta:
        ordering = ['order']

Creación de la migración:

Ahora que hemos actualizado los modelos, necesitamos crear una nueva migración para reflejar los cambios en el campo "order". Para ello, abre la terminal y ejecuta el siguiente comando:

python manage.py makemigrations courses

El problema es que tendremos la siguiente salida:

"""It is impossible to add a non-nullable field 'order' to content without
specifying a default. This is because the database needs something to populate
existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null
value for this column)
2) Quit and manually define a default value in models.py.
Select an option:""


Django nos está diciendo que tenemos que facilitarle un valor por defecto para el nuevo campo que hemos creado "order" y que se aplicará en los valores que ya existen en la base de datos. Si el campo incluye la sentencia null=True, se aceptarán valores nulos y Django creará las migraciones automáticamente en vez de preocuparnos por los valores por defecto. En nuestro caso puedes especificar un valor por defecto o cancelar la migración y añadir el atributo default al campo order en el archivo models.py antes de realizar la migración.

En nuestro caso pulsaremos 1 más Enter para facilitar al programa un valor por defecto en los valores existentes. Verás la siguiente salida:

"""Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible
to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>>"""

Ahora pulsa un 0 más Enter. Con esto le estaremos facilitando el valor por defecto necesario. Django te volverá a preguntar lo mismo esta vez en relación con el modelo Module. Al igual que antes, escoge la primera opción y vuelve a introducir el cero como valor por defecto.

Finalmente verás una salida similar a esta, con las migraciones realizadas:

"""Migrations for 'courses':
courses/migrations/0003_alter_content_options_alter_module_options_and_more.py
- Change Meta options on content
- Change Meta options on module
- Add field order to content
- Add field order to module"""

Ahora aplica las migraciones con la siguiente instrucción:

python manage.py migrate

La salida de este comando te indicará si la migraciones han sido correctamente aplicadas:

Applying courses.0003_alter_content_options_alter_module_options_and_more... OK

Bien, ahora vamos a probar este nuevo campo. Abre el shell con el siguiente comando:

python manage.py shell

¿Qué tal si creamos un nuevo curso?

>>> from django.contrib.auth.models import User
>>> from courses.models import Subject, Course, Module
>>> user = User.objects.last()
>>> subject = Subject.objects.last()
>>> c1 = Course.objects.create(subject=subject, owner=user, title='Curso 1', slug='curso1')

Has creado un curso en la base de datos. Ahora, añadirás módulos al curso y verás cómo su orden se calcula automáticamente. Creas un módulo inicial y verificas su orden:

>>> m1 = Module.objects.create(course=c1, title='Módulo 1')
>>> m1.order
0

OrderField establece su valor en 0, ya que este es el primer objeto Módulo creado para el curso dado. Puedes crear un segundo módulo para el mismo curso:

>>> m2 = Module.objects.create(course=c1, title='Módulo 2')
>>> m2.order
1

OrderField calcula el siguiente valor de orden, añadiendo 1 al orden más alto de los objetos existentes. Creemos un tercer módulo, forzando un orden específico:

>>> m3 = Module.objects.create(course=c1, title='Módulo 3', order=5)
>>> m3.order 
5

Si proporcionas un orden personalizado al crear o guardar un objeto, OrderField usará ese valor en lugar de calcular el orden. Añadamos un cuarto módulo:

>>> m4 = Module.objects.create(course=c1, title='Módulo 4')
>>> m4.order 
6

El orden para este módulo se ha establecido automáticamente. Tu campo OrderField no garantiza que todos los valores de orden sean consecutivos. Sin embargo, respeta los valores de orden existentes y siempre asigna el siguiente orden basado en el orden existente más alto.


Vamos a crear un segundo curso y añadirle un módulo:

>>> c2 = Course.objects.create(subject=subject, title='Curso 2', slug='curso2', owner=user)
>>> m5 = Module.objects.create(course=c2, title='Módulo 1')
>>> m5.order
0

Para calcular el orden del nuevo módulo, el campo solo toma en consideración los módulos existentes que pertenecen al mismo curso. Dado que este es el primer módulo del segundo curso, el orden resultante es 0. Esto se debe a que especificaste for_fields=['course'] en el campo de orden del modelo Module.

¡Felicidades! Has creado con éxito tu primer campo de modelo personalizado. A continuación, vas a crear un sistema de autenticación para el CMS.


Añadiendo vistas de autenticación:

Ahora que has creado un modelo de datos polimórfico, vas a construir un CMS para gestionar los cursos y sus contenidos. El primer paso es añadir un sistema de autenticación para el CMS.

Añadiendo un sistema de autenticación:

Vas a utilizar el framework de autenticación de Django para que los usuarios se autentiquen en la plataforma de e-learning. Tanto los instructores o proferosres como los estudiantes serán instancias del modelo de usuario de Django, por lo que podrán iniciar sesión en el sitio utilizando las vistas de autenticación de django.contrib.auth.

Edita el archivo urls.py principal del proyecto educa e incluye las vistas de inicio de sesión y cierre de sesión del framework de autenticación de Django:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
    path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), 
    path('admin/', admin.site.urls),
]

if
settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Si la configuración DEBUG está activada, se añaden las URLs para servir archivos estáticos.


Creando las plantillas de autentificación.


Crea la siguiente estructura de archivos dentro de la aplicación courses.

templates/
    base.html
    registration/
        login.html
        logged_out.html

Antes de construir las plantillas de autentificación, necesitamos preparar la plantila base.html. Edítala y añade el siguiente contenido:

{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>{% block title %}Educa{% endblock %}</title>
        <link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
    <div id="header">
        <a href="/" class="logo">Educa</a>
        <ul class="menu">
            {% if request.user.is_authenticated %}
            <li>
<!-- Actualmente es necesario enviar la orden de logout mediante 
el método post, sino no se ejecutará. Para disimular el botón y que 
parezca un enlace hay que añadir código CCS. #logout-form #log_out button -->
            <form id="logout-form" action="{% url 'logout' %}" method="post">
                {% csrf_token %}
                <button type="submit">Sign out</button>
            </form>
            </li>
            {% else %}
                <li><a href="{% url "login" %}">Sign in</a></li>
            {% endif %}
        </ul>
</div>
<div id="content">
    {% block content %}
    {% endblock %}
</div>
<script>
    document.addEventListener('DOMContentLoaded', (event) => {
    // DOM loaded
    {% block domready %}
    {% endblock %}
    })
</script>
</body>
</html>

Código CSS para disimular el botón como que fuera un enlace:

Elearning/educa/courses/static/css/base.css

#...
#logout-form {
    display: inline;
    margin: 0;
    padding: 0;
    border: none;
    background: none;
}

#logout-form button {
    padding: 0;
    border: none;
    background: none;
    color: white; /* Color del enlace */
    text-decoration: none; /* Subrayado para indicar que es un enlace */
    cursor: pointer; /* Cambiar el cursor al pasar por encima */
}

Este es el template base que será extendido por el resto de los templates. En este template, defines los siguientes bloques:

  • title: El bloque para que otros templates añadan un título personalizado para cada página.
  • content: El bloque principal para el contenido. Todos los templates que extiendan el template base deben añadir contenido a este bloque.
  • domready: Ubicado dentro del event listener de JavaScript para el evento DOMContentLoaded. Permite ejecutar código cuando el Document Object Model (DOM) ha terminado de cargar.

Los estilos CSS usados en este template están ubicados en el directorio static/ de la aplicación de cursos en el código que viene con este post. Copia el directorio static/ dentro del mismo directorio de tu proyecto para usarlos. Puedes encontrar el contenido del directorio en este enlace.

Edita la plantilla registration/login.html y añade el siguiente código:


Elearning/educa/courses/templates/registration/login.html

{% extends "base.html" %}
{% block title %}Log-in{% endblock %}
{% block content %}
<h1>Log-in</h1>
<div class="module">
    {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
    {% else %}
    <p>Please, use the following form to log-in:</p>
    {% endif %}
    <div class="login-form">
        <form action="{% url 'login' %}" method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <input type="hidden" name="next" value="{{ next }}" />
            <p><input type="submit" value="Log-in"></p>
        </form>
    </div>
</div>
{% endblock %}

Esta es la plantilla de login standar para la vista de login.

Ahora edita la plantilla registration/logged_out.html y añade el siguiente código:

Elearning/educa/courses/templates/registration/logged_out.html

{% extends "base.html" %}
{% block title %}Logged out{% endblock %}
{% block content %}
<h1>Logged out</h1>
<div class="module">
    <p>
        You have been successfully logged out.
        You can <a href="{% url 'login' %}">log-in again</a>.
    </p>
</div>
{% endblock %}

Esta es la plantilla que mostrará al usuario después de que se haya desconectado.

Vamos a ver si funciona. Ejecuta el servidor de desarrollo de Django con:

python manage.py runserver

Si abres esta dirección en tu navegador, http://127.0.0.1:8000/accounts/login/, deberías ver la siguiente página.

The account login page


Abre esta dirección http://127.0.0.1:8000/accounts/logout/ en el navegador. Debería verse la página de desconexión tal como se muestra a continuación:


account logged out page



Con esto habremos creado un sistema de autenticación de usuarios para nuestro CMS.



El código fuente de este capítulo se puede encontrar en el siguiente enlace de GITHUB