12

Building an E-Learning Platform

In the previous chapter, you learned the basics of the internationalization and localization of Django projects. You added internationalization to your online shop project. You learned how to translate Python strings, templates, and models. You also learned how to manage translations, and you created a language selector and added localized fields to your forms.

In this chapter, you will start a new Django project that will consist of an e-learning platform with your own content management system (CMS). Online learning platforms are a great example of applications where you need to provide tools to generate content with flexibility in mind.

In this chapter, you will learn how to:

  • Create models for the CMS
  • Create fixtures for your models and apply them
  • Use model inheritance to create data models for polymorphic content
  • Create custom model fields
  • Order course contents and modules
  • Build authentication views for the CMS

The source code for this chapter can be found at https://github.com/PacktPublishing/Django-4-by-example/tree/main/Chapter12.

All the Python modules used in this chapter are included in the requirements.txt file in the source code that comes with this chapter. You can follow the instructions to install each Python module below, or you can install all the requirements at once with the command pip install -r requirements.txt.

Setting up the e-learning project

Your final practical project will be an e-learning platform. First, create a virtual environment for your new project within the env/ directory with the following command:

python -m venv env/educa

If you are using Linux or macOS, run the following command to activate your virtual environment:

source env/educa/bin/activate

If you are using Windows, use the following command instead:

.enveducaScriptsactivate

Install Django in your virtual environment with the following command:

pip install Django~=4.1.0

You are going to manage image uploads in your project, so you also need to install Pillow with the following command:

pip install Pillow==9.2.0

Create a new project using the following command:

django-admin startproject educa

Enter the new educa directory and create a new application using the following commands:

cd educa
django-admin startapp courses

Edit the settings.py file of the educa project and add courses to the INSTALLED_APPS setting, as follows. The new line is highlighted in bold:

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

The courses application is now active for the project. Next, we are going to prepare our project to serve media files, and we will define the models for the courses and course contents.

Serving media files

Before creating the models for courses and course contents, we will prepare the project to serve media files. Course instructors will be able to upload media files to course contents using the CMS that we will build. Therefore, we will configure the project to serve media files.

Edit the settings.py file of the project and add the 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 portability.

Now, edit the main urls.py file of the educa project and modify the code, as follows. New lines are highlighted in bold:

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)

We have added the static() helper function to serve media files with the Django development server during development (that is, when the DEBUG setting is set to True).

Remember that the static() helper function is suitable for development but not for production use. Django is very inefficient at serving static files. Never serve your static files with Django in a production environment. You will learn how to serve static files in a production environment in Chapter 17, Going Live.

The project is now ready to serve media files. Let’s create the models for the courses and course contents.

Building the course models

Your 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 your course catalog will look like:

Subject 1
  Course 1
    Module 1
      Content 1 (image)
      Content 2 (text)
    Module 2
      Content 3 (text)
      Content 4 (file)
      Content 5 (video)
      ...

Let’s build the course models. 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
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=True)
    def __str__(self):
        return self.title

These are the initial Subject, Course, and Module models. The Course model fields are as follows:

  • owner: The instructor who created this course.
  • subject: The subject that this course belongs to. It is a ForeignKey field that points to the Subject model.
  • title: The title of the course.
  • slug: The slug of the course. This will be used in URLs later.
  • overview: A TextField column to store an overview of the course.
  • created: The date and time when the course was created. It will be automatically set by Django when creating new objects because of auto_now_add=True.

Each course is divided into several modules. Therefore, the Module model contains a ForeignKey field that points to the Course model.

Open the shell and run the following command to create the initial migration for this application:

python manage.py makemigrations

You will see the following output:

Migrations for 'courses':
  courses/migrations/0001_initial.py:
    - Create model Course
    - Create model Module
    - Create model Subject
    - Add field subject to course

Then, run the following command to apply all migrations to the database:

python manage.py migrate

You should see output that includes all applied migrations, including those of Django. The output will contain the following line:

Applying courses.0001_initial... OK

The models of your courses application have been synced with the database.

Registering the models in the administration site

Let’s add the course models to the administration 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
@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 models for the course application are now registered on the administration site. Remember that you use the @admin.register() decorator to register models on the administration site.

Using fixtures to provide initial data for models

Sometimes, you might want to prepopulate your database with hardcoded data. This is useful for automatically including initial data in the project setup, instead of having to add it manually. Django comes with a simple way to load and dump data from the database into files that are called fixtures. Django supports fixtures in JSON, XML, or YAML formats. You are going to create a fixture to include several initial Subject objects for your project.

First, create a superuser using the following command:

python manage.py createsuperuser

Then, run the development server using the following command:

python manage.py runserver

Open http://127.0.0.1:8000/admin/courses/subject/ in your browser. Create several subjects using the administration site. The change list page should look as follows:

Figure 12.1: The subject change list view on the administration site

Run the following command from the shell:

python manage.py dumpdata courses --indent=2

You will see an output similar to the following:

[
{
  "model": "courses.subject",
  "pk": 1,
  "fields": {
    "title": "Mathematics",
    "slug": "mathematics"
  }
},
{
  "model": "courses.subject",
  "pk": 2,
  "fields": {
    "title": "Music",
    "slug": "music"
  }
},
{
  "model": "courses.subject",
  "pk": 3,
  "fields": {
    "title": "Physics",
    "slug": "physics"
  }
},
{
  "model": "courses.subject",
  "pk": 4,
  "fields": {
    "title": "Programming",
    "slug": "programming"
  }
}
]

The dumpdata command dumps data from the database into the standard output, serialized in JSON format by default. The resulting data structure includes information about the model and its fields for Django to be able to load it into the database.

You can limit the output to the models of an application by providing the application names to the command, or specifying single models for outputting data using the app.Model format. You can also specify the format using the --format flag. By default, dumpdata outputs the serialized data to the standard output. However, you can indicate an output file using the --output flag. The --indent flag allows you to specify indentations. For more information on dumpdata parameters, run python manage.py dumpdata --help.

Save this dump to a fixtures file in a new fixtures/ directory in the courses application using the following commands:

mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json

Run the development server and use the administration site to remove the subjects you created, as shown in Figure 12.2:

Figure 12.2: Deleting all existing subjects

After deleting all subjects, load the fixture into the database using the following command:

python manage.py loaddata subjects.json

All Subject objects included in the fixture are loaded into the database again:

Figure 12.3: Subjects from the fixture are now loaded into the database

By default, Django looks for files in the fixtures/ directory of each application, but you can specify the complete path to the fixture file for the loaddata command. You can also use the FIXTURE_DIRS setting to tell Django additional directories to look in for fixtures.

Fixtures are not only useful for setting up initial data, but also for providing sample data for your application or data required for your tests.

You can read about how to use fixtures for testing at https://docs.djangoproject.com/en/4.1/topics/testing/tools/#fixture-loading.

If you want to load fixtures in model migrations, look at Django’s documentation about data migrations. You can find the documentation for migrating data at https://docs.djangoproject.com/en/4.1/topics/migrations/#data-migrations.

You have created the models to manage course subjects, courses, and course modules. Next, you will create models to manage different types of module contents.

Creating models for polymorphic content

You 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. You need a versatile data model that allows you to store diverse content that is accessible through a single interface. In Chapter 7, Tracking User Actions, you learned about the convenience of using generic relations to create foreign keys that can point to the objects of any model. You 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.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

Then, add the following code to the end of the file:

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.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')

This is the Content model. A module contains multiple contents, so you define a ForeignKey field that points to the Module model. You can also set up a generic relation to associate objects from different models that represent different types of content. Remember that you need three different fields to set up a generic relation. In your Content model, these are:

  • content_type: A ForeignKey field to the ContentType model.
  • object_id: A PositiveIntegerField to store the primary key of the related object.
  • item: A GenericForeignKey field to the related object combining the two previous fields.

Only the content_type and object_id fields have a corresponding column in the database table of this model. The item field allows you to retrieve or set the related object directly, and its functionality is built on top of the other two fields.

You are going to use a different model for each type of content. Your Content models will have some common fields, but they will differ in the actual data they can store. This is how you will create a single interface for different types of content.

Using model inheritance

Django supports model inheritance. It works in a similar way to standard class inheritance in Python. Django offers the following three options to use model inheritance:

  • Abstract models: Useful when you want to put some common information into several models.
  • Multi-table model inheritance: Applicable when each model in the hierarchy is considered a complete model by itself.
  • Proxy models: Useful when you need to change the behavior of a model, for example, by including additional methods, changing the default manager, or using different meta options.

Let’s take a closer look at each of them.

Abstract models

An abstract model is a base class in which you define the fields you want to include in all child models. Django doesn’t create any database tables for abstract models. A database table is created for each child model, including the fields inherited from the abstract class and the ones defined in the child model.

To mark a model as abstract, you need to include abstract=True in its Meta class. Django will recognize that it is an abstract model and will not create a database table for it. To create child models, you just need to subclass the abstract model.

The following example shows an abstract Content model and a child Text model:

from django.db import models
class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    class Meta:
        abstract = True
class Text(BaseContent):
    body = models.TextField()

In this case, Django would create a table for the Text model only, including the title, created, and body fields.

Multi-table model inheritance

In multi-table inheritance, each model corresponds to a database table. Django creates a OneToOneField field for the relationship between the child model and its parent model. To use multi-table inheritance, you have to subclass an existing model. Django will create a database table for both the original model and the sub-model. The following example shows multi-table inheritance:

from django.db import models
class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
class Text(BaseContent):
    body = models.TextField()

Django will include an automatically generated OneToOneField field in the Text model and create a database table for each model.

Proxy models

A proxy model changes the behavior of a model. Both models operate on the database table of the original model. To create a proxy model, add proxy=True to the Meta class of the model. The following example illustrates how to create a proxy model:

from django.db import models
from django.utils import timezone
class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
class OrderedContent(BaseContent):
    class Meta:
        proxy = True
        ordering = ['created']
    def created_delta(self):
        return timezone.now() - self.created

Here, you define an OrderedContent model that is a proxy model for the Content model. This model provides a default ordering for QuerySets and an additional created_delta() method. Both models, Content and OrderedContent, operate on the same database table, and objects are accessible via the ORM through either model.

Creating the Content models

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

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

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, you define an abstract model named ItemBase. Therefore, you set abstract=True in its Meta class.

In this model, you define the owner, title, created, and updated fields. These common fields will be used for all types of content.

The owner field allows you to store which user created the content. Since this field is defined in an abstract class, you need a different related_name for each sub-model. Django allows you 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 you 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.

You have defined four different Content models that inherit from the ItemBase abstract model. They are as follows:

  • Text: To store text content
  • File: To store files, such as PDFs
  • Image: To store image files
  • Video: To store videos; you use an URLField field to provide a video URL in order to embed it

Each child model contains the fields defined in the ItemBase class in addition to its own fields. A database table will be created for the Text, File, Image, and Video models, respectively. There will be no database table associated with the ItemBase model since it is an abstract model.

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

content_type = models.ForeignKey(ContentType,
                   on_delete=models.CASCADE,
                   limit_choices_to={'model__in':(
                                     'text',
                                     'video',
                                     'image',
                                     'file')})

You add a limit_choices_to argument to limit the ContentType objects that can be used for the generic relation. You use the model__in field lookup to filter the query to the ContentType objects with a model attribute that is 'text', 'video', 'image', or 'file'.

Let’s create a migration to include the new models you have added. Run the following command from the command line:

python manage.py makemigrations

You will see the following output:

Migrations for 'courses':
  courses/migrations/0002_video_text_image_file_content.py
    - Create model Video
    - Create model Text
    - Create model Image
    - Create model File
    - Create model Content

Then, run the following command to apply the new migration:

python manage.py migrate

The output you see should end with the following line:

Applying courses.0002_video_text_image_file_content... OK

You have created models that are suitable for adding diverse content to the course modules. However, there is still something missing in your models: the course modules and contents should follow a particular order. You need a field that allows you to order them easily.

Creating custom model fields

Django comes with a complete collection of model fields that you can use to build your models. However, you can also create your own model fields to store custom data or alter the behavior of existing fields.

You need a field that allows you to define an order for the objects. An easy way to specify an order for objects using existing Django fields is by adding a PositiveIntegerField to your models. Using integers, you can easily specify the order of the objects. You can create a custom order field that inherits from PositiveIntegerField and provides additional behavior.

There are two relevant functionalities that you will build into your order field:

  • Automatically assign an order value when no specific order is provided: When saving a new object with no specific order, your field should automatically assign the number that comes after the last existing ordered object. If there are two objects with orders 1 and 2 respectively, when saving a third object, you should automatically assign order 3 to it if no specific order has been provided.
  • Order objects with respect to other fields: Course modules will be ordered with respect to the course they belong to and module contents with respect to the module they belong to.

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

from django.db import models
from django.core.exceptions import ObjectDoesNotExist
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:
                    # filter 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. Your OrderField field takes an optional for_fields parameter, which allows you to indicate the fields used to order the data.

Your field overrides the pre_save() method of the PositiveIntegerField field, which is executed before saving the field to the database. In this method, you perform the following actions:

  1. You check whether a value already exists for this field in the model instance. You use self.attname, which is the attribute name given to the field in the model. If the attribute’s value is different from None, you calculate the order you should give it as follows:
    1. You build a QuerySet to retrieve all objects for the field’s model. You retrieve the model class the field belongs to by accessing self.model.
    2. If there are any field names in the for_fields attribute of the field, you filter the QuerySet by the current value of the model fields in for_fields. By doing so, you calculate the order with respect to the given fields.
    3. You retrieve the object with the highest order with last_item = qs.latest(self.attname) from the database. If no object is found, you assume this object is the first one and assign order 0 to it.
    4. If an object is found, you add 1 to the highest order found.
    5. You assign the calculated order to the field’s value in the model instance using setattr() and return it.
  2. If the model instance has a value for the current field, you use it instead of calculating it.

When you create custom model fields, make them generic. Avoid hardcoding data that depends on a specific model or field. Your field should work in any model.

You can find more information about writing custom model fields at https://docs.djangoproject.com/en/4.1/howto/custom-model-fields/.

Adding ordering to module and content objects

Let’s add the new field to your models. Edit the models.py file of the courses application, and import the OrderField class and a field to the Module model, as follows:

from .fields import OrderField
class Module(models.Model):
    # ...
    order = OrderField(blank=True, for_fields=['course'])

You name the new field order and specify that the ordering is calculated with respect to the course by setting for_fields=['course']. This means that the order for a new module will be assigned by adding 1 to the last module of the same Course object.

Now, you can edit the __str__() method of the Module model to include its order, as follows:

class Module(models.Model):
    # ...
    def __str__(self):
        return f'{self.order}. {self.title}'

Module contents also need to follow a particular order. Add an OrderField field to the Content model, as follows:

class Content(models.Model):
    # ...
    order = OrderField(blank=True, for_fields=['module'])

This time, you specify that the order is calculated with respect to the module field.

Finally, let’s add a default ordering for both models. Add the following Meta class to the Module and Content models:

class Module(models.Model):
    # ...
    class Meta:
        ordering = ['order']
class Content(models.Model):
    # ...
    class Meta:
        ordering = ['order']

The Module and Content models should now look as follows:

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=True)
    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.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')
    order = OrderField(blank=True, for_fields=['module'])
    class Meta:
            ordering = ['order']

Let’s create a new model migration that reflects the new order fields. Open the shell and run the following command:

python manage.py makemigrations courses

You will see the following output:

It is impossible to add a non-nullable field 'order' to content without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.
Select an option:

Django is telling you that you have to provide a default value for the new order field for existing rows in the database. If the field includes null=True, it accepts null values and Django creates the migration automatically instead of asking for a default value. You can specify a default value, or cancel the migration and add a default attribute to the order field in the models.py file before creating the migration.

Enter 1 and press Enter to provide a default value for existing records. You will see the following output:

Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>>

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. Choose the first option and enter 0 as the default value again. Finally, you will see an output similar to the following one:

Migrations for 'courses':
courses/migrations/0003_alter_content_options_alter_module_options_and_more.py
    - Change Meta options on content
    - Change Meta options on module
    - Add field order to content
    - Add field order to module

Then, apply the new migrations with the following command:

python manage.py migrate

The output of the command will inform you that the migration was successfully applied, as follows:

Applying courses.0003_alter_content_options_alter_module_options_and_more... OK

Let’s test your new field. Open the shell with the following command:

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

You have created a course in the database. Now, you will add modules to the course and see how their order is automatically calculated. You create an initial module and check its order:

>>> m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0

OrderField sets its value to 0, since this is the first Module object created for the given course. You can create a second module for the same course:

>>> m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1

OrderField calculates the next order value, adding 1 to the highest order for existing objects. Let’s create a third module, forcing a specific order:

>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5

If you provide a custom order when creating or saving an object, OrderField will use that value instead of calculating the order.

Let’s add a fourth module:

>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6

The order for this module has been automatically set. Your OrderField field does not guarantee that all order values are consecutive. However, it respects existing order values and always assigns the next order based on the highest existing order.

Let’s create a second course and add a module to it:

>>> c2 = Course.objects.create(subject=subject, title='Course 2', slug='course2', owner=user)
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0

To calculate the new module’s order, the field only takes into consideration existing modules that belong to the same course. Since this is the first module of the second course, the resulting order is 0. This is because you specified for_fields=['course'] in the order field of the Module model.

Congratulations! You have successfully created your first custom model field. Next, you are going to create an authentication system for the CMS.

Adding authentication views

Now that you have created a polymorphic data model, you are going to build a CMS to manage the courses and their contents. The first step is to add an authentication system for the CMS.

Adding an authentication system

You are going to use Django’s authentication framework for users to authenticate to the e-learning platform. Both instructors and students will be instances 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 file 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 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, you need to prepare the base template for your project. Edit the base.html template file and add the following content to it:

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

This is the base template that will be extended by the rest of the templates. In this template, you 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 JavaScript event listener for the DOMContentLoaded event. It allows you to execute code when the Document Object Model (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 with this chapter. Copy the static/ directory into the same directory of your project to use them. You can find the contents of the directory at https://github.com/PacktPublishing/Django-4-by-Example/tree/main/Chapter12/educa/courses/static.

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 logging out. Run the development server with the following command:

python manage.py runserver

Open http://127.0.0.1:8000/accounts/login/ in your browser. You should see the login page:

Figure 12.4: The account login page

Open http://127.0.0.1:8000/accounts/logout/ in your browser. You should see the Logged out page now, as shown in Figure 12.5:

Figure 12.5: The account logged out page

You have successfully created an authentication system for the CMS.

Additional resources

The following resources provide additional information related to the topics covered in this chapter:

Summary

In this chapter, you learned how to use fixtures to provide initial data for models. By using model inheritance, you created a flexible system to manage different types of content for the course modules. You also implemented a custom model field on order objects and created an authentication system for the e-learning platform.

In the next chapter, you will implement the CMS functionality to manage course contents using class-based views. You will use the Django groups and permissions system to restrict access to views, and you will implement formsets to edit the content of courses. You will also create a drag-and-drop functionality to reorder course modules and their content using JavaScript and Django.

Join us on Discord

Read this book alongside other users and the author.

Ask questions, provide solutions to other readers, chat with the author via Ask Me Anything sessions, and much more. Scan the QR code or visit the link to join the book community.

https://packt.link/django

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

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