Creating a content management system

Now that we have created a versatile data model, we are going to build a content management system (CMS). The CMS will allow instructors to create courses and manage their content. We need to provide the following functionality:

  • Login to the CMS.
  • List the courses created by the instructor.
  • Create, edit, and delete courses.
  • Add modules to a course and re-order them.
  • Add different types of content to each module and re-order contents.

Adding the authentication system

We are going to use Django's authentication framework in our platform. Both instructors and students will be an instance of Django's User model. Thus, they will be able to login to the site using the authentication views of django.contrib.auth.

Edit the main urls.py file of the educa project and include the login and logout views of Django's authentication framework:

from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.auth import views as auth_views

urlpatterns = [
    url(r'^accounts/login/$', auth_views.login, name='login'),
    url(r'^accounts/logout/$', auth_views.logout, name='logout'),
    url(r'^admin/', include(admin.site.urls)),
]

Creating the authentication templates

Create the following file structure inside the courses application directory:

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

Before building the authentication templates, we need to prepare the base template for our project. Edit the base.html template file and add the following content to it:

{% load staticfiles %}
<!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><a href="{% url "logout" %}">Sign out</a></li>
      {% else %}
        <li><a href="{% url "login" %}">Sign in</a></li>
      {% endif %}
    </ul>
  </div>
  <div id="content">
    {% block content %}
    {% endblock %}
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  <script>
    $(document).ready(function() {
      {% block domready %}
      {% endblock %}
    });
  </script>
</body>
</html>

This is the base template that will be extended by the rest of the templates. In this template, we define the following blocks:

  • title: The block for other templates to add a custom title for each page.
  • content: The main block for content. All templates that extend the base template should add content to this block.
  • domready: Located inside the $document.ready() function of jQuery. It allows us to execute code when the DOM has finished loading.

The CSS styles used in this template are located in the static/ directory of the courses application, in the code that comes along with this chapter. You can copy the static/ directory into the same directory of your project to use them.

Edit the registration/login.html template and add the following code to it:

{% extends "base.html" %}

{% block title %}Log-in{% endblock %}

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

This is a standard login template for Django's login view. Edit the registration/logged_out.html template and add the following code to it:

{% extends "base.html" %}

{% block title %}Logged out{% endblock %}

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

This is the template that will be displayed to the user after logout. Run the development server with the command python manage.py runserver and open http://127.0.0.1:8000/accounts/login/ in your browser. You should see the login page like this:

Creating the authentication templates

Creating class-based views

We are going to build views to create, edit, and delete courses. We will use class-based views for this. Edit the views.py file of the courses application and add the following code to it:

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(ManageCourseListView, self).get_queryset()
        return qs.filter(owner=self.request.user)

This is the ManageCourseListView view. It inherits from Django's generic ListView. We override the get_queryset() method of the view to retrieve only courses created by the current user. To prevent users from editing, updating, or deleting courses they didn't create, we will also need to override the get_queryset() method in the create, update, and delete views. When you need to provide a specific behavior for several class-based views, it is recommended to use mixins.

Using mixins for class-based views

Mixins are a special kind of multiple inheritance for a class. You can use them to provide common discrete functionality that, added to other mixins, allows you to define the behavior of a class. There are two main situations to use mixins:

  • You want to provide multiple optional features for a class
  • You want to use a particular feature in several classes

You can find documentation about how to use mixins with class-based views at https://docs.djangoproject.com/en/1.8/topics/class-based-views/mixins/.

Django comes with several mixins that provide additional functionality to your class-based views. You can find all mixins at https://docs.djangoproject.com/en/1.8/ref/class-based-views/mixins/.

We are going to create a mixin class that includes a common behavior and use it for the course's views. Edit the views.py file of the courses application and modify it as follows:

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

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

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

class OwnerCourseMixin(OwnerMixin):
    model = Course

class OwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin):
    fields = ['subject', 'title', 'slug', 'overview']
    success_url = reverse_lazy('manage_course_list')
    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'
    success_url = reverse_lazy('manage_course_list')

In this code, we create the OwnerMixin and OwnerEditMixin mixins. We will use these mixins together with the ListView, CreateView, UpdateView, and DeleteView views provided by Django. OwnerMixin implements the following method:

  • get_queryset(): This method is used by the views to get the base QuerySet. Our mixin will override this method to filter objects by the owner attribute to retrieve objects that belong to the current user (request.user).

