Django
Django E-learning Platform IV

Django E-learning Platform IV

Part IV:

In the fourth part of Django E-learning Platform, we are going to build the functionality to access course contents, create a student registration system and manage student enrollment onto courses.

Table of contents

Displaying Courses

For our course catalog, we have to build the following functionalities:

  • List all available courses, optionally filtered by subject.
  • Displaying a single course overview

Views

Edit the views.py file of the course application and add the following code.

.....................................................................


from django.db.models import Count

from .models import Course,Module,Content,Subject
from .forms import ModuleFormSet

.....................................................................

class CourseListView(TemplateResponseMixin,View):
    model = Course
    template_name = 'courses/course/list.html'
    def get(self,request,subject=None):
        subjects = Subject.objects.annotate(
            total_courses = Count('courses')
        )

        courses = Course.objects.annotate(
            total_modules = Count('modules')
        )

        if subject:
            subject = get_object_or_404(Subject,slug=subject)
            courses = courses.filter(subject=subject)

        return self.render_to_response({'subjects':subjects,
                                        'subject':subject,
                                        'courses':courses})

CourseListView inherits from TemplateResponseMixin and View. In this view, we perform the following tasks:

  • We retrieve all the subjects, using the ORM’s annotate() method and with the Count() aggregation function to include the total number of the courses for each subject.
  • We retrieve all available courses, including the total number of modules contained in each course.
  • If a subject slug URL parameter is given, we retrieve the corresponding subject object and limit the query to the courses that belong to the given subject.
  • We use the render_to_response() method provided by TemplateResponseMixin to render the objects to a template and return an HTTP response.

Now, let us create a detail view to display single course overview.

from django.views.generic.detail import DetailView
...................................................
class CourseDetailView(DetailView):
    model = Course
    template_name = "courses/course/detail.html"

This view inherits from the generic the DetailView provided by Django. We specify the model and the template_name attributes. Djanog’s DetailView expects a primary key (pk) or slug URL parameter to retrieve a single object for the given mode. The view renders the templates specified in template_name, including the Course object in the template context variable object.

Urls

Edit the main urls.py file of the educa project and add the following URL pattern to it.

..................................................
from courses.views import CourseListView

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')),
    path("",CourseListView.as_view(),name='course_list'),
]

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

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

from django.urls import path
from . import views


urlpatterns = [
.......................................................................
     path('subject/<slug:subject>/',views.CourseListView.as_view(),name='course_list_subject'),
     path('<slug:slug>/',views.CourseDetailView.as_view(),name='course_detail'),
]

Templates

Create the following structure inside the templates/courses/ directory of the courses application.

Put following code in list.html

{% extends "base.html" %}
{% block title %}
{% if subject %}
    {{subject.title}} courses
{% else %}
    All courses
{% endif %}

{% endblock %}

{% block content %}
<h1>
    {% if subject %}
        {{subject.title}} courses

    {% else %}
        All courses

    {% endif %}
</h1>

<div class="contents">
    <h3>
        Subjects
    </h3>
    <ul id="modules">
        <li {% if not subject %} class="selected" {% endif %}>
            <a href="{% url 'course_list'%}">
                All
            </a>
        </li>
        {% for s in subjects %}
            <li {% if subject == s %} class="selected" {% endif %}>
                <a href="{% url 'course_list_subject' s.slug %}">
                    {{s.title}}
                    <br>
                    <span>
                        {{s.total_courses }} course {{s.total_courses|pluralize}}
                    </span>
                </a>
            </li>

        {% endfor %}
    </ul>
</div>
<div class="module">
    {% for course in courses %}
        {% with subject=course.subject %}
        <h3>
            <a href="{% url 'course_detail' course.slug %}">
                {{course.title}}
            </a>
        </h3>
        <p>
            <a href="{% url 'course_list_subject' subject.slug %}">
                {{ subject }}
            </a>
            {{ course.total_modules }} modules.
            Instructor: {{ course.owner.get_full_name }}
        </p>

        {% endwith %}

    {% endfor %}
