viernes, 8 de marzo de 2024

29.- Creando un sistema para gestionar los contenidos.

Creando un Sistema de Gestión de Contenidos

En el post anterior, creamos los modelos de la aplicación para la plataforma de aprendizaje en línea y aprendimos cómo crear y aplicar fixtures de datos para los modelos. Creamos un campo de modelo personalizado para ordenar objetos e implementamos la autenticación de usuarios.

En este, aprenderás a construir la funcionalidad para que los instructores creen cursos y gestionen el contenido de esos cursos de manera versátil y eficiente.

Veremos cosas cómo:

  • Crear un sistema de gestión de contenido utilizando vistas basadas en clases y mixins.
  • Construir formsets y model formsets para editar módulos de curso y contenido de módulos.
  • Gestionar grupos y permisos.
  • Implementar una funcionalidad de arrastrar y soltar para reordenar módulos y contenido.


Creación de un CMS

Ahora que has creado un modelo de datos versátil, vas a construir el CMS. El CMS permitirá a los instructores crear cursos y gestionar su contenido. Necesitas proporcionar las siguientes funcionalidades:

  • Listar los cursos creados por el instructor.
  • Crear, editar y eliminar cursos.
  • Agregar módulos a un curso y reordenarlos.
  • Agregar diferentes tipos de contenido a cada módulo.
  • Reordenar módulos y contenido del curso.

Comencemos con las vistas CRUD (Create, Read, Update, Delete) básicas.


Creación de vistas basadas en clases

Vasmos a construir vistas para crear, editar y eliminar cursos. Utilizaremos vistas basadas en clases para esto. Edita el archivo views.py de la aplicación de cursos y agrega el siguiente código:

from django.views.generic.list import ListView
from .models import Course
class ManageCourseListView(ListView):
    model = Course
    template_name = 'courses/manage/course/list.html'

    def
get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(owner=self.request.user)

Esta es la vista ManageCourseListView. Hereda de ListView genérico de Django. Sobreescribes el método get_queryset() de la vista para recuperar solo los cursos creados por el usuario actual. Para evitar que los usuarios editen, actualicen o eliminen cursos que no hayan creado, también necesitarás sobreescribir el método get_queryset() en las vistas de crear, actualizar y eliminar. Cuando necesites proporcionar un comportamiento específico para varias vistas basadas en clases, se recomienda que uses mixins.


Uso de mixins para vistas basadas en clases

Los mixins son un tipo especial de herencia múltiple para una clase. Puedes usarlos para proporcionar funcionalidad discreta común que, cuando se agrega a otros mixins, te permite definir el comportamiento de una clase. Hay dos situaciones principales para usar mixins:

  • Quieres proporcionar varias características opcionales para una clase.
  • Quieres usar una característica particular en varias clases.

Django viene con varios mixins que proporcionan funcionalidades adicionales a tus vistas basadas en clases. Puedes aprender más sobre mixins en https://docs.djangoproject.com/en/5.0/topics/class-basedviews/mixins/.

Vamos a implementar un comportamiento común para múltiples vistas en clases mixin y lo usarás para las vistas del curso. Edita el archivo views.py de la aplicación de cursos y modifícalo de la siguiente manera:

Vas a implementar un comportamiento común para múltiples vistas en clases mixin y usarlo para las vistas de curso. Edita el archivo views.py de la aplicación de cursos y modifícalo de la siguiente manera:


learning/educa/courses/views.py

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Course

class OwnerMixin:
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(owner=self.request.user)

class OwnerEditMixin:
    def form_valid(self, form):
        form.instance.owner = self.request.user
        return super().form_valid(form)

class OwnerCourseMixin(OwnerMixin):
    model = Course
    fields = ['subject', 'title', 'slug', 'overview']
    success_url = reverse_lazy('manage_course_list')

class OwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin):
    template_name = 'courses/manage/course/form.html'

class ManageCourseListView(OwnerCourseMixin, ListView):
    template_name = 'courses/manage/course/list.html'

class CourseCreateView(OwnerCourseEditMixin, CreateView):
    pass

class CourseUpdateView(OwnerCourseEditMixin, UpdateView):
    pass

class CourseDeleteView(OwnerCourseMixin, DeleteView):
    template_name = 'courses/manage/course/delete.html'

En este código se crean las clases mixin OwnerMixin y OwnerEditMixin. Usarás estas mixins junto con las vistas ListView, CreateView, UpdateView y DeleteView proporcionadas por Django. OwnerMixin implementa el método get_queryset(), que es utilizado por las vistas para obtener el QuerySet base. Tu mixin sobrescribirá este método para filtrar objetos por el atributo owner y así obtener objetos que pertenezcan al usuario actual (request.user).

OwnerEditMixin implementa el método form_valid(), que es utilizado por vistas que usan la mixin ModelFormMixin de Django, es decir, vistas con formularios o formularios de modelos como CreateView y UpdateView. form_valid() se ejecuta cuando el formulario enviado es válido. El comportamiento predeterminado para este método es guardar la instancia (para formularios de modelos) y redirigir al usuario a success_url. Sobreescribes este método para establecer automáticamente el usuario actual en el atributo owner del objeto que se está guardando. Al hacerlo, estableces automáticamente el propietario para un objeto cuando se guarda. Tu clase OwnerMixin se puede utilizar para vistas que interactúan con cualquier modelo que contenga un atributo owner. También defines una clase OwnerCourseMixin que hereda de OwnerMixin y proporciona los siguientes atributos para las vistas hijas:
  • model: El modelo utilizado para QuerySets; es utilizado por todas las vistas.
  • fields: Los campos del modelo para construir el formulario de modelo de las vistas CreateView y UpdateView.
  • success_url: Utilizado por CreateView, UpdateView y DeleteView para redirigir al usuario después de que el formulario se envía correctamente o el objeto se elimina. Utilizas una URL con el nombre manage_course_list, que crearás más tarde.
