Django
Django E-learning Platform

Django E-learning Platform

Part I: Models and Authentication

We will be building an E-Learning Platform in Django.

In this first part we will design the models necessary for our e-learning system. We will also add basic authentication system to our E-learning platform.

Table of Contents

Basic Installation

#terminal
mkdir Django_Elearn
cd Django_Elearn
python3 -m venv venv
source venv/bin/activate
pip install django
pip install Pillow
django-admin startproject educa
cd educa
django-admin startapp courses

To add the application to our project edit settings.py of your project.

# Application definition

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

Serving Media Files

Edit the settings.py file to the project and add following lines:


MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'

This will enable Django to manage file uploads and serve media files. MEDIA_URL is the base URL used to serve the media files uploaded by users. MEDIA_ROOT is the local path where they reside. Paths and URLs for files are built dynamically by prepending the project path or the media URL to them for probability.

Now, edit the main urls.py file of the project and modify the code as follows.

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
]

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

Building the Course Model

Our e-learning platform will offer courses on various subjects. Each course will be divided into a configurable number of modules and each module will contain a configurable number of contents. The contents will be of various types: text, files, images or videos. The following example shows what the data structure of our course catalog will look like:

  • Subject 1
    • Course 1
      • Module 1
        • Content I (text)
        • Content II (img)
        • Content III (video)
      • Module 2
        • Content I (img)

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

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Subject(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200,unique=True)

    class Meta:
        ordering = ['title']

    def __str__(self):
        return self.title
    

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)

    class Meta:
        ordering = ['-created']

    def __str__(self):
        return self.title
    
class Module(models.Model):
    course = models.ForeignKey(Course,related_name='modules',on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=200)

    def __str__(self):
        return self.title

Make migrations and migrate.

#Terminal
python manage.py makemigrations
python manage.py migrate

Registering the models in the adminstration site

Edit the admin.py file inside the courses application directory and add the following code to it.

from django.contrib import admin

from .models import Subject,Course,Module

# Register your models here.
@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
    list_display = ['title','slug']
    prepopulated_fields = {'slug':('title',)}

class ModuleInline(admin.StackedInline):
    model = Module

@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    list_display = ['title','subject','created']
    list_filter = ['created','subject']
    search_fields = ['title','overview']
    prepopulated_fields = {'slug':('title',)}
    inlines = [ModuleInline]

The superuser

Create the superuser with the following command

#Terminal
python manage.py createsuperuser

Using fixtures to provide initial data for models

Add some data to our models using admin panel

Saving the data in json form.

#terminal
mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subject.json

Delete the data from the database

Now, load the data with the following command.

#terminal
python manage.py loaddata subjects.json

Creating models for polymorphic content

We plan to add different types of content to the course modules, such as text, images, files and videos. Polymorphism is the provision of a single interface to entities of different types. We need a versatile data model that allows us to store diverse content that is accessible through a single interface. We are going to create a Content model that represents the modules’ contents and define a generic relation to associate any object with the content object.

Edit the models.py file of the courses application and add the following imports.

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey


......................................
class Content(models.Model):
    module = models.ForeignKey(Module,related_name='contents',on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
                                     on_delete=models.CASCADE)
    object_id = models.PositiveBigIntegerField()
    item = GenericForeignKey('content_type','object_id')

This is the Content model. A module contains multiple contents, so we define a ForeignKey field that points to the Module model. Only the content_type and object_id fields have a corresponding column in the database table of this model. The item field allows us to retrieve or set the related object directly, and its functionality is built on top of the other two fields.

We are going to use a different model for each type of content. Our Content models will have some common files but they will differ in the actual data they can store.

Creating the Content Models

The Content model of our courses application contains a generic relation to associate different types of content with it. We will create a different model for each type of content. All the Content models will have some fields in common and additional fields to store custom data. We are going to create an abstract model that provides the common fields for all Content models.

Edit models.py file and add the following code:

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 __str__(self):
        return self.title
    
class Text(ItemBase):
    content = models.TextField()

class File(ItemBase):
    file = models.FileField(upload_to='files')

class Image(ItemBase):
    file = models.FileField(upload_to='images')

class Video(ItemBase):
    url = models.URLField()

In this code, we define an abstract model named ItemBase. In this model, we define the owner, title, created and updated fields. These common fields will be used for all types of content.

The owner field allows us to store which user created the content. Since this field is defined in an abstract class, we need a different related_name for each sub-model. Django allows us to specify a placeholder for the model class name in the related_name attribute as %(class)s. By doing so, the related_name for each child model will be generated automatically. Since we are using ‘%(class)s_related’ as the related_name, the reverse relationship for child models will be text_related, file_related, image_related and video_related, respectively.

Edit the Content model we created previously and modify its content_type field, as follows.:

class Content(models.Model):
    module = models.ForeignKey(Module,related_name='contents',on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
                                     on_delete=models.CASCADE,
                                     limit_choices_to={'model__in':(
                                         'text','video','image','file'
                                     )})
    object_id = models.PositiveBigIntegerField()
    item = GenericForeignKey('content_type','object_id')

We add a limit_choices_to argument to limit the ContentType objects that can be used for the generic relation. We used the ‘model__’ in field lookup to filter the query to the ContentType objects with a model attribute that is ‘text’, ‘video’, ‘image’, or ‘file’.

Create migrations and migrate.

#terminal
python manage.py makemigrations
python manage.py migrate

Creating Custom model fields

Create a new fields.py file inside the courses application directory and add the following code to it.

from typing import Any
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model


class OrderField(models.PositiveIntegerField):
    def __init__(self, for_fields=None,*args,**kwargs):
        self.for_fields = for_fields
        super().__init__(*args,**kwargs)

    def pre_save(self, model_instance, add):
        if getattr(model_instance,self.attname) is None:
            #no current value
            try:
                qs = self.model.objects.all()
                if self.for_fields:
                    #field by objects with the same field values
                    #for the fields in "for_fields"
                    query = {field:getattr(model_instance,field)\
                             for field in self.for_fields}
                    qs = qs.filter(**query)

                    #get the order of the last item
                    last_item= qs.latest(self.attname)
                    value = last_item.order +1 
            except ObjectDoesNotExist:
                value = 0
            setattr(model_instance,self.attname,value)
            return value
        else:
            return super().pre_save(model_instance,add)

This is the custom OrderField. It inherits from the PositiveIntegerField field provided by Django. Our OrderField field takes an optional for_fields parameter, which allows us to indicate the fields used to order the data.

Adding ordering to module and content objects

Edit the models.py file of the courses application as follows.

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

from .fields import OrderField

# Create your models here.
.......................................................
    
class Module(models.Model):
    course = models.ForeignKey(Course,related_name='modules',on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=200)
    order = OrderField(blank=True,for_fields=['course'])

    class Meta:
        ordering = ['order']


    def __str__(self):
        return f'{self.order}.{self.title}'
    
class Content(models.Model):
    module = models.ForeignKey(Module,related_name='contents',on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
                                     on_delete=models.CASCADE,
                                     limit_choices_to={'model__in':(
                                         'text','video','image','file'
                                     )})
    object_id = models.PositiveBigIntegerField()
    item = GenericForeignKey('content_type','object_id')
    order = OrderField(blank=True,for_fields=['module'])

    class Meta:
        ordering = ['order']

Making migrations

#Terminal
python manage.py makemigrations courses

Django is telling us to provide a default value for the new order field for existing rows in the database.

Enter 1 and press Enter to provide a default value for existing records. Enter 0 so that this is the default value for existing records and press Enter. Django will ask you for a default value for the Module model too. Repeat above process.

#Terminal
python manage.py migrate

Testing our OrderField in shell

#Terminal
python manage.py shell

Create a new course, as follows:

>>>from django.contrib.auth.models import User
>>>from courses.models import Subject,Course,Module
>>>user = User.objects.last()
>>>subject = Subject.objects.last()
>>>c1 = Course.objects.create(Subject=subject,owner=user,title='Course 1',slug='Course1'
>>>m1 = Module.objects.create(course=c1,title='Module 1')
>>m1.order
>>m2=Module.objects.create(course=c1,title='Module 2',order=6)
>>>m2.order
>>>m3=Module.objects.create(course=c1,title='Module 3')
>>>m3.order

Adding an Authentication System

We are going to use Django’s authentication framework for users to authentication to the e-learning platform. Both instructors and students will be instance of Django’s User model, so they will be able to log in to the site using the authentication views of django.contrib.auth.

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


from django.contrib import admin
from django.urls import path
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),
]

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

Creating Authentication Templates

Create following folders and files within the Courses applications.

Put the following files in the 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><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>
            document.addEventListener('DOMContentLoaded',(event)=>{
                //DOM loaded
                {% block domready %}
                {% endblock %}
            });
        </script>
    </body>
</html>

Put the following files in the login.html.

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

Put the following files in the log_out.html.