</div>
{% endblock %}

Run the server and visit: http://127.0.0.1:8000/

When we select a subject.

Add following lines of codes to detail.html

{% extends 'base.html' %}

{% block title %}
    {{object.title}}

{% endblock %}

{% block content %}

    {% with subject=object.subject %}
    <h1>
       
        {{object.title}}
    </h1>
    <div class="module">
        <h2>Overview</h2>
    
    <p>
        <a href="{% url 'course_list_subject' subject.slug %}">
            {{subject.title}}
        </a>
        {{object.modules.count}} modules
        <br>
        Instructor: {{object.owner}}
    </p>
    {{object.overview|linebreaks}}
</div>

    {% endwith %}

{% endblock %}

Output of our detail page.

Adding Student Registration

Create a new application

Create a new application using the following command:

#terminal
python manage.py startapp students

Add our application to our project.

Edit settings.py of our project.

INSTALLED_APPS = [
    'students',
    'courses',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Create a student registration view

Edit the views.py file of the students application and write the following code.


from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import authenticate,login


# Create your views here.
class StudentRegistrationView(CreateView):
    template_name = 'students/student/registration.html'
    form_class = UserCreationForm
    success_url = reverse_lazy('student_course_list')

    def form_valid(self, form):
        result = super().form_valid(form)
        cd = form.cleaned_data
        user = authenticate(username = cd['username'],
                            password = cd['password1'])
        login(self.request,user)
        return result

This views allows students to register on our site. We use the generic CreateView, which provides the functionality for creating model objects. This view requires the following attributes.

  • template_name: The path of the template to render this view.
  • form_class: The form for creating objects, which has to be ModelForm. We use Django’s UserCreationForm as the registration form to create User objects.
  • success_url: The URL to redirect the user to when the form is successfully submitted.

The form_valid() method is executed when valid form data has been posted. It has to return an HTTP response. We override this method to log the user after they have successfully signed up.

Student Urls

Create a new file inside the students application directory and name it urls.py. Add the following code to it.

from django.urls import path
from . import views

urlpatterns = [
    path('register/',views.StudentRegistrationView.as_view(),
         name='student_registration')
]

Now, edit the main urls.py of the educa project and include the URLs for the student application by adding the following pattern to our URL configuration.


urlpatterns = [
    .......................................
    path("",CourseListView.as_view(),name='course_list'),
    path("students/",include('students.urls')),
]

..................................................

Student templates

Create the following file structure inside the students application directory.

Add the following lines of codes to registration.html

{% extends "base.html" %}
{% block title %}
    Sign up
{% endblock %}

{% block content %}
    <h1>Signup</h1>
    <div class="module">
        <p>Enter your details to create an account: </p>
        <form method="post">
            {{form.as_p}}
            {% csrf_token %}
            <p><input type="submit" value="Create my account"></p>
        </form>

    </div>

{% endblock content %}

Run the server and visit: http://127.0.0.1:8000/students/register/

Enrolling on Courses

After users create an account, they should be able to enroll courses. To store enrollments, we need to create a many-to-many relationship between Course and User models.

Models

Edit the models.py file of the courses application and add the following field to Course model.

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

    class Meta:
        ordering = ['-created']

    def __str__(self):
        return self.title

Make migrations and migrate.

Forms

Create a new file inside the students application directory and name it forms.py. Add the following code to it.

from django import forms
from courses.models import Course

class CourseEnrollForm(forms.Form):
    course = forms.ModelChoiceField(
        queryset=Course.objects.all(),
        widget=forms.HiddenInput)

We are going to use this form for students enroll on courses. The course field is for the course on which the user will be enrolled; therefor, it’s a ModelChoiceField. We use a HiddenInput widget because we are not going to show this field to the user. We are going to use this form in the CourseDetailView view to display a button to enroll.

Students views

Edit the views.py file of the students application and add the following code:


...............................................................
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import FormView

from .forms import CourseEnrollForm

# Create your views here.
................................................................

class StudentEnrollCourseView(LoginRequiredMixin,FormView):
    
    course = None
    form_class = CourseEnrollForm

    def form_valid(self,form):
        self.course = form.cleaned_data['course']
        self.course.students.add(self.request.user)
        return super().form_valid(form)
    
    def get_success_url(self):
        return reverse_lazy('student_course_detail',args=[self.course.id])

This view handles enrolling on courses. The view inherits the LoginRequiredMixin mixin so that only logged-in users can access the view. It also inherits from Djanog’s FormView view, since we handle a form submission. We use the CourseEnrollForm form for the form_class attribute and also define a course attribute for sorting the given the Course object. When the form is valid, we add the current user to the students enrolled on the course.

Students urls

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

from django.urls import path
from . import views

urlpatterns = [
    path('register/',views.StudentRegistrationView.as_view(),
         name='student_registration'),
    path('enroll-course/',views.StudentEnrollCourseView.as_view(),
         name='student_enroll_course'),
]

Course Detail View

Add the enroll button form to the course overview page. Edit the views.py file of the courses application and modify CourseDetailView to make it look as follows.

from typing import Any, Dict
from django import http
from django.db.models.query import QuerySet
from django.shortcuts import render

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

from django.shortcuts import redirect,get_object_or_404
from django.views.generic.base import TemplateResponseMixin, View
from braces.views import CsrfExemptMixin, JSONRequestResponseMixin

from django.contrib.auth.mixins import (LoginRequiredMixin,
                                        PermissionRequiredMixin)

from django.forms.models import modelform_factory
from django.apps import apps
from django.db.models import Count
from django.views.generic.detail import DetailView

from students.forms import CourseEnrollForm

from .models import Course,Module,Content,Subject
from .forms import ModuleFormSet

# Create your views here.

...............................................
class CourseDetailView(DetailView):
    model = Course
    template_name = "courses/course/detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['enroll_form'] = CourseEnrollForm(
            initial={'course':self.object}
        )
        return context