Defines una mixin OwnerCourseEditMixin con el siguiente atributo:
- template_name: La plantilla que utilizarás para las vistas CreateView y UpdateView. Finalmente, creas las siguientes vistas que heredan de OwnerCourseMixin:
  • ManageCourseListView: Lista los cursos creados por el usuario. Hereda de OwnerCourseMixin y ListView. Define un atributo template_name específico para una plantilla para listar cursos.
  • CourseCreateView: Utiliza un formulario de modelo para crear un nuevo objeto Course. Utiliza los campos definidos en OwnerCourseMixin para construir un formulario de modelo y también hereda de CreateView. Utiliza la plantilla definida en OwnerCourseEditMixin.
  • CourseUpdateView: Permite la edición de un objeto Course existente. Utiliza los campos definidos en OwnerCourseMixin para construir un formulario de modelo y también hereda de UpdateView. Utiliza la plantilla definida en OwnerCourseEditMixin.
  • CourseDeleteView: Hereda de OwnerCourseMixin y la DeleteView genérica. Define un atributo template_name específico para una plantilla para confirmar la eliminación del curso.
Con esto hemos creado las vistas básicas para gestionar los cursos. A continuación, vas a utilizar los grupos y permisos de autenticación de Django para limitar el acceso a estas vistas.

Trabajando con grupos y permisos


Actualmente, cualquier usuario puede acceder a las vistas para gestionar cursos, pero queremos restringir estas vistas para que solo los instructores tengan permiso para crear y gestionar cursos.

El marco de autenticación de Django incluye un sistema de permisos que te permite asignar permisos a usuarios y grupos. Vamos a crear un grupo para los usuarios instructores y asignar permisos para crear, actualizar y eliminar cursos.

Ejecuta el servidor de desarrollo utilizando el siguiente comando:

python manage.py runserver

Abre http://127.0.0.1:8000/admin/auth/group/add/ en tu navegador para crear un nuevo objeto de Grupo.

Agrega el nombre "Instructores" y elige todos los permisos de la aplicación de cursos, excepto aquellos del modelo Subject, de la siguiente manera:

The Instructors group permissions

Como puedes ver, hay cuatro permisos diferentes para cada modelo:

  1. can view
  2. can add
  3. can change
  4. can delete
Después de escoger los permisos para este grupo haz clic en el botón Guardar.

Django crea permisos para modelos automáticamente, pero también puedes crear permisos personalizados. Veremos como hacerlo más adelante. Puedes leer más sobre cómo agregar permisos personalizados en https://docs.djangoproject.com/en/5.0/topics/auth/customizing/#custom-permissions.

Ahora abre http://127.0.0.1:8000/admin/auth/user/add/ y crea un nuevo usuario. Edita el usuario y agrégalo al grupo "Instructores", de la siguiente manera:

User group selection

Los usuarios heredan los permisos de los grupos a los que pertenecen, pero también puedes agregar permisos individuales a un usuario único utilizando el sitio de administración. Los usuarios que tienen is_superuser configurado en True tienen automáticamente todos los permisos. Para restringir el acceso a las vistas basadas en clases, vamos a utilizar dos mixins proporcionados por django.contrib.auth para limitar el acceso a las vistas:

        - LoginRequiredMixin: Reproduce la funcionalidad del decorador login_required.         - PermissionRequiredMixin: Otorga acceso a la vista a usuarios con un permiso específico.

Recuerda que los superusuarios automáticamente tienen todos los permisos. Edita el archivo views.py de la aplicación de cursos y agrega la siguiente importación:


learning/educa/courses/views.py

from django.contrib.auth.mixins import LoginRequiredMixin, \
PermissionRequiredMixin

Haz que la clase OwnerCourseMixin herede de las clases LoginRequiredMixin y PermissionRequiredMixin, de esta forma:


learning/educa/courses/views.py

class OwnerCourseMixin(OwnerMixin, 
                       LoginRequiredMixin, PermissionRequiredMixin):
    model = Course
    fields = ['subject', 'title', 'slug', 'overview']
    success_url = reverse_lazy('manage_course_list')

Después añade el atributo permission_required a la vista course de la siguiente manera:


learning/educa/courses/views.py

class ManageCourseListView(OwnerCourseMixin, ListView):
    template_name = 'courses/manage/course/list.html'
    permission_required = 'courses.view_course'

class CourseCreateView(OwnerCourseEditMixin, CreateView):
    permission_required = 'courses.add_course'

class CourseUpdateView(OwnerCourseEditMixin, UpdateView):
    permission_required = 'courses.change_course'

class CourseDeleteView(OwnerCourseMixin, DeleteView):
    template_name = 'courses/manage/course/delete.html'
    permission_required = 'courses.delete_course'

PermissionRequiredMixin comprueba que el usuario que accede a la vista tiene los permisos que están especificados en el atributo permission_required. Con estos las vistas solo son accesibles a los usuarios que están autorizados a ello.

Creemos ahora los patrones URL´s para estas vistas. Crea un nuevo archivo dentro de la aplicación courses y llamalo urls.py. Luego añade el siguiente código:


learning/educa/courses/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('mine/',
        views.ManageCourseListView.as_view(),
        name='manage_course_list'),
    path('create/',
        views.CourseCreateView.as_view(),
        name='course_create'),
    path('<pk>/edit/',
        views.CourseUpdateView.as_view(),
        name='course_edit'),
    path('<pk>/delete/',
        views.CourseDeleteView.as_view(),
        name='course_delete'),
]
Estos son los patrones de URL para las vistas de listar, crear, editar y eliminar cursos. El parámetro pk se refiere al campo de clave primaria. Recuerda que pk es una abreviatura de clave primaria. Cada modelo de Django tiene un campo que sirve como su clave primaria. Por defecto, la clave primaria es el campo id generado automáticamente. Las vistas genéricas de Django para objetos individuales recuperan un objeto por su campo pk. Edita el archivo urls.py principal del proyecto educa e incluye los patrones de URL de la aplicación de cursos, de la siguiente manera.

El código nuevo aparece resaltado:

learning/educa/courses/urls.py

from django.contrib import admin
from django.urls import path, include
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),
    path('course/', include('courses.urls')),
]

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

Necesitamos crear las plantillas para estas vistas. Crea los siguientes archivos y directorios dentro del directorio templates/ de la aplicación course.

courses/
        manage/
                course/
                        list.html
                        form.html
                        delete.html


Edita el archivo de plantilla courses/manage/course/list.html y añádele el siguiente código:

Elearning/educa/courses/templates/courses/manage/course/list.html