{% 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'%}">
                Login Again
            </a>
        </p>
    </div>

{% endblock %}

CSS

Create following folders and files in the courses application.

@import url(//fonts.googleapis.com/css?family=Roboto:500,300,400);

body {
    margin:0;
    font-family:'Roboto', sans-serif;
    font-weight:400;
    color:#333;
}

a {
    color:#3fad37;
    text-decoration:none;
}

ul {
    float:left;
}

h1, h2, h3, h4, h5, h6 {
    font-family:'Roboto', sans-serif;
    font-weight:300;
}

h1 {
    background:#efefef;
    width:100%;
    overflow:auto;
    padding:20px 0 20px 40px;
    margin:0;
}

.module {
    padding:10px 20px;
    float:left;
    width:600px;
}

.module h3 {
    margin:20px 0 0;
    padding:0;
    width:100%;
}

.module p {
    margin:10px 0 20px;
    width:100%;
    float:left;
}

#header {
    background:#4dcc43;
    overflow:auto;
    padding:10px 20px;
    border-bottom:6px solid #3fad37;
}

#header .logo {
    text-decoration:none;
    font-family:'Roboto', sans-serif;
    font-weight:300;
    text-transform:uppercase;
    font-size:24px;
    color:#fff;
    float:left;
}

#header a {
    color:#fff;
}

#header .menu {
    list-style:none;
    float:right;
    margin:0;
    padding:0;
}

.contents {
    width:20%;
    padding:10px;
    float:left;
    background:#333;
    color: #fff;
    font-family:'Roboto', sans-serif;
}

.contents ul {
    list-style:none;
    margin:0;
    padding:0;
    width:100%;
}

.contents ul li {
    margin:0 0 10px 0;
    padding:4px 10px 10px;
    overflow:auto;
    cursor:move;
}

.contents ul li.selected {
    background:#3f3f3f;
}

.contents ul li:hover {
    background:#3f3f3f;
}

.contents ul li.placeholder {
  background: #333;
  border:1px dashed #ccc;
  padding:0;
  margin:10px 0;
}

.contents ul span {
    text-transform:uppercase;
    color:#bbb;
    font-size:14px;
}

.contents ul a {
    display:block;
    color:#fff;
    text-decoration:none;
}

.contents ul a:hover {
    color:#4dcc43;
    cursor:move;
}

ul.content-types li {
    list-style:none;
    float:left;
    margin:10px;
    background:#efefef;
    padding:8px 14px;
}

.hidden {
    display:none;
}

form p {
    overflow:auto;
}

.errorlist {
    color:#ae2c2c;
    margin:0;
}

label {
    float:left;
    clear:both;
    margin:0 0 8px 0;
}

input, select, textarea {
    border:1px solid #ccc;
    border-bottom:3px solid #ccc;
    padding:8px 12px;
    font-size:16px;
    font-family:'Roboto', sans-serif;
    float:left;
    clear:both;
    width:300px;
}

textarea {
    height:80px;
}

select {
    width:324px;
}

input[type=submit], a.button {
    border-radius:5px;
    background:#4dcc43;
    color:#fff;
    font-size:16px;
    text-transform:uppercase;
    border:none;
    padding:10px 20px;
    margin:20px 0;
}

a.secondary-button {
    border:3px solid #4dcc43;
    padding:10px 20px;
    margin:10px 0;
}

input[type=submit]:hover, a.button:hover {
    background:#3fad37;
}

ul#course-modules {
    list-style:none;
    overflow:auto;
}

ul#course-modules textarea {
    width:600px;
    height:120px;
}

ul#course-modules li {
    padding:20px;
    overflow:auto;
    cursor:move;
    user-select:none;
}

ul#course-modules li:nth-child(even) {
    background:#efefef;
}

ul#course-modules li:hover {
    background:#ccc;
}

#module-contents div {
    padding:10px 20px;
    border:1px solid #ccc;
    background:#fff
}

#module-contents div.placeholder {
  border:1px dashed #ccc;
}

#module-contents form {
    margin:0;
    padding:0;
}

#module-contents input[type=submit] {
    color:#3fad37;
    background:none;
    margin:-20px 0 0;
    padding:0;
    float:left;
    text-transform:none;
}

#module-contents div:hover {
    cursor:move;
}

.course-info {
    border:1px solid #ccc;
    padding:0 20px;
    margin-bottom:10px;
    width:400px;
    overflow:auto;
}

.course-info a {
    margin-right:10px;
}

.helptext {
    color:#ccc;
    padding-left:20px;
}

#chat {
    top: 64px;
    bottom:0;
    position:fixed;
    width:100%;
    overflow-y:scroll;
    padding-bottom:150px;
}

#chat .message {
    background:#efefef;
    padding:10px 20px;
    border-radius:4px;
    width:auto;
    display:inline;
    float:left;
    margin:10px 10px 0;
    min-width:440px;
    clear:both;
}

#chat .message.me {
    display:inline;
    float:right;
    background:#DCEDE0;
    color:#56A668;
}

#chat .date {
    color:#aaa;
    font-style:italic;
    font-size:12px;
}

#chat-input {
    position:absolute;
    bottom:0;
    background:#efefef;
    width:100%;
    padding-top:20px;
}

#chat-input input {
    width:96%;
    position:left;
    float:left;
    display:inline;
    margin-left:2%;
    margin-right:2%;
    padding-left:0;
    padding-right:0;
}