We use the get_context_data() method to include the enrollment form in the context for rendering the templates. We initialize the hidden course field of the form with the current Course objects so that it can be submitted directly.

templates

Edit the courses/course/detail.html template and add the following lines of codes.

{% extends 'base.html' %}

{% block title %}
    {{object.title}}

{% endblock %}

{% block content %}

    {% with subject=object.subject %}
    <h1>
       
        {{object.title}}
    </h1>
    <div class="module">
        <h2>Overview</h2>
    
    <p>
        <a href="{% url 'course_list_subject' subject.slug %}">
            {{subject.title}}
        </a>
        {{object.modules.count}} modules
        <br>
        Instructor: {{object.owner}}
    </p>
    {{object.overview|linebreaks}}
    {% if request.user.is_authenticated %}
        <form action="{% url 'student_enroll_course' %}" method="post">
            {{enroll_form}}
            {% csrf_token %}
            <input type="submit" value="Enroll now">
        </form>
    {% else %}
        <a href="{% url 'student_registration' %}" class="button">
            Register to enroll
        </a>
    {% endif %}
</div>

    {% endwith %}

{% endblock %}

Output so far

Run the server: and visit the some course.

If you are not logged in you will see the following output.

If you are logged in you will see the enroll options.

Accessing the course contents

Views

We need a view for displaying the courses that students are enrolled on and a view for accessing the actual course contents. Edit the views.py file of the students application and add the following code to it.

..................................................
from django.views.generic.list import ListView

from courses.models import Course

from .forms import CourseEnrollForm

# Create your views here.
......................................................................
class StudentCourseListView(LoginRequiredMixin,ListView):
    model = Course
    template_name = "students/course/list.html"

    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(students__in = [self.request.user])
    
    

This is the view to see courses that students are enrolled on. Here, we override the get_queryset() method to retrieve only the courses that a student is enrolled on; we filter the QuerySet by the student’s ManyToManyField to do so.

Add the following code to views.py file of the students application.

...............................................................
from django.views.generic.detail import DetailView