{% extends "base.html" %}
{% block title %}My courses{% endblock %}
{% block content %}
<h1>My courses</h1>
<div class="module">
    {% for course in object_list %}
    <div class="course-info">
        <h3>{{ course.title }}</h3>
        <p>
            <a href="{% url 'course_edit' course.id %}">Edit</a>
            <a href="{% url 'course_delete' course.id %}">Delete</a>
            <a href="{% url 'course_module_update' course.id %}">Edit modules</a>
            {% if course.modules.count > 0 %}
            <a href="{% url 'module_content_list' course.modules.first.id %}">
                Manage contents
            </a>
            {% endif %}
        </p>
    </div>
    {% empty %}
    <p>You haven't created any courses yet.</p>
    {% endfor %}
    <p>
        <a href="{% url 'course_create' %}" class="button">Create new course</a>
    </p>
</div>
{% endblock %}
Esta es la plantilla para la vista ManageCourseListView. En esta plantilla, se listan los cursos creados por el usuario actual. Se incluyen los enlaces para editar o borrar cada curso y también para crear nuevos cursos.

Ejecuta el servidor de desarrollo de Django utilizando la instrucción:

python manage.py runserver

Abre la dirección http://127.0.0.1:8000/accounts/login/?next=/course/mine/ en tu navegador y logueate con un usuario que pertenezca al grupo de instructores, después de loguearte serás redirigido a la dirección http://127.0.0.1:8000/course/mine/ y verás la siguiente página:

The instructor courses page with no courses


Esta página mostrará los cursos creados por el actual usuario.

Creemos la plantilla que mostrará el formulario para crear y actualizar la vista course. Edita la plantilla courses/manage/course/form.html


Elearning/educa/courses/templates/courses/manage/course/form.html

{% extends "base.html" %}
{% block title %}
{% if object %}
Edit course "{{ object.title }}"
{% else %}
Create a new course
{% endif %}
{% endblock %}
{% block content %}
<h1>
    {% if object %}
    Edit course "{{ object.title }}"
    {% else %}
    Create a new course
    {% endif %}
</h1>
<div class="module">
    <h2>Course info</h2>
    <form method="post">
        {{ form.as_p }}
        {% csrf_token %}
        <p><input type="submit" value="Save course"></p>
    </form>
</div>
{% endblock %}

Esta plantilla se usa tanto por la vista CourseCreateView como por la CourseUpdateView. En esta plantilla, comprobamos si una variable de objeto está en el contexto. Si el objeto existe en el contexto, sabes que estás actualizando un curso existente y lo utilizas en el título de la página. De lo contrario, estás creando un nuevo objeto de Curso. Abre http://127.0.0.1:8000/course/mine/ en tu navegador y haz clic en el botón CREATE NEW COURSE. Verás la siguiente página:

The form to create a new course


Rellena el formulario y haz clic en el botón "SAVE COURSE". El curso se grabará y serás redirigido a la página de lista de cursos. Debería verse más o menos de la siguiente forma:

The instructor courses page with one course

Ahora haz clic en el enlace para editar el curso que acabas de crear. Verás el formulario de nuevo, pero está vez estarás editando un curso (objeto) ya existente en vez de crear uno.

Finalmente, edita la plantilla courses/manage/course/delete.html y añade el siguiente código:


Elearning/educa/courses/templates/courses/manage/course/delete.html

{% extends "base.html" %}
{% block title %}Delete course{% endblock %}
{% block content %}
<h1>Delete course "{{ object.title }}"</h1>
<div class="module">
    <form action="" method="post">
        {% csrf_token %}
        <p>Are you sure you want to delete "{{ object }}"?</p>
        <input type="submit" value="Confirm">
    </form>
</div>
{% endblock %}

Esta es la plantilla para la vista CourseDeleteView. Esta vista hereda de DeleteView, proporcionada por django, la cual espera que el usuario confirme borrar el objeto, en este caso el curso.

Vamos a probarla. Abre la lista de cursos en el navegador y haz clic en el enlace para borrar el curso. Deberías ver la siguiente página de confirmación:

The delete course confirmation page

Haz clic en el botón "CONFIRM". El curso se borrará y serás redirigido de nuevo a la página de lista de cursos.

Con esto los profesores o instructores pueden ya crear, editar y borrar los diferentes cursos. Ahora necesitamos que puedan añadir módulos a sus cursos y subir contenido a través del CMS. Vamos a ponernos con la forma de añadir módulos.


Gestionar módulos de curso y su contenido

Vamos a construir un sistema para gestionar los módulos del curso y su contenido. Necesitarás construir formularios que puedan ser utilizados para gestionar múltiples módulos por curso y diferentes tipos de contenido para cada módulo. Tanto los módulos como su contenido deberán seguir un orden específico y deberías poder reordenarlos utilizando el CMS.


Uso de formsets para módulos de curso

Django viene con una capa de abstracción para trabajar con múltiples formularios en la misma página. Estos grupos de formularios se conocen como formsets. Los formsets gestionan múltiples instancias de un determinado Form o ModelForm. Todos los formularios se envían a la vez y el formset se encarga del número inicial de formularios a mostrar, limitando el número máximo de formularios que se pueden enviar y validando todos los formularios.

Los formsets incluyen un método is_valid() para validar todos los formularios a la vez. También puedes proporcionar datos iniciales para los formularios y especificar cuántos formularios vacíos adicionales mostrar. Puedes aprender más sobre formsets en https://docs.djangoproject.com/en/5.0/topics/forms/formsets/ y sobre model formsets en https://docs.djangoproject.com/en/5.0/topics/forms/modelforms/#model-formsets.

Dado que un curso se divide en un número variable de módulos, tiene sentido usar formsets para gestionarlos. Crea un archivo forms.py en el directorio de la aplicación de cursos y agrega el siguiente código:


Elearning/educa/courses/forms.py

from django import forms
from django.forms.models import inlineformset_factory
from .models import Course, Module

ModuleFormSet = inlineformset_factory(Course,
                                      Module,
                                      fields=['title',
                                              'description'],
                                      extra=2,
                                      can_delete=True)

Este es el formset ModuleFormSet. Se construye utilizando la función inlineformset_factory() proporcionada por Django. Los formsets en línea son una pequeña abstracción sobre los formsets que simplifican el trabajo con objetos relacionados. Esta función te permite construir dinámicamente un formset de modelos para los objetos Module relacionados con un objeto Course.

Utilizas los siguientes parámetros para construir el formset:

- fields: Los campos que se incluirán en cada formulario del formset. - extra: Te permite establecer el número de formularios extra vacíos que se mostrarán en el formset. - can_delete: Si lo estableces en True, Django incluirá un campo booleano para cada formulario que se representará como un input de checkbox. Te permite marcar los objetos que deseas eliminar.