OwnerEditMixin implements the following method:

  • form_valid(): This method is used by views that use Django's ModelFormMixin mixin, i.e., views with forms or modelforms such as CreateView and UpdateView. form_valid() is executed when the submitted form is valid. The default behavior for this method is saving the instance (for modelforms) and redirecting the user to success_url. We override this method to automatically set the current user in the owner attribute of the object being saved. By doing so, we set the owner for an object automatically when it is saved.

Our OwnerMixin class can be used for views that interact with any model that contains an owner attribute.

We also define an OwnerCourseMixin class that inherits OwnerMixin and provides the following attribute for child views:

  • model: The model used for QuerySets. Used by all views.

We define a OwnerCourseEditMixin mixin with the following attributes:

  • fields: The fields of the model to build the model form of the CreateView and UpdateView views.
  • success_url: Used by CreateView and UpdateView to redirect the user after the form is successfully submitted. We use a URL with the name manage_course_list that we are going to create later.

Finally, we create the following views that subclass OwnerCourseMixin:

  • ManageCourseListView: Lists the courses created by the user. It inherits from OwnerCourseMixin and ListView.
  • CourseCreateView: Uses a modelform to create a new Course object. It uses the fields defined in OwnerCourseEditMixin to build a model form and also subclasses CreateView.
  • CourseUpdateView: Allows editing an existing Course object. It inherits from OwnerCourseEditMixin and UpdateView.
  • CourseDeleteView: Inherits from OwnerCourseMixin and the generic DeleteView. Defines success_url to redirect the user after the object is deleted.

Working with groups and permissions

We have created the basic views to manage courses. Currently, any user could access these views. We want to restrict these views so that only instructors have permission to create and manage courses. Django's authentication framework includes a permission system that allows you to assign permissions to users and groups. We are going to create a group for instructor users and assign permissions to create, update, and delete courses.

Run the development server using the command python manage.py runserver and open http://127.0.0.1:8000/admin/auth/group/add/ in your browser to create a new Group object. Add the name Instructors and choose all permissions of the courses application except those of the Subject model as follows:

Working with groups and permissions

As you can see, there are three different permissions for each model: Can add, can change, and Can delete. After choosing permissions for this group, click the Save button.

Django creates permissions for models automatically, but you can also create custom permissions. You can read more about adding custom permissions at https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#custom-permissions.

Open http://127.0.0.1:8000/admin/auth/user/add/ and create a new user. Edit the user and add the Instructors group to it as follows:

Working with groups and permissions

Users inherit the permissions of the groups they belong to, but you can also add individual permissions to a single user using the administration site. Users that have is_superuser set to True have all permissions automatically.

Restricting access to class-based views

We are going to restrict access to the views so that only users with the appropriate permissions can add, change, or delete Course objects. The authentication framework includes a permission_required decorator to restrict access to views. Django 1.9 will include permission mixins for class-based views. However, Django 1.8 does not include them. Therefore, we are going to use permission mixins provided by a third-party module named django-braces.

Using mixins from django-braces

Django-braces is a third-party module that contains a collection of generic mixins for Django. These mixins provide additional features for class-based views. You can see a list of all mixins provided by django-braces at http://django-braces.readthedocs.org/en/latest/.

Install django-braces via pip using the command:

pip install django-braces==1.8.1

We are going to use the following two mixins from django-braces to limit access to views:

  • LoginRequiredMixin: Replicates the login_required decorator's functionality.
  • PermissionRequiredMixin: Grants access to the view to users that have a specific permission. Remember that superusers automatically have all permissions.

Edit the views.py file of the courses application and add the following import:

from braces.views import LoginRequiredMixin, 
                         PermissionRequiredMixin

Make OwnerCourseMixin inherit LoginRequiredMixin like this:

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

Then, add a permission_required attribute to the create, update, and delete views, as follows:

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

class CourseUpdateView(PermissionRequiredMixin,
                       OwnerCourseEditMixin,
                       UpdateView):
    template_name = 'courses/manage/course/form.html'
    permission_required = 'courses.change_course'

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

PermissionRequiredMixin checks that the user accessing the view has the permission specified in the permission_required attribute. Our views are now only accessible to users that have proper permissions.

Let's create URLs for these views. Create a new file inside the courses application directory and name it urls.py. Add the following code to it:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^mine/$',
        views.ManageCourseListView.as_view(), 
        name='manage_course_list'),
    url(r'^create/$',
        views.CourseCreateView.as_view(),
        name='course_create'),
    url(r'^(?P<pk>d+)/edit/$',
        views.CourseUpdateView.as_view(),
        name='course_edit'),
    url(r'^(?P<pk>d+)/delete/$', 
        views.CourseDeleteView.as_view(),
        name='course_delete'),
]