from courses.models import Course

from .forms import CourseEnrollForm

# Create your views here.

 ......................................................   

class StudentCourseDetailView(DetailView):
    model  = Course
    template_name = "students/course/detail.html"

    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(students__in = [self.request.user])
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        #get course object
        course = self.get_object()
        if 'module_id' in self.kwargs:
            #get current moduel
            context['module'] = course.modules.get(
                id=self.kwargs['module_id']
            )
        else:
            #get first module
            context['module'] = course.modules.all()[0]

        return context
    

This is the StudentCourseDetailView view. We override the get_queryset() method to limit the base QuerySet to courses on which the student is enrolled. We also override the get_contenxt_data() method to set a course module in the context if the module_id URL parameter is given. Otherwise, we set the first module of the course. This way, students will be able to navigate through modules inside a course.

urls

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

from django.urls import path
from . import views

urlpatterns = [
    path('register/',views.StudentRegistrationView.as_view(),
         name='student_registration'),
    path('enroll-course/',views.StudentEnrollCourseView.as_view(),
         name='student_enroll_course'),

    path('courses/',views.StudentCourseListView.as_view(),
         name='student_course_list'),
    
    path('course/<pk>/',
         views.StudentCourseDetailView.as_view(),
         name='student_course_detail'),

    path('course/<pk>/<module_id>/',
         views.StudentCourseDetailView.as_view(),
         name='student_course_detail_module'),
]

templates

Create the following file structure inside the templates/students/ directory of the students application.

Edit the students/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 'student_course_detail' course.id %}">
                        Access contents
                    </a>
                </p>
            </div>
        {% empty %}
            <p>
                You are not enrolled in any courses yet
                <a href="{% url 'course_list'%}">
                    Browse courses
                </a>
                to enroll on a course
            </p>

        {% endfor %}
    </div>
{% endblock %}

Now, edit the settings.py file of educa project and add the following code to it.

................................................
from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')
...............................................

Now, add the following code to students/course/detail.html template and add the following code to it.

{% extends 'base.html' %}

{% block title %}
    {{object.title}}
{% endblock %}

{% block content %}
    <h1>
        {{module.title}}
    </h1>
    <div class="contents">
        <h3>Modules</h3>
        <ul id="modules">
            {% for m in object.modules.all %}
                <li data-id = "{{m.id}}" {% if m== modules %} class="selected" {% endif %}>
                    <a href="{% url 'student_course_detail_module' object.id.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>
    </div>

    <div class="module">
        {% for content in module.contents.all %}
            {% with item = content.item %}
                <h2>{{item.title}}</h2>
                {{item.render}}

            {% endwith %}

        {% endfor %}
    </div>
{% endblock content %}

Rendering Different types of content

To display the course contents, we need to render the different content types that we created: text, image, video and file.

Edit the models.py file of the courses application and add the following render() method to the ItemBase model.

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

    class Meta:
        abstract = True

    def render(self):
        return render_to_string(
            f'courses/content/{self._meta.model_name}.html',
            {'item':self}
        )
    
    def __str__(self):
        return self.title

This method uses the render_to_string() function for rendering a template and returning the rendered content as a string. Each kind of content is rendered using a template named after the content model. We use self._meta.model_name to generate the appropriate template name for each content model dynamically. The render() method provides a common interface for rendering diverse content.

Create the following file structure inside the templates/courses/ directory of the courses application.

Add the following code to text.html

{{item.content|linebreaks}}

Now add the following content to file.html

<p>
    <a href="{{item.file.url}}" class="button">
        Download file
    </a>
</p>

Next, add following line of codes to image.html.

<p>
    <img src="{{item.file.url}}" alt="{{item.title}}">
</p>

For video, install the following app.

#terminal
pip install django-embed-video

Add this application to our project.

# Application definition

INSTALLED_APPS = [
    'students',
    'courses',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'embed_video',
]

Add the following code to video.html

{% load embed_video_tags %}
{% video item.url "small" %}

Output