Edita el archivo views.py de la aplicación courses y agrega el siguiente código a él:


Elearning/educa/courses/views.py

from django.shortcuts import redirect, get_object_or_404
from django.views.generic.base import TemplateResponseMixin, View
from .forms import ModuleFormSet

class CourseModuleUpdateView(TemplateResponseMixin, View):
    template_name = 'courses/manage/module/formset.html'
    course = None

    def get_formset(self, data=None):
        return ModuleFormSet(instance=self.course, data=data)

    def dispatch(self, request, pk):
        self.course = get_object_or_404(Course, id=pk, owner=request.user)
        return super().dispatch(request, pk)
    
    def get(self, request, *args, **kwargs):
        formset = self.get_formset()
        return self.render_to_response({'course': self.course, 'formset': formset})
    
    def post(self, request, *args, **kwargs):
        formset = self.get_formset(data=request.POST)
        if formset.is_valid():
            formset.save()
            return redirect('manage_course_list')
        return self.render_to_response({'course': self.course, 'formset': formset})

La vista CourseModuleUpdateView maneja el formset para agregar, actualizar y eliminar módulos para un curso específico. Esta vista hereda de los siguientes mixins y vistas:

- TemplateResponseMixin: Este mixin se encarga de renderizar plantillas y devolver una respuesta HTTP. Requiere un atributo template_name que indica la plantilla a renderizar y proporciona el método render_to_response() para pasarle un contexto y renderizar la plantilla.

- View: La clase de vista básica proporcionada por Django.

En esta vista, implementas los siguientes métodos:

- get_formset(): Defines este método para evitar repetir el código para construir el formset. Creas un objeto ModuleFormSet para el objeto Course dado con datos opcionales.

- dispatch(): Este método es proporcionado por la clase View. Toma una solicitud HTTP y sus parámetros e intenta delegar a un método en minúsculas que coincida con el método HTTP utilizado. Una solicitud GET se delega al método get() y una solicitud POST a post(), respectivamente. En este método, utilizas la función de atajo get_object_or_404() para obtener el objeto Course para el parámetro de id dado que pertenece al usuario actual. Incluyes este código en el método dispatch() porque necesitas recuperar el curso tanto para las solicitudes GET como POST. Lo guardas en el atributo course de la vista para hacerlo accesible a otros métodos.

- get(): Ejecutado para solicitudes GET. Construyes un formset ModuleFormSet vacío y lo renderizas en la plantilla junto con el objeto Course actual utilizando el método render_to_response() proporcionado por TemplateResponseMixin.

- post(): Ejecutado para solicitudes POST.

- En este método, realizas las siguientes acciones:

1. Construyes una instancia de ModuleFormSet utilizando los datos enviados. 2. Ejecutas el método is_valid() del formset para validar todos sus formularios. 3. Si el formset es válido, lo guardas llamando al método save(). En este punto, se aplican todos los cambios realizados, como agregar, actualizar o marcar módulos para eliminar, a la base de datos. Luego, rediriges a los usuarios a la URL manage_course_list. Si el formset no es válido, renderizas la plantilla para mostrar cualquier error en su lugar.

Edita el archivo urls.py de la aplicación "courses" y añade el siguiente patrón URL:


Elearning/educa/courses/urls.py

path('<pk>/module/',
    views.CourseModuleUpdateView.as_view(),
    name='course_module_update'),

Crea un nuevo directorio dentro de la plantilla "courses/manage" y llamalé module. Crea esta plantilla courses/manage/module/formset.html y añade el siguiente código:


Elearning/educa/courses/templates/courses/manage/module/formset.html

{% extends "base.html" %}
{% block title %}
Edit "{{ course.title }}"
{% endblock %}
{% block content %}
<h1>Edit "{{ course.title }}"</h1>
<div class="module">
    <h2>Course modules</h2>
    <form method="post">
        {{ formset }}
        {{ formset.management_form }}
        {% csrf_token %}
        <input type="submit" value="Save modules">
    </form>
</div>
{% endblock %}

En esta plantilla, creas un elemento HTML <form> en el que incluyes el formset. También incluyes el formulario de gestión para el formset con la variable {{ formset.management_form }}. El formulario de gestión incluye campos ocultos para controlar el número inicial, total, mínimo y máximo de formularios. Puedes ver que es muy fácil crear un formset.

Edita la plantilla courses/manage/course/list.html y agrega el siguiente enlace para la URL course_module_update debajo de los enlaces Editar y Eliminar del curso:


Elearning/educa/courses/templates/courses/manage/course/list.html

            <a href="{% url 'course_edit' course.id %}">Edit</a>
            <a href="{% url 'course_delete' course.id %}">Delete</a>
            <a href="{% url 'course_module_update' course.id %}">Edit modules</a>            

Con esto ya hemos incluido el enlace para editar los módulos del curso.

Vamos a probarlo. Abre la dirección http://127.0.0.1:8000/course/mine/ en tu navegador. Crea un curso y haz clic en "Edit Modules". Deberías ver algo parecido a esto:

The course edit page, including the formset for course modules

El formset incluye un formulario para cada objeto Module contenido en el curso. Después de estos, se muestran dos formularios extra vacíos porque estableciste extra=2 para ModuleFormSet. Cuando guardas el formset, Django incluirá otros dos campos extra para agregar nuevos módulos.


Agregando contenido a los módulos del curso

Ahora, necesitas una forma de agregar contenido a los módulos del curso. Tienes cuatro tipos diferentes de contenido: texto, video, imagen y archivo. Podrías considerar crear cuatro vistas diferentes para crear contenido, con una vista para cada modelo. Sin embargo, vas a adoptar un enfoque más genérico y crear una vista que maneje la creación o actualización de los objetos de cualquier modelo de contenido.

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


Elearning/educa/courses/views.py

from django.forms.models import modelform_factory
from django.apps import apps
from .models import Module, Content

class ContentCreateUpdateView(TemplateResponseMixin, View):
    module = None
    model = None
    obj = None
    template_name = 'courses/manage/content/form.html'

    def get_model(self, model_name):
        if model_name in ['text', 'video', 'image', 'file']:
            return apps.get_model(app_label='courses', model_name=model_name)
        return None
    
    def get_form(self, model, *args, **kwargs):
        Form = modelform_factory(model, exclude=['owner', 'order', 'created', 'updated'])
        return Form(*args, **kwargs)
    
    def dispatch(self, request, module_id, model_name, id=None):
        self.module = get_object_or_404(Module, id=module_id, course__owner=request.user)
        self.model = self.get_model(model_name)
        if id:
            self.obj = get_object_or_404(self.model, id=id, owner=request.user)
        return super().dispatch(request, module_id, model_name, id)    