These are the URL patterns for the list, create, edit, and delete course views. Edit the main urls.py file of the educa project and include the URL patterns of the courses application as follows:

urlpatterns = [
    url(r'^accounts/login/$', auth_views.login, name='login'),
    url(r'^accounts/logout/$', auth_views.logout, name='logout'),
    url(r'^admin/', include(admin.site.urls)),
    url(r'^course/', include('courses.urls')),
]

We need to create the templates for these views. Create the following directories and files inside the templates/ directory of the courses application:

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

Edit the courses/manage/course/list.html template and add the following code to it:

{% 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>
        </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 %}

This is the template for the ManageCourseListView view. In this template, we list the courses created by the current user. We include links to edit or delete each course, and a link to create new courses.

Run the development server using the command python manage.py runserver. Open http://127.0.0.1:8000/accounts/login/?next=/course/mine/ in your browser and log in with a user that belongs to the Instructors group. After logging in, you will be redirected to the URL http://127.0.0.1:8000/course/mine/ and you should see the following page:

Using mixins from django-braces

This page will display all courses created by the current user.

Let's create the template that displays the form for the create and update course views. Edit the courses/manage/course/form.html template and write the following code:

{% 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 action="." method="post">
      {{ form.as_p }}
      {% csrf_token %}
      <p><input type="submit" value="Save course"></p>
    </form>
  </div>
{% endblock %}

The form.html template is used for both the CourseCreateView and CourseUpdateView views. In this template, we check if an object variable is in the context. If object exists in the context, we know that we are updating an existing course, and we use it in the page title. Otherwise, we are creating a new Course object.

Open http://127.0.0.1:8000/course/mine/ in your browser and click the Create new course button. You will see the following page:

Using mixins from django-braces

Fill in the form and click the Save course button. The course will be saved and you will be redirected to the course list page. It should look as follows:

Using mixins from django-braces

Then, click the Edit link for the course you have just created. You will see the form again, but this time you are editing an existing Course object instead of creating one.

Finally, edit the courses/manage/course/delete.html template and add the following code:

{% 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" class"button" value="Confirm">
    </form>
  </div>
{% endblock %}

This is the template for the CourseDeleteView view. This view inherits from DeleteView provided by Django, which expects user confirmation to delete an object.

Open your browser and click the Delete link of your course. You should see the following confirmation page:

Using mixins from django-braces

Click the CONFIRM button. The course will be deleted and you will be redirected to the course list page again.

Instructors can now create, edit, and delete courses. Next, we need to provide them with a content management system to add modules and contents to courses. We will start by managing course modules.

Using formsets

Django comes with an abstraction layer to work with multiple forms on the same page. These groups of forms are known as formsets. Formsets manage multiple instances of certain Form or ModelForm. All forms are submitted at once and the formset takes care of things like the initial number of forms to display, limiting the maximum number of forms that can be submitted, and validating all forms.

Formsets include an is_valid() method to validate all forms at once. You can also provide initial data for the forms and specify how many additional empty forms to display.

You can learn more about formsets at https://docs.djangoproject.com/en/1.8/topics/forms/formsets/, and about model formsets at https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#model-formsets.

Managing course modules

Since a course is divided into a variable number of modules, it makes sense to use formsets here. Create a forms.py file in the courses application directory and add the following code to it:

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)

This is the ModuleFormSet formset. We build it using the inlineformset_factory() function provided by Django. Inline formsets is a small abstraction on top of formsets that simplifies working with related objects. This function allows us to build a model formset dynamically for the Module objects related to a Course object.

We use the following parameters to build the formset:

  • fields: The fields that will be included in each form of the formset.
  • extra: Allows us to set up the number of empty extra forms to display in the formset.
  • can_delete: If you set this to True, Django will include a Boolean field for each form that will be rendered as a checkbox input. It allows you to mark the objects you want to delete.

Edit the views.py file of the courses application and add the following code to it:

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(CourseModuleUpdateView, 
                     self).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})

The CourseModuleUpdateView view handles the formset to add, update, and delete modules for a specific course. This view inherits from the following mixins and views:

  • TemplateResponseMixin: This mixin takes charge of rendering templates and returning an HTTP response. It requires a template_name attribute that indicates the template to be rendered and provides the render_to_response() method to pass it a context and render the template.
  • View: The basic class-based view provided by Django.

