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
- Adding Student Registration
- Enrolling on Courses
- Accessing Course Contents
- Rendering different type of contents
- Output
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" %}