Esta es la primera parte de ContentCreateUpdateView. Te permitirá crear y actualizar contenidos de diferentes modelos. Esta vista define los siguientes métodos:

- get_model(): Aquí, verificas que el nombre del modelo dado sea uno de los cuatro modelos de contenido: Text, Video, Image o File. Luego, utilizas el módulo apps de Django para obtener la clase real para el nombre del modelo dado. Si el nombre del modelo dado no es uno de los válidos, devuelves None.

- get_form(): Construyes un formulario dinámico utilizando la función modelform_factory() del marco de trabajo de formularios. Dado que vas a construir un formulario para los modelos Text, Video, Image y File, utilizas el parámetro exclude para especificar los campos comunes a excluir del formulario y dejar que se incluyan automáticamente todos los demás atributos. Al hacerlo, no necesitas saber qué campos incluir dependiendo del modelo.

- dispatch(): Recibe los siguientes parámetros de URL y almacena el módulo correspondiente, el nombre del modelo y el objeto de contenido como atributos de clase: - module_id: El ID del módulo con el que el contenido está/estará asociado. - model_name: El nombre del modelo del contenido a crear/actualizar. - id: El ID del objeto que se está actualizando. Es None para crear nuevos objetos.

Agrega los siguientes métodos get() y post() a ContentCreateUpdateView:


Elearning/educa/courses/views.py

    def get(self, request, module_id, model_name, id=None):
        form = self.get_form(self.model, instance=self.obj)
        return self.render_to_response({'form': form, 'object': self.obj})
    
    def post(self, request, module_id, model_name, id=None):
        form = self.get_form(self.model, instance=self.obj, data=request.POST, files=request.FILES)
        if form.is_valid():
            obj = form.save(commit=False)
            obj.owner = request.user
            obj.save()
            if not id:
                # new content
                Content.objects.create(module=self.module, item=obj)
            return redirect('module_content_list', self.module.id)
        return self.render_to_response({'form': form, 'object': self.obj})

Estos métodos son los siguientes:

- get(): Se ejecuta cuando se recibe una solicitud GET. Construyes el formulario de modelo para la instancia de Texto, Video, Imagen o Archivo que se está actualizando. De lo contrario, no pasas ninguna instancia para crear un nuevo objeto, ya que self.obj es None si no se proporciona ningún ID.

- post(): Se ejecuta cuando se recibe una solicitud POST. Construyes el formulario de modelo, pasando cualquier dato y archivos enviados a él. Luego, lo validas. Si el formulario es válido, creas un nuevo objeto y asignas request.user como su propietario antes de guardarlo en la base de datos. Verificas el parámetro id. Si no se proporciona ningún ID, sabes que el usuario está creando un nuevo objeto en lugar de actualizar uno existente. Si se trata de un objeto nuevo, creas un objeto de Contenido para el módulo dado y lo asocias con el nuevo contenido.

Ahora, edita el archivo urls.py de la aplicación courses y añade el siguiente patrón URLs:


Elearning/educa/courses/urls.py

    path('module/<int:module_id>/content/<model_name>/create/',
         views.ContentCreateUpdateView.as_view(),
         name='module_content_create'),
    path('module/<int:module_id>/content/<model_name>/<id>/',
         views.ContentCreateUpdateView.as_view(),
         name='module_content_update'),   

Los nuevos patrones de URL son los siguientes:

- `module_content_create`: Para crear nuevos objetos de texto, vídeo, imagen o archivo y añadirlos a un módulo. Incluye los parámetros `module_id` y `model_name`. El primero permite vincular el nuevo objeto de contenido al módulo dado. El último especifica el modelo de contenido para construir el formulario.

- `module_content_update`: Para actualizar un objeto existente de texto, vídeo, imagen o archivo. Incluye los parámetros `module_id` y `model_name`, y un parámetro `id` para identificar el contenido que se está actualizando.

Crea un nuevo directorio dentro del directorio de plantillas `courses/manage/` y nómbralo `content`. Crea la plantilla `courses/manage/content/form.html` y añade el siguiente código a ella:


Elearning/educa/courses/templates/courses/manage/content/form.html

 {% extends "base.html" %}
{% block title %}
{% if object %}
Edit content "{{ object.title }}"
{% else %}
Add new content
{% endif %}
{% endblock %}
{% block content %}
<h1>
    {% if object %}
    Edit content "{{ object.title }}"
    {% else %}
    Add new content
    {% endif %}
</h1>
<div class="module">
    <h2>Course info</h2>
    <form action="" method="post" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <p><input type="submit" value="Save content"></p>
    </form>
</div>
{% endblock %} 

Esta es la plantilla para la vista `ContentCreateUpdateView`. En esta plantilla, verificas si una variable de objeto está en el contexto. Si el objeto existe en el contexto, estás actualizando un objeto existente. De lo contrario, estás creando un objeto nuevo.

Incluyes `enctype="multipart/form-data"` en el elemento HTML `<form>` porque el formulario contiene una carga de archivos para los modelos de contenido de Archivo e Imagen.

Ejecuta el servidor de desarrollo, abre http://127.0.0.1:8000/course/mine/, haz clic en "Editar módulos" para un curso existente y crea un módulo. Luego abre el shell de Python con el siguiente comando:

python manage.py shell

Obtén el Id del objeto de más reciente creación de la siguiente manera:

>>> from courses.models import Module

>>> Module.objects.latest('id').id

Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/course/module/4/content/image/create/ en tu navegador, reemplazando el ID del módulo con el que obtuviste antes. Verás el formulario para crear un objeto de Imagen, como sigue:

The course add new image content form


No envíes el formulario aún. Si tratas de hacerlo fallará, porque aún no hemos definido la URLs del módulo content_list. Lo haremos en un momento.

También necesitamos una vista para borrar el contenido. Edita el archivo views.py de la aplicación courses y añade el siguiente código:

Elearning/educa/courses/views.py

class ContentDeleteView(View):
    def post(self, request, id):
        content = get_object_or_404(Content, id=id, module__course__owner=request.user)
        module = content.module
        content.item.delete()
        content.delete()
        return redirect('module_content_list', module.id)
