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'
- 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.
- 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.
Trabajando con grupos y permisos
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:
Como puedes ver, hay cuatro permisos diferentes para cada modelo:
- can view
- can add
- can change
- can delete
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'), ]
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)
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 %}
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:
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:
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:
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:
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
4
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:
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.
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)
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>
<p>{{ item }} ({{ item|model_name }})</p> <a href="{% url "module_content_update" module.id item|model_name item.id %}"> Edit </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 %}
Reordenando los módulos y sus contenidos.
Utilizando mixins de django-braces
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'})
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'})
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'),
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>
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 %}
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 %}
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 %}
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 %}
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 %}
No hay comentarios:
Publicar un comentario