In this view, we implement the following methods:

  • get_formset(): We define this method to avoid repeating the code to build the formset. We create a ModuleFormSet object for the given Course object with optional data.
  • dispatch(): This method is provided by the View class. It takes an HTTP request and its parameters and attempts to delegate to a lowercase method that matches the HTTP method used: A GET request is delegated to the get() method and a POST request to post() respectively. In this method, we use the get_object_or_404() shortcut function to get the Course object for the given id parameter that belongs to the current user. We include this code in the dispatch() method because we need to retrieve the course for both GET and POST requests. We save it into the course attribute of the view to make it accessible to other methods.
  • get(): Executed for GET requests. We build an empty ModuleFormSet formset and render it to the template together with the current Course object using the render_to_response() method provided by TemplateResponseMixin.
  • post(): Executed for POST requests. In this method, we perform the following actions:
    1. We build a ModuleFormSet instance using the submitted data.
    2. We execute the is_valid() method of the formset to validate all of its forms.
    3. If the formset is valid, we save it by calling the save() method. At this point, any changes made, such as adding, updating, or marking modules for deletion, are applied to the database. Then, we redirect users to the manage_course_list URL. If the formset is not valid, we render the template to display any errors instead.

Edit the urls.py file of the courses application and add the following URL pattern to it:

url(r'^(?P<pk>d+)/module/$', 
    views.CourseModuleUpdateView.as_view(),     
    name='course_module_update'),

Create a new directory inside the courses/manage/ template directory and name it module. Create a courses/manage/module/formset.html template and add the following code to it:

{% extends "base.html" %}

{% block title %}
  Edit "{{ course.title }}"
{% endblock %}

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

In this template, we create a <form> HTML element, in which we include our formset. We also include the management form for the formset with the variable {{ formset.management_form }}. The management form includes hidden fields to control the initial, total, minimum, and maximum number of forms. As you can see, it's very easy to create a formset.

Edit the courses/manage/course/list.html template and add the following link for the course_module_update URL below the course edit and delete links:

<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>

We have included the link to edit the course modules. Open http://127.0.0.1:8000/course/mine/ in your browser and click the Edit modules link for a course. You should see a formset as follows:

Managing course modules

The formset includes a form for each Module object contained in the course. After these, two empty extra forms are displayed because we set extra=2 for ModuleFormSet. When you save the formset, Django will include another two extra fields to add new modules.

Adding content to course modules

Now, we need a way to add content to course modules. We have four different types of content: Text, Video, Image, and File. We can consider creating four different views to create content, one for each model. Yet, we are going to take a more generic approach and create a view that handles creating or updating objects of any content model.

Edit the views.py file of the courses application and add the following code to it:

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(ContentCreateUpdateView,  
           self).dispatch(request, module_id, model_name, id)

This is the first part of ContentCreateUpdateView. It will allow us to create and update contents of different models. This view defines the following methods:

  • get_model(): Here, we check that the given model name is one of the four content models: text, video, image, or file. Then we use Django's apps module to obtain the actual class for the given model name. If the given model name is not one of the valid ones, we return None.
  • get_form(): We build a dynamic form using the modelform_factory() function of the form's framework. Since we are going to build a form for the Text, Video, Image, and File models, we use the exclude parameter to specify the common fields to exclude from the form and let all other attributes be included automatically. By doing so, we don't have to know which fields to include depending on the model.
  • dispatch(): It receives the following URL parameters and stores the corresponding module, model, and content object as class attributes:
    • module_id: The id for the module that the content is/will be associated with.
    • model_name: The model name of the content to create/update.
    • id: The id of the object that is being updated. It's None to create new objects.

Add the following get() and post() methods to ContentCreateUpdateView:

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})

These methods are as follows:

  • get(): Executed when a GET request is received. We build the model form for the Text, Video, Image, or File instance that is being updated. Otherwise, we pass no instance to create a new object, since self.obj is None if no id is provided.
  • post(): Executed when a POST request is received. We build the modelform passing any submitted data and files to it. Then we validate it. If the form is valid, we create a new object and assign request.user as its owner before saving it to the database. We check for the id parameter. If no id is provided, we know the user is creating a new object instead of updating an existing one. If this is a new object, we create a Content object for the given module and associate the new content to it.