La clase ContentDeleteView recupera el objeto Content con el ID dado. Elimina el objeto relacionado de Texto, Video, Imagen o Archivo. Finalmente, elimina el objeto Content y redirige al usuario a la URL module_content_list para listar los otros contenidos del módulo.

Edita el archivo urls.py de la aplicación courses y agrega el siguiente patrón de URL:

Elearning/educa/courses/urls.py

path('content/<int:id>/delete/',
         views.ContentDeleteView.as_view(),
         name='module_content_delete'),

Con esto los instructores pueden crear, actualizar y borrar contenido fácilmente. 


Manejando los módulos y sus contenidos.

Has creado vistas para crear, editar y eliminar módulos de cursos y sus contenidos. A continuación, necesitas una vista para mostrar todos los módulos de un curso y listar los contenidos de un módulo específico.

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


Elearning/educa/courses/views.py

class ModuleContentListView(TemplateResponseMixin, View):
    template_name = 'courses/manage/module/content_list.html'

    def get(self, request, module_id):
        module = get_object_or_404(Module, id=module_id, course__owner=request.user)
        return self.render_to_response({'module': module})

Esta es la vista `ModuleContentListView`. Esta vista obtiene el objeto Módulo con el ID dado que pertenece al usuario actual y renderiza una plantilla con el módulo dado.

Edita el archivo urls.py de la aplicación "courses" y agrega el siguiente patrón de URL:


Elearning/educa/courses/urls.py

path('module/<int:module_id>/',
         views.ModuleContentListView.as_view(),
         name='module_content_list'),

Crea una nueva plantilla dentro del directorio templates/courses/manage/module/ y nómbrala content_list.html. Agrega el siguiente código:


Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% extends "base.html" %}

{% block title %}
Module {{ module.order|add:1 }}: {{ module.title }}
{% endblock %}

{% block content %}
{% with course=module.course %}
<h1>Course "{{ course.title }}"</h1>
<div class="contents">
    <h3>Modules</h3>
    <ul id="modules">
        {% for m in course.modules.all %}
        <li data-id="{{ m.id }}" {% if m == module %} 
        class="selected" {% endif %}>
            <a href="{% url 'module_content_list' m.id %}">
                <span>
                    Module <span class="order">{{ m.order|add:1 }}</span>
                </span>
                <br>
                {{ m.title }}
            </a>
        </li>
        {% empty %}
        <li>No modules yet.</li>
        {% endfor %}
    </ul>
    <p><a href="{% url 'course_module_update' course.id %}">
            Edit modules</a></p>
</div>
<div class="module">
    <h2>Module {{ module.order|add:1 }}: {{ module.title }}</h2>
    <h3>Module contents:</h3>
    <div id="module-contents">
        {% for content in module.contents.all %}
        <div data-id="{{ content.id }}">
            {% with item=content.item %}
            <p>{{ item }}</p>
            <a href="#">Edit</a> 
            <form action="{% url 'module_content_delete' content.id %}" method="post">
                <input type="submit" value="Delete">
                {% csrf_token %}
            </form>
            {% endwith %}
        </div>
        {% empty %}
        <p>This module has no contents yet.</p>
        {% endfor %}
    </div>
    <h3>Add new content:</h3>
    <ul class="content-types">
        <li>
            <a href="{% url 'module_content_create' module.id 'text' %}">
                Text
            </a>
        </li>
        <li>
            <a href="{% url 'module_content_create' module.id 'image' %}">
                Image
            </a>
        </li>
        <li>
            <a href="{% url 'module_content_create' module.id 'video' %}">
                Video
            </a>
        </li>
        <li>
            <a href="{% url 'module_content_create' module.id 'file' %}">
                File
            </a>
        </li>
    </ul>
</div>
{% endwith %}
{% endblock %}

Asegúrate de que ninguna etiqueta de plantilla se divida en varias líneas.

Esta es la plantilla que muestra todos los módulos de un curso y los contenidos del módulo seleccionado. Iteras sobre los módulos del curso para mostrarlos en una barra lateral. Iteras sobre los contenidos de un módulo y accedes a `content.item` para obtener el objeto relacionado de Texto, Vídeo, Imagen o Archivo. También incluyes enlaces para crear nuevo contenido de texto, vídeo, imagen o archivo.

Quieres saber qué tipo de objeto es cada uno de los objetos de `item`: Texto, Vídeo, Imagen o Archivo. Necesitas el nombre del modelo para construir la URL para editar el objeto. Además de esto, podrías mostrar cada elemento en la plantilla de manera diferente según el tipo de contenido que sea. Puedes obtener el nombre del modelo para un objeto desde la clase Meta del modelo accediendo al atributo `_meta` del objeto. Sin embargo, Django no permite acceder a variables o atributos que comienzan con un guión bajo en las plantillas para evitar recuperar atributos privados o llamar a métodos privados. Puedes resolver esto escribiendo un filtro de plantilla personalizado.

Crea la siguiente estructura de archivos dentro del directorio de la aplicación "courses":

templatetags/

       __init__.py

       course.py

Edita este nuevo archivo y añade el siguiente código:


Elearning/educa/courses/templatetags/course.py

from django import template

register = template.Library()

@register.filter
def model_name(obj):
    try:
        return obj._meta.model_name
    except AttributeError:
        return None

Este es el filtro de plantilla `model_name`. Puedes aplicarlo en las plantillas como `object|model_name` para obtener el nombre del modelo de un objeto.

Edita la plantilla `templates/courses/manage/module/content_list.html` y añade la siguiente línea debajo de la etiqueta de plantilla `{% extends %}`:

{% load course %}

Esto cargará las etiquetas de plantilla del curso. Luego, encuentra las siguientes líneas:

<p>{{ item }}</p>
<a href="#">Edit</a>
y remplázala por la siguiente:

<p>{{ item }} ({{ item|model_name }})</p>
<a href="{% url "module_content_update" module.id item|model_name item.id %}">
Edit
</a>
En el código anterior, muestras el nombre del modelo del elemento en la plantilla y también usas el nombre del modelo para construir el enlace para editar el objeto.

Edita la plantilla `courses/manage/course/list.html` y añade un enlace a la URL `module_content_list`, de esta manera:

<a href="{% url "course_module_update" course.id %}">Edit modules</a>
{% if course.modules.count > 0 %}
<a href="{% url "module_content_list" course.modules.first.id %}">
Manage contents
</a>
{% endif %}
El nuevo enlace permite a los usuarios acceder al contenido del primer módulo del curso, si lo hay.
Detén el servidor de desarrollo y ejecútalo nuevamente usando el comando:

```
python manage.py runserver
```

Al detener y ejecutar nuevamente el servidor de desarrollo, te aseguras de que el archivo de etiquetas de plantilla del curso se cargue.

Abre http://127.0.0.1:8000/course/mine/ y haz clic en el enlace "Gestionar contenidos" para un curso que contenga al menos un módulo. Verás una página como la siguiente:

The page to manage course module contents


Cuando haces clic en un módulo en la barra lateral izquierda, su contenido se muestra en el área principal. La plantilla también incluye enlaces para agregar nuevo contenido de texto, vídeo, imagen o archivo para el módulo que se está mostrando.

Agrega un par de tipos diferentes de contenido al módulo y observa el resultado. El contenido del módulo aparecerá debajo de "Contenidos del módulo".

Managing different module contents


Reordenando los módulos y sus contenidos.


Para permitir a los instructores del curso reordenar módulos y sus contenidos usando la funcionalidad de arrastrar y soltar, implementaremos una funcionalidad de arrastrar y soltar en JavaScript que les permita reordenar los módulos de un curso arrastrándolos.

Para implementar esta función, utilizaremos la biblioteca HTML5 Sortable, que simplifica el proceso de crear listas ordenables utilizando la API nativa HTML5 de Arrastrar y Soltar.

Cuando los usuarios terminen de arrastrar un módulo, utilizaremos la API Fetch de JavaScript para enviar una solicitud HTTP asíncrona al servidor que almacenará el nuevo orden de los módulos.

Puedes encontrar más información sobre la API de Arrastrar y Soltar de HTML5 en https://www.w3schools.com/html/html5_draganddrop.asp. También puedes encontrar ejemplos construidos con la biblioteca HTML5 Sortable en https://lukasoppermann.github.io/html5sortable/. La documentación para la biblioteca HTML5 Sortable está disponible en https://github.com/lukasoppermann/html5sortable.

Utilizando mixins de django-braces


django-braces es un módulo de terceros que contiene una colección de mixins genéricos para Django. Estos mixins proporcionan características adicionales para las vistas basadas en clases. Puedes ver una lista de todos los mixins proporcionados por django-braces en https://django-braces.readthedocs.io/.

Utilizaremos los siguientes mixins de django-braces:

- 'CsrfExemptMixin': Se utiliza para evitar comprobar el token de protección contra falsificación de solicitudes entre sitios (CSRF) en las solicitudes POST. Necesitas esto para realizar solicitudes POST AJAX sin necesidad de pasar un csrf_token.

- 'JsonRequestResponseMixin': Analiza los datos de la solicitud como JSON y también serializa la respuesta como JSON y devuelve una respuesta HTTP con el tipo de contenido application/json.

Instala django-braces a través de pip utilizando el siguiente comando:

```
pip install django-braces
```

Necesitas una vista que reciba el nuevo orden de los IDs de módulo codificados en JSON y actualice el orden en consecuencia. Edita el archivo views.py de la aplicación "courses" y agrega el siguiente código a él:

Elearning/educa/courses/views.py

from braces.views import CsrfExemptMixin, JsonRe

class ModuleOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):
    def post(self, request):
        for id, order in self.request_json.items():
            Module.objects.filter(
                id=id, course__owner=request.user).update(order=order)
        return self.render_json_response({'saved': 'OK'})
Esta es la vista ModuleOrderView, la cual nos va a permitir actualizar el orden de los cursos del módulo. Podemos construir una vista similar para cambiar el orden de los contenidos del módulo. Añade al archivo anterior el siguiente código.

Elearning/educa/courses/views.py

class ContentOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):
    def post(self, request):
        for id, order in self.request_json.items():
            Content.objects.filter(
                id=id, module__course__owner=request.user).update(order=order)
        return self.render_json_response({'saved': 'OK'})
A continuación edita el archivo urls.py de la aplicación courses y añade el siguiente patrón URLs:

Elearning/educa/courses/urls.py

path('module/order/',
         views.ModuleOrderView.as_view(),
         name='module_order'),
path('content/order/',
         views.ContentOrderView.as_view(),
         name='content_order'),
Claro, aquí tienes la traducción al español:

Finalmente, necesitas implementar la funcionalidad de arrastrar y soltar en la plantilla. Utilizaremos la biblioteca HTML5 Sortable, la cual simplifica la creación de elementos ordenables utilizando la API estándar de arrastrar y soltar de HTML.

Edita la plantilla base.html ubicada en el directorio templates/ de la aplicación de courses y añade el siguiente bloque resaltado en azul:

Elearning/educa/courses/templates/base.html

{% 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 que no sea un botón hay que usar css. Código para disimular el 
        botón como un enlace en base.css #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>
    {% block include_js %}
    {% endblock %}
    <script>
        document.addEventListener('DOMContentLoaded', (event) => {
            // DOM loaded
            {% block domready %}
            {% endblock %}
        })
    </script>

</body>

</html>
Este nuevo bloque llamado include_js te permitirá insertar archivos JavaScript en cualquier plantilla que extienda la plantilla base.html.

A continuación, edita la plantilla courses/manage/module/content_list.html y añade el siguiente código resaltado en azul al final de la plantilla:

Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% block content %}
#...
{% endblock %}

{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/
html5sortable.min.js"></script>
{% endblock %}
En este código, cargas la biblioteca HTML5 Sortable desde un CDN público. Ahora agrega el siguiente bloque domready resaltado en negrita a la plantilla courses/manage/module/content_list.html:

Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% block content %}
#...
{% endblock %}

{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/
html5sortable.min.js"></script>
{% endblock %}

{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';
{% endblock %}
En estas nuevas líneas, agregas código JavaScript al bloque {% block domready %} que fue definido en el escuchador de eventos para el evento DOMContentLoaded en la plantilla base.html. Esto garantiza que tu código JavaScript se ejecutará una vez que la página se haya cargado. Con este código, defines las opciones para la solicitud HTTP para reordenar los módulos que implementarás a continuación. Enviarás una solicitud POST usando la Fetch API para actualizar el orden de los módulos. La ruta URL module_order se construye y almacena en la constante JavaScript moduleOrderUrl.

Agrega el siguiente código resaltado en negrita al bloque domready:

Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% block content %}
#...
{% endblock %}

{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/
html5sortable.min.js"></script>
{% endblock %}

{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';

sortable('#modules', {
    forcePlaceholderSize: true,
    placeholderClass: 'placeholder'
    });
{% endblock %}
En el nuevo código, defines un elemento ordenable para el elemento HTML con id="modules", que es la lista de módulos en la barra lateral. Recuerda que usas un selector CSS # para seleccionar el elemento con el id dado. Cuando comienzas a arrastrar un elemento, la biblioteca HTML5 Sortable crea un elemento de marcador de posición para que puedas ver fácilmente dónde se colocará el elemento.

Estableces la opción forcePlacehoderSize en true, para forzar al elemento de marcador de posición a tener una altura, y utilizas placeholderClass para definir la clase CSS para el elemento de marcador de posición. Utilizas la clase llamada placeholder que está definida en el archivo estático css/base.css cargado en la plantilla base.html.

Abre http://127.0.0.1:8000/course/mine/ en tu navegador y haz clic en "Administrar contenidos" para cualquier curso. Ahora puedes arrastrar y soltar los módulos del curso en la barra lateral izquierda, como se muestra a continuación:

Reordering modules with the drag-and-drop functionality


Mientras arrastras el elemento, verás el elemento de marcador de posición creado por la biblioteca Sortable, el cual tiene un borde punteado. El elemento de marcador de posición te permite identificar la posición donde se soltará el elemento arrastrado.

Cuando arrastras un módulo a una posición diferente, necesitas enviar una solicitud HTTP al servidor para almacenar el nuevo orden. Esto se puede hacer adjuntando un controlador de eventos al elemento ordenable y enviando una solicitud al servidor utilizando la Fetch API de JavaScript.

Edita el bloque domready de la plantilla courses/manage/module/content_list.html y agrega el siguiente código resaltado en azul:

Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% block content %}
#...
{% endblock %}

{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/
html5sortable.min.js"></script>
{% endblock %}

{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';

sortable('#modules', {
    forcePlaceholderSize: true,
    placeholderClass: 'placeholder'
    })[0].addEventListener('sortupdate', function(e) {
        modulesOrder = {};
        var modules = document.querySelectorAll('#modules li');
        modules.forEach(function (module, index) {
        // update module index
        modulesOrder[module.dataset.id] = index;
        // update index in HTML element
        module.querySelector('.order').innerHTML = index + 1;
        // add new order to the HTTP request options
        options['body'] = JSON.stringify(modulesOrder);
        // send HTTP request
        fetch(moduleOrderUrl, options)
        });
        });
{% endblock %}
En el nuevo código, se crea un escuchador de eventos para el evento sortupdate del elemento ordenable.

El evento sortupdate se activa cuando un elemento se suelta en una posición diferente. Las siguientes tareas se realizan en la función del evento:

1. Se crea un diccionario vacío modulesOrder. Las claves para este diccionario serán los ID de los módulos, y los valores contendrán el índice de cada módulo.

2. Los elementos de lista del elemento HTML #modules se seleccionan con document.querySelectorAll(), utilizando el selector CSS #modules li.

3. forEach() se utiliza para iterar sobre cada elemento de lista.

4. El nuevo índice para cada módulo se almacena en el diccionario modulesOrder. El ID de cada módulo se recupera del atributo data-id HTML accediendo a module.dataset.id. Se utiliza el ID como clave del diccionario modulesOrder y el nuevo índice del módulo como valor.

5. Se actualiza el orden mostrado para cada módulo seleccionando el elemento con la clase CSS order. Dado que el índice es de base cero y queremos mostrar un índice de base uno, sumamos 1 al índice.

6. Se añade una clave llamada body al diccionario options con el nuevo orden contenido en modulesOrder. El método JSON.stringify() convierte el objeto JavaScript en una cadena JSON. Este es el cuerpo para la solicitud HTTP para actualizar el orden de los módulos.

7. Se utiliza la Fetch API creando una solicitud HTTP fetch() para actualizar el orden de los módulos. La vista ModuleOrderView que corresponde a la URL module_order se encarga de actualizar el orden de los módulos.

Ahora puedes arrastrar y soltar módulos. Cuando terminas de arrastrar un módulo, se envía una solicitud HTTP a la URL module_order para actualizar el orden de los módulos. Si actualizas la página, el último orden de los módulos se mantendrá porque se actualizó en la base de datos. La siguiente imagen muestra un orden diferente para los módulos en la barra lateral después de ordenarlos usando arrastrar y soltar.

New order for modules after reordering them with drag and drop

Si encuentras algún problema, recuerda utilizar las herramientas de desarrollo del navegador para depurar JavaScript y solicitudes HTTP. Por lo general, puedes hacer clic derecho en cualquier lugar del sitio web para abrir el menú contextual y luego hacer clic en "Inspeccionar" o "Inspeccionar elemento" para acceder a las herramientas de desarrollo web de tu navegador.

Ahora, para añadir la misma funcionalidad de arrastrar y soltar para permitir a los instructores de cursos ordenar el contenido del módulo, edita el bloque domready de la plantilla courses/manage/module/content_list.html y agrega el siguiente código resaltado en azul:

Elearning/educa/courses/templates/courses/manage/module/content_list.html

{% block domready %}
// ...
const contentOrderUrl = '{% url "content_order" %}';
        sortable('#module-contents', {
        forcePlaceholderSize: true,
        placeholderClass: 'placeholder'
        })[0].addEventListener('sortupdate', function(e) {
        contentOrder = {};
        var contents = document.querySelectorAll('#module-contents div');
        contents.forEach(function (content, index) {
        // update content index
        contentOrder[content.dataset.id] = index;
        // add new order to the HTTP request options
        options['body'] = JSON.stringify(contentOrder);
        // send HTTP request
        fetch(contentOrderUrl, options)
        });
        });
{% endblock %}

En este caso, utilizas la URL content_order en lugar de module_order y construyes la funcionalidad ordenable en el elemento HTML con el ID module-contents. La funcionalidad es principalmente la misma que para ordenar los módulos del curso. En este caso, no necesitas actualizar la numeración de los contenidos porque no incluyen ningún índice visible.

Ahora puedes arrastrar y soltar tanto módulos como contenidos del módulo, como se muestra en la siguiente imagen.

Reordering module contents with the drag-and-drop functionality


Puedes encontrar el código de este post en la siguiente dirección de GITLAB.



No hay comentarios:

Publicar un comentario