Edit the urls.py file of the courses application and add the following URL patterns to it:

url(r'^module/(?P<module_id>d+)/content/(?P<model_name>w+)/create/$',
    views.ContentCreateUpdateView.as_view(),
    name='module_content_create'),
url(r'^module/(?P<module_id>d+)/content/(?P<model_name>w+)/(?P<id>d+)/$',
    views.ContentCreateUpdateView.as_view(), 
    name='module_content_update'),

The new URL patterns are:

  • module_content_create: To create new text, video, image, or file objects and adding them to a module. It includes the module_id and model_name parameters. The first one allows linking the new content object to the given module. The latter specifies the content model to build the form for.
  • module_content_update: To update an existing text, video, image, or file object. It includes the module_id and model_name parameters, and an id parameter to identify the content that is being updated.

Create a new directory inside the courses/manage/ template directory and name it content. Create the template courses/manage/content/form.html and add the following code to it:

{% extends "base.html" %}

{% block title %}
  {% if object %}
    Edit content "{{ object.title }}"
  {% else %}
    Add a new content
  {% endif %}
{% endblock %}

{% block content %}
  <h1>
    {% if object %}
      Edit content "{{ object.title }}"
    {% else %}
      Add a 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 %}

This is the template for the ContentCreateUpdateView view. In this template, we check if an object variable is in the context. If object exists in the context, we know that we are updating an existing object. Otherwise, we are creating a new object.

We include enctype="multipart/form-data" to the <form> HTML element, because the form contains a file upload for the File and Image content models.

Run the development server. Create a module for an existing course and open http://127.0.0.1:8000/course/module/6/content/image/create/ in your browser. Change the module id in the URL if necessary. You will see the form to create an Image object as follows:

Adding content to course modules

Don't submit the form yet. If you try to do so, it will fail because we haven't defined the module_content_list URL yet. We are going to create it in a bit.

We also need a view to delete contents. Edit the views.py file of the courses application and add the following code:

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)

The ContentDeleteView retrieves the Content object with the given id, it deletes the related Text, Video, Image, or File object, and finally, it deletes the Content object and redirects the user to the module_content_list URL to list the other contents of the module.

Edit the urls.py file of the courses application and add the following URL pattern to it:

url(r'^content/(?P<id>d+)/delete/$', 
    views.ContentDeleteView.as_view(),
    name='module_content_delete'),

Now, instructors can create, update, and delete contents easily.

Managing modules and contents

We have built views to create, edit, and delete course modules and contents. Now, we need a view to display all modules for a course and list contents for a specific module.

Edit the views.py file of the courses application and add the following code to it:

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})

This is ModuleContentListView. This view gets the Module object with the given id that belongs to the current user and renders a template with the given module.

Edit the urls.py file of the courses application and add the following URL pattern to it:

url(r'^module/(?P<module_id>d+)/$', 
    views.ModuleContentListView.as_view(),    
    name='module_content_list'),

Create a new template inside the templates/courses/manage/module/ directory and name it content_list.html. Add the following code to it:

{% 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>
    <hr>
    <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 %}

This is the template that displays all modules for a course and the contents for the selected module. We iterate over the course modules to display them in a sidebar. We also iterate over the module's contents and access content.item to get the related Text, Video, Image, or File object. We also include links to create new text, video, image, or file contents.

We want to know which type of object each of the item object is: Text, Video, Image, or File. We need the model name to build the URL to edit the object. Besides this, we could display each item in the template differently, based on the type of content it is. We can get the model for an object from the model's Meta class, by accessing the object's _meta attribute. Nevertheless, Django doesn't allow accessing variables or attributes starting with underscore in templates to prevent retrieving private attributes or calling private methods. We can solve this by writing a custom template filter.

Create the following file structure inside the courses application directory:

templatetags/
    __init__.py
    course.py

Edit the course.py module and add the following code to it:

from django import template

register = template.Library()

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

This is the model_name template filter. We can apply it in templates as object|model_name to get the model's name for an object.

Edit the templates/courses/manage/module/content_list.html template and add the following line after the {% extends %} template tag:

{% load course %}

This will load the course template tags. Then, replace the following lines:

<p>{{ item }}</p>
<a href="#">Edit</a>

...with the following ones:

<p>{{ item }} ({{ item|model_name }})</p>
<a href="{% url "module_content_update" module.id item|model_name item.id %}">Edit</a>

Now, we display the item model in the template and use the model name to build the link to edit the object. Edit the courses/manage/course/list.html template and add a link to the module_content_list URL like this:

<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 %}

The new link allows users to access the contents of the first module of the course, if any.

Open http://127.0.0.1:8000/course/mine/ and click the Manage contents link for a course that contains at least one module. You will see a page like the following one:

Managing modules and contents

When you click on a module in the left sidebar, its contents are displayed in the main area. The template also includes links to add a new text, video, image, or file content for the module being displayed. Add a couple of different contents to the module and take a look at the result. The contents will appear after Module contents like the following example:

Managing modules and contents

Reordering modules and contents

We need to offer a simple way to re-order course modules and their contents. We will use a JavaScript drag-n-drop widget to let our users reorder the modules of a course by dragging them. When users finish dragging a module, we will launch an asynchronous request (AJAX) to store the new module order.

We need a view that receives the new order of modules' id encoded in JSON. Edit the views.py file of the courses application and add the following code to it:

from braces.views import CsrfExemptMixin, JsonRequestResponseMixin

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'})

This is ModuleOrderView. We use the following mixins of django-braces:

  • CsrfExemptMixin: To avoid checking for a CSRF token in POST requests. We need this to perform AJAX POST requests without having to generate csrf_token.
  • JsonRequestResponseMixin: Parses the request data as JSON and also serializes the response as JSON and returns an HTTP response with the application/json content type.

We can build a similar view to order a module's contents. Add the following code to the views.py file:

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'})

Now, edit the urls.py file of the courses application and add the following URL patterns to it:

url(r'^module/order/$',
    views.ModuleOrderView.as_view(),
    name='module_order'),
url(r'^content/order/$',
    views.ContentOrderView.as_view(),
    name='content_order'),

Finally, we need to implement the drag-n-drop functionality in the template. We will use the jQuery UI library for this. jQuery UI is built on top of jQuery and it provides a set of interface interactions, effects, and widgets. We will use its sortable element. First, we need to load jQuery UI in the base template. Open the base.html file located in the templates/ directory of the courses application, and add jQuery UI below the script to load jQuery, as follows:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>

We load the jQuery UI library just after the jQuery framework. Now, edit the courses/manage/module/content_list.html template and add the following code to it at the bottom of the template:

{% block domready %}
$('#modules').sortable({
    stop: function(event, ui) {
        modules_order = {};
        $('#modules').children().each(function(){
            // update the order field
            $(this).find('.order').text($(this).index() + 1);
            // associate the module's id with its order
            modules_order[$(this).data('id')] = $(this).index();
        });
        $.ajax({
            type: 'POST',
            url: '{% url "module_order" %}',
            contentType: 'application/json; charset=utf-8',
            dataType: 'json',
               data: JSON.stringify(modules_order)
           });
    }
});

$('#module-contents').sortable({
    stop: function(event, ui) {
        contents_order = {};
        $('#module-contents').children().each(function(){
            // associate the module's id with its order
            contents_order[$(this).data('id')] = $(this).index();
        });

        $.ajax({
            type: 'POST',
            url: '{% url "content_order" %}',
            contentType: 'application/json; charset=utf-8',
            dataType: 'json',
            data: JSON.stringify(contents_order),
        });
    }
});
{% endblock %}

This JavaScript code is in the {% block domready %} block and therefore it will be included inside the $(document).ready() event of jQuery that we defined in the base.html template. This guarantees that our JavaScript code is executed once the page has been loaded. We define a sortable element for the modules list in the sidebar and a different one for the module's content list. Both work in a similar manner. In this code, we perform the following tasks:

  1. First, we define a sortable element for the modules HTML element. Remember that we use #modules, since jQuery uses CSS notation for selectors.
  2. We specify a function for the stop event. This event is triggered every time the user finishes sorting an element.
  3. We create an empty modules_order dictionary. The keys for this dictionary will be the modules' id, and the values will be the assigned order for each module.
  4. We iterate over the #module children elements. We recalculate the displayed order for each module and get its data-id attribute, which contains the module's id. We add the id as key of the modules_order dictionary and the new index of module as the value.
  5. We launch an AJAX POST request to the content_order URL, including the serialized JSON data of modules_order in the request. The corresponding ModuleOrderView takes care of updating the modules order.

The sortable element to order contents is quite similar to this one. Go back to your browser and reload the page. Now you will be able to click and drag both, modules and contents, to reorder them like the following example:

Reordering modules and contents

Great! Now you can re-order both course modules and module contents.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset