django-badge

DISCLAIMER: Still some work to do here. Notably, missing the admin site, which is the last part of the article.

Backend Development with Django

This walkthrough provides a step-by-step guide to building the backend of the portfolio project using Django and Django REST Framework (DRF). Starting from environment setup and dependency installation, it moves through defining models, creating serializers, and constructing viewsets that expose the data as clean JSON APIs. Along the way, the backend is configured to handle media files for documents and images, the database is seeded with sample data, and API routes are registered with a router to keep endpoints consistent and predictable. By the end, it becomes a fully functional backend complete with a browseable API, ready to serve courses, projects, and articles to the Angular frontend.

Initializing the Backend

python3 -m venv .venv \
source .venv/bin/activate 
pip install django djangorestframework django-cors-headers Pillow
pip freeze > requirements.txt
django-admin startproject backend

The first step in constructing the backend of this portfolio project is setting up a dedicated Python environment and installing the core dependencies. In the terminal, begin by creating a virtual environment with python3 -m venv .venv and activating it with source .venv/bin/activate. This ensures that all packages and configurations remain isolated from the global system environment.

Once the environment is active, the essential libraries are installed for building a Django REST API:

  • Django – the main web framework.
  • Django REST Framework (DRF) – for creating API endpoints.
  • django-cors-headers – to handle cross-origin requests between the frontend and backend.
  • Pillow – an imaging library used by Django for managing uploaded image files.

After installation, the pip freeze > requirements.txt command captures these dependencies in a requirements.txt file, making it easy to reproduce the environment later. This CLI command should add six lines to the requirements.txt file with the following packages asigref, django, django-cors-headers, djangorestframework, pillow, and sqlparse. Finally, django-admin startproject backend generates the initial project structure under a folder named backend, providing the foundation for all further backend development, which leads to the next step where the third party apps, cors middleware, and media url must be added to the settings.py file as shown below.

backend/backend/settings.py

...
INSTALLED_APPS = [
    ...
    # third party apps, under django.contrib.*
    "rest_framework",
    "corsheaders"
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    ...
]

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
 

In settings.py, three key additions prepare the backend for API development and file handling. Adding rest_framework and corsheaders to INSTALLED_APPS enables the use of Django REST Framework for building API endpoints and configures cross-origin resource sharing so the Angular frontend can communicate with the backend. The CorsMiddleware entry in MIDDLEWARE ensures that CORS headers are applied to each request. Finally, defining MEDIA_URL and MEDIA_ROOT sets up a dedicated path for serving and storing uploaded media files, which is essential for handling images and various documents in the portfolio.

Next, navigate to the backend directory in the terminal before running the command before adding the local app to the settings.py file as shown below.

python manage.py startapp portfolio
INSTALLED_APPS = [
    ...
    # local apps
    "portfolio" 
    ...
]

The command python manage.py startapp portfolio generates the scaffolding for a new local Django app named portfolio. This includes essential files in a new backend/portfolio/ directory such as models.py, views.py, and admin.py, which define the core logic of the app. It’s important to run this command before adding "portfolio" to the INSTALLED_APPS list in backend/backend/settings.py, because Django must recognize the app’s directory and configuration in order to register it properly. If the app name is added to INSTALLED_APPS without first creating it, Django will raise an error since it cannot find the corresponding module. Once created and registered, the portfolio app becomes an active part of the backend project, ready to define models, views, and APIs specific to the portfolio system.

Creating the Models

With the portfolio app in place, the next step is to define the models that form the backbone of the backend. In Django, models act as the blueprint for the database, allowing us to describe data structures in Python code while Django translates them into the appropriate database schema—in this case, SQLite3. These models not only define the fields and relationships between different entities but also give us a high-level API for querying and managing data without writing raw SQL. For this project, we’ll be creating models that mirror the key elements of the portfolio: a Semester model to organize coursework by term, a Class model to represent individual courses, a Document model for associated files, a Project model to showcase hands-on work, and an Article model for markdown-based writeups. In the sections that follow, we’ll break these down in detail, starting with the necessary imports and then moving through each model step by step. All models are located in a single file within the backend directory called portfolio/models.py.

Imports for Models

from django.db import models
from django.utils.text import slugify
from django.utils import timezone
from pathlib import Path

At the top of the models.py file, there are a handful of libraries that power the functionality of the models. The core django.db.models module provides the base classes and field types used to define database models in Django. The slugify utility from django.utils.text allows us to convert names and titles into URL-friendly slugs, which are essential for clean and readable links. Timezone is also imported from django.utils to generate timestamps that respect Django’s timezone settings, ensuring consistent handling of dates and times. Finally, Python’s built-in Path from the pathlib library helps us work safely and flexibly with file system paths, which is particularly useful when constructing upload destinations for documents and images.

Semester Model:

class Semester(models.Model):
    label = models.CharField(max_length=64, unique=True) 
    year = models.PositiveIntegerField()
    sort_index = models.PositiveIntegerField(editable=False, unique=True)
    slug = models.SlugField(max_length=80, unique=True, blank=True)
    expanded = models.BooleanField(default=False)
    
    class Meta:
        ordering = ["sort_index", "id"]
        
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.label)

        if not self.sort_index:
            last_semester = Semester.objects.filter(year=self.year).order_by("-sort_index").first()
            if last_semester:
                self.sort_index = last_semester.sort_index + 1
            else:
                self.sort_index = self.year * 100

        super().save(*args, **kwargs)
    
    def __str__(self):
        return self.label

The class Semester model defines the structure for grouping courses by academic term, serving as the organizational layer for the portfolio. It stores a label (e.g., "Summer & Fall 2023”), the year, and a sort_index that allows custom ordering of terms. A slug field is generated automatically from the label using slugify, creating URL-friendly identifiers, while the expanded boolean allows us to toggle whether a semester is open or collapsed in the frontend display. In the class Meta, the ordering ensures that semesters are retrieved consistently by sort index and ID. In the save method, both the slug and sort_index are set dynamically: if no slug exists, one is created from the label, and if no sort index exists, it is generated based on the year and incremented relative to previous semesters. Crucially, the Semester model is directly tied to the Class model through a ForeignKey relationship in Class. This means each course belongs to a specific semester, and in turn, a semester can contain multiple classes. This relationship allows us to group courses neatly within their academic term, reinforcing the hierarchical structure of the portfolio.

Class Model:

class Class(models.Model):
    name = models.CharField(max_length=200) # e.g. "Linux Foundation"
    slug = models.SlugField(max_length=220, unique=True, blank=True) # e.g. "d281-linux-foundation"
    code = models.CharField(max_length=32, blank=True) # e.g. "D281"
    description = models.TextField(blank=True)
    semester = models.ForeignKey(Semester, on_delete=models.PROTECT, related_name="classes", null=True, blank=True)
    
    def save(self, *args, **kwargs):
        if not self.slug:
            base = self.code or self.name
            self.slug = slugify(base)
        super().save(*args, **kwargs)
    
    def __str__(self):
        return self.code or self.name

The class Class model represents individual courses within the portfolio, such as “Linux Foundations”. Its fields include a name for the course title, an optional code for identifiers like WGU’s course codes, a description for additional details, and a slug that generates a clean URL-friendly identifier based on the code or name. The key relationship here is the foreign key (ForeignKey) to the class Semester model, which ties each class to a specific academic term. This is defined with on_delete=models.PROTECT to ensure that deleting a semester will not automatically delete its associated classes, preserving the course data. The related_name="classes" option makes it easy to access all classes belonging to a semester in a reverse lookup (e.g., semester.classes.all()). In the def save(...) method, the model ensures that every class has a slug: if none is provided, Django uses either the course code or name to generate one. Finally, the def __str__(...) method overrides the default string representation of the model, improving readability in the Django admin by displaying either the course code or the course name when the object is referenced. Together, these features make the Class model a flexible yet structured way to represent coursework within each semester of the portfolio.

Document Model:

def class_document_upload_path(instance, filename):
    # MEDIA_ROOT/courses/<class_slug>/<order>_<safe-title>.pdf
    safe = slugify(instance.title or "document")
    return f"classes/{instance.course.slug}/{instance.order:03d}_{safe}{Path(filename).suffix}"


class Document(models.Model):
    course = models.ForeignKey(Class, on_delete=models.CASCADE, related_name="documents")
    title = models.CharField(max_length=200)
    tab_label = models.CharField(max_length=100)
    category = models.CharField(max_length=50, blank=True)
    order = models.PositiveIntegerField(default=1)
    file = models.FileField(upload_to=class_document_upload_path)
    is_public = models.BooleanField(default=True)
    created_at = models.DateTimeField(default=timezone.now, editable=False)

    class Meta:
        ordering = ["id", "order"]
        unique_together = [("course", "tab_label")] # prevents duplicate tab names within a class

    def __str__(self):
        return f"{self.course} - {self.tab_label}"

The class Document model is designed to store course-related files, such as lecture notes, assignments, or study guides. Each document is linked to a specific Class through a foreign key, meaning a single course can have multiple associated documents, but each document belongs to only one course. Unlike the Semester–Class relationship, which uses on_delete=models.PROTECT to safeguard entire courses from being deleted accidentally, the Class–Document relationship uses on_delete=models.CASCADE. This ensures that when a class is removed, all of its associated documents are deleted as well, keeping the database clean of orphaned files. The file field uses a custom upload_to function, class_document_upload_path(...), which organizes uploads into directories based on the course slug and orders them numerically (e.g., classes/d281-linux-foundation/001_discrete-mathematics.pdf). Other fields give the model flexibility and structure: title stores a descriptive name, tab_label provides a shorter identifier for frontend navigation, and category allows grouping by type (e.g., “Notes,” “Homework”). The order field controls display order, while is_public determines whether a document is visible to users. The created_at timestamp records when each file was added. Finally, the unique constraint on ("course", "tab_label") prevents duplicate tab names within the same class, ensuring clarity for users. Together, these features make the Document model a robust system for managing and organizing course files in a way that mirrors real-world academic structure.

Project Model

def project_image_upload_path(instance, filename):
    # MEDIA_ROOT/projects/<project_slug>/<filename>
    safe = slugify(instance.name or "image")
    return f"projects/images/{safe}{Path(filename).suffix}"

class Project(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    category = models.CharField(max_length=50, blank=True)
    image = models.ImageField(upload_to="projects/images/", blank=True)
    someBool = models.BooleanField(default=False)
    sort_index = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(default=timezone.now, editable=False)
    updated_at = models.DateTimeField(default=timezone.now, editable=True)
    
    class Meta:
        ordering = ["sort_index", "id"]
        unique_together = [("name", "slug")]

    def save(self, *args, **kwargs):
        if not self.slug:
            base = self.code or self.name
            self.slug = slugify(base)
        super().save(*args, **kwargs)
    
    def __str__(self):
        return self.name

The class Project model represents hands-on work and portfolio pieces, such as web apps, data science notebooks, or design projects. It includes core descriptive fields like name, description, and an optional category for grouping different types of projects. A slug is generated automatically for clean URLs, while the image field allows each project to have an associated thumbnail or preview image, stored in the projects/images/ directory. The sort_index field provides flexibility for ordering projects on the frontend, and timestamp fields (created_at and updated_at) track when projects are added or modified. A unique constraint on ("name", "slug") ensures that no two projects accidentally share the same identifiers, maintaining consistency in database records and URLs. Unlike the Document model, which cascades deletions when a Class is removed, the class Project model stands independently — it is not tied to higher-level structures like semesters or classes. Instead, it serves as a parent model for class Articles, with each project able to contain multiple writeups or detailed explanations of the work it showcases. This one-to-many relationship allows projects to be documented in depth, while keeping all related articles neatly grouped under their respective project. Finally, the save(...) method enforces slug generation from the project name if none is provided, ensuring every project can be referenced reliably. Together, these fields and relationships make the class Project model the central container for displaying and organizing portfolio work.

Article Model

def article_upload_path(instance):
    # MEDIA_ROOT/projects/<project_slug>/<filename>
    safe = slugify(instance.title)
    return f"projects/{instance.project.slug}/{safe}.md"

class Article(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="articles")
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    category = models.CharField(max_length=50, blank=True) # markdown
    file = models.FileField(upload_to=article_upload_path, blank=True)
    image = models.ImageField(upload_to={"projects/images/"}, blank=True)
    whatABool = models.BooleanField(default=False)
    sort_index = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(default=timezone.now, editable=False)
    updated_at = models.DateTimeField(default=timezone.now, editable=True)
    
    class Meta:
        ordering = ["sort_index", "id"]
        unique_together = [("project", "slug")]

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)
    
    def __str__(self):
        return self.title

The class Article model is designed to provide written documentation and detailed explanations for each project, making it the natural child of the class Project model. Through its foreign key relationship with Project—defined with on_delete=models.CASCADE—an article cannot exist independently, and will be removed if its parent project is deleted. This enforces data integrity while ensuring that articles always remain tied to their respective projects. The related_name="articles" option also allows projects to easily query all of their associated articles (e.g., project.articles.all()). Articles are stored as Markdown files, with the file field configured to use a custom article_upload_path(...) function. This organizes files neatly inside a project-specific folder, producing paths like projects/my-project/my-article.md. Alongside the file, fields such as title, slug, and category provide structure and readability, while an optional image gives each article a visual element for previews or thumbnails. The sort_index controls display order, and timestamp fields (created_at and updated_at) track changes over time. Finally, a unique constraint on ("project", "slug") guarantees that no two articles within the same project share the same identifier, ensuring reliable URLs. Together, these features make the class Article model a flexible yet organized way to expand on projects with in-depth writeups, tutorials, or reflections.

Serializing the Data

With the models defined, the next step is to make their data usable outside of the Django backend. This is where serializers come in. In Django REST Framework (DRF), serializers act as the bridge between complex Python objects—like model instances—and formats such as JSON that can be consumed by the frontend. Put simply: models define how data is stored, while serializers define how data is presented. This separation of concerns ensures that the backend can maintain clean database structures while still delivering data in a format that the Angular frontend (or any client) can easily work with. In this project, we’ll be creating serializers for the portfolio’s core structures: Semester, Class, Document, Project, and Article. Each serializer is tailored to return the right level of detail, whether that’s a compact representation for list views or a more detailed version for individual records. In the subsections that follow, we’ll break these down one by one, showing how they map to the models and prepare the data for API endpoints. All serializers are located in a single file called portfolio/serializers.py.

Imports for Serializers

from rest_framework import serializers
from .models import Class, Document, Semester, Article, Project

At the top of the serializers file, import the tools needed to connect the models to Django REST Framework. The serializers module from rest_framework provides base classes such as ModelSerializer, which automatically generate fields from model definitions and handle conversion between Python objects and JSON. Also import the five core models from the portfolio app—Class, Document, Semester, Article, and Project—so that each one can be paired with its corresponding serializer. Together, these imports form the foundation for building structured API representations of the backend data.

Document Serializer

class DocumentSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Document
        fields = ["id", "title", "tab_label", "category", "order", "file_url", "is_public", "created_at"]

    def get_file_url(self, obj):
        req = self.context.get("request")
        if not getattr(obj, "file", None):
            return None
        return req.build_absolute_uri(obj.file.url) if req else obj.file.url

The class DocumentSerializer converts class Document model instances into JSON so that course files can be displayed in the frontend. It exposes key fields like title, tab_label, category, order, visibility (is_public), and timestamps, giving the client all the context it needs to organize and present course materials. In addition to these, it defines a custom field, file_url, using SerializerMethodField. This method constructs a full, absolute URL to the uploaded file by checking the current request context. If the request is available, it returns a complete path (e.g., http://localhost:8000/media/...), ensuring the frontend can directly link to and download the file. If no request is passed, it falls back to the relative file path. This setup makes the DocumentSerializer especially useful for handling files in a clean, user-friendly way that bridges the backend storage system with the frontend interface. Notably, the raw file field is omitted from the serializer. While the model stores this information, the raw field only contains the internal storage path, which is less useful to the frontend. By providing file_url instead, the serializer delivers exactly what the client needs—a ready-to-use link—without exposing unnecessary implementation details. This approach makes the DocumentSerializer especially effective at bridging backend file storage with a clean, user-friendly frontend interface.

Class Serializer

class ClassBaseSerializer(serializers.ModelSerializer):
    documents = DocumentSerializer(many=True, read_only=True)
    class Meta:
        model = Class
        fields = ["id", "name", "code", "slug", "description", "documents"]

The class ClassBaseSerializer handles the conversion of class Class model instances into JSON, exposing essential course information such as name, code, slug, and description. To give each class richer context, it also nests the class DocumentSerializer under the documents field. This means that whenever a class is serialized, all of its related documents are automatically included in the output, allowing the frontend to fetch both the course details and its supporting files in a single API response. The many=True option signals that multiple documents can be returned, while read_only=True ensures that documents are only exposed for reading and cannot be directly modified through this serializer. Like with the class DocumentSerializer, the fields chosen here are intentional: only the attributes most relevant to displaying classes are included, keeping the response lightweight. By embedding documents directly inside classes, this serializer captures the natural hierarchy of the data—where each course serves as a container for its study materials—making it straightforward for the frontend to render structured, course-based views.

Semester & Semester Mini Serializer

class SemesterSerializer(serializers.ModelSerializer):
    classes = ClassBaseSerializer(many=True, read_only=True)
    class Meta:
        model = Semester
        fields = ["id", "label", "year", "sort_index", "slug", "classes", "expanded"]

class SemesterMiniSerializer(serializers.ModelSerializer):
    class Meta:
        model = Semester
        fields = ["id", "label", "year", "sort_index", "slug", "expanded"]

The serializers for Semester come in two forms: a full version and a simplified version. The SemesterSerializer exposes all of a semester’s core fields—label, year, sort_index, slug, and expanded—and nests the class ClassBaseSerializer so that every class within the semester is included in the output. This makes it ideal for semester-focused views where the frontend needs to display a complete term along with its courses and their associated documents. The SemesterMiniSerializer, on the other hand, strips away the nested classes and returns only the semester’s basic details. This leaner serializer is especially useful when displaying semester context in places where listing every class would create unnecessary duplication. For example, in the class ClassDetailSerializer, the mini version is used so that a class can show which semester it belongs to without also pulling in every other class from that term. Separating these two serializers balances detail and efficiency, ensuring that the API only provides as much information as each view requires.

Class Detail Serializer

class ClassDetailSerializer(ClassBaseSerializer):
    semester = SemesterMiniSerializer(read_only=True)
    class Meta(ClassBaseSerializer.Meta):
        fields = ClassBaseSerializer.Meta.fields + ["semester"]

The class ClassDetailSerializer extends the class ClassBaseSerializer by adding contextual information about the semester each class belongs to. Instead of embedding the full class SemesterSerializer (which would include every other class in the same term), it uses the class SemesterMiniSerializer to return just the semester’s basic details—label, year, sort_index, slug, and expanded. This approach keeps the API response concise while still showing the academic context of a class. By inheriting from class ClassBaseSerializer, the serializer automatically includes all of the class’s core fields as well as its related documents. The semester field is then layered on top, providing a lightweight link back to the term without unnecessary nesting. In practice, the class ClassBaseSerializer is best suited for list views where multiple classes are displayed together with their documents, while the class ClassDetailSerializer is designed for single-class views that require additional context about the semester. This separation ensures that each endpoint returns just the right amount of information for its use case, avoiding circular or overly large responses.

Article & Project Serializers

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ["id", "project_id", "title", "slug", "category", "file", "image", "whatABool", "sort_index", "created_at", "updated_at"]

class ProjectSerializer(serializers.ModelSerializer):
    articles = ArticleSerializer(many=True, read_only=True)
    class Meta:
        model = Project
        fields = ["id", "name", "description", "category", "slug", "image", "articles", "someBool", "sort_index", "created_at", "updated_at"]

The class ArticleSerializer provides a straightforward mapping of the Article model into JSON, exposing all of its key fields such as title, slug, category, associated file and image, ordering (sort_index), and timestamps. Including the project_id ensures that each article can be tied back to its parent project, even when the project itself isn’t fully serialized in the response. This makes it well-suited for article-focused views, where detailed information about individual writeups is required. The class ProjectSerializer builds on this by embedding the class ArticleSerializer in the articles field. This establishes a clear one-to-many relationship, allowing each project to return not only its own metadata (name, description, slug, category, image, etc.) but also the list of articles associated with it. The many=True and read_only=True settings ensure that multiple articles can be included without direct modification through the project serializer. Together, these serializers mirror the structure defined in the models: projects act as parent containers for articles, while articles provide detailed writeups and documentation for each project. In practice, the class ArticleSerializer is useful for displaying or managing individual writeups, while the class ProjectSerializer is better for portfolio-style views where projects are presented alongside all of their related articles. This balance between detail and hierarchy ensures the API provides just the right amount of context depending on the client’s needs.

In summary, the serializers bridge the gap between the Django models and the JSON data the frontend consumes. By carefully structuring serializers for Semester, Class, Document, Project, and Article, each model’s data is presented at the right level of detail depending on context—compact for lists, and richer for detail views. Nesting serializers, like including documents within classes or articles within projects, mirrors the natural hierarchy of the portfolio while still keeping responses efficient. The separation between full and “mini” serializers avoids duplication and overly complex payloads, while custom fields such as file_url provide frontend-ready links without exposing internal storage details. Altogether, these serializers give the backend a flexible and consistent way to serve structured, client-friendly data through the API, setting the stage for building the views and endpoints that will power the portfolio.

Constructing the Views

With the models and serializers in place, the next step is to connect them through Django’s views, which serve as the entry points for client requests. In a Django REST Framework project, the views.py file defines how incoming HTTP requests—such as fetching a list of classes, retrieving a project, or loading a document—are handled and what data is returned. Views sit between the models and serializers, pulling the right records from the database and passing them through the appropriate serializer before sending them back to the client as JSON. By organizing the views into ViewSets, RESTful endpoints for listing can be quickly created, retrieved, and and behavior customized with additional actions. An added benefit of this setup is that DRF automatically generates a browseable API, allowing developers to explore endpoints, view serialized data, and test requests directly in the browser—making it an invaluable tool during development. All of the following code for views are located in a single file called portfolio/views.py.

Imports for Views

from django.http import HttpResponse
from rest_framework import viewsets, permissions
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from .models import Class, Semester, Document, Project, Article
from .serializers import ClassBaseSerializer, ClassDetailSerializer, SemesterSerializer, DocumentSerializer, ProjectSerializer, ArticleSerializer
from .permissions import ReadOnly

At the top of the views.py file, the libraries needed to connect the models and serializers to Django REST Framework’s request-handling system are imported. From Django itself, HttpResponse for returning plain text responses when needed is brought in. From DRF (rest_framework), viewsets to build RESTful endpoints with minimal boilerplate, permissions to control access, Response to standardize API outputs, and action to define custom routes beyond the standard CRUD operations are all imported. The utility get_object_or_404 is also included to safely retrieve model instances, returning a 404 error if an object is not found. Finally, the portfolio models—Class, Semester, Document, Project, and Article—are imported alongside their corresponding serializers so that each viewset can query the database and return structured JSON data. Together, these imports form the foundation for constructing the API endpoints that will expose the portfolio’s backend.

Courses View Sets


class SemesterViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [ReadOnly]
    lookup_field = "slug"
    queryset = Semester.objects.all().prefetch_related("classes")
    serializer_class = SemesterSerializer
    
class ActionSerializerMixin:
    serializer_action_classes = {}
    
    def get_serializer_class(self):
        return self.serializer_action_classes.get(self.action, self.serializer_class)
    
class ClassViewSet(ActionSerializerMixin, viewsets.ReadOnlyModelViewSet):
    permission_classes = [ReadOnly]
    lookup_field = "slug"
    queryset = Class.objects.all()
    serializer_class = ClassBaseSerializer
    serializer_action_classes = {
        "retrieve": ClassDetailSerializer,
        "list": ClassBaseSerializer,
    }
    
    def get_queryset(self):
        if self.action == "retrieve":
            return (self.queryset
                    .select_related("semester")
                    .prefetch_related("documents"))
        return self.queryset.prefetch_related("documents")

    @action(detail=True, methods=["get"])
    def documents(self, request, slug=None):
        cls = self.get_object()
        ser = self.get_serializer(cls, context={"request": request})
        return Response(ser.data["documents"])

class DocumentViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [ReadOnly]
    queryset = Document.objects.all()
    serializer_class = DocumentSerializer
    

The Courses endpoints are organized around three viewsets,SemesterViewSet, ClassViewSet, and DocumentViewSet, plus a small ActionSerializerMixin to swap serializers per action. SemesterViewSet exposes terms by slug and prefetches related classes to avoid N+1 queries when listing semesters. The ActionSerializerMixin enables action-specific serializers; ClassViewSet uses it to return a lightweight class list (ClassBaseSerializer for list) and a richer, context-aware detail (ClassDetailSerializer for retrieve). Classes are looked up by slug and the queryset is optimized: retrieve adds select_related("semester") and prefetch_related("documents"), while list prefetches documents to keep responses fast. A custom route, @action detail=True documents, returns just the serialized documents for a given class—handy for UI panels that load materials on demand without refetching the entire class payload. Finally, DocumentViewSet provides the standard ReadOnly interface for documents themselves. At this point in time, full CRUD is not needed from the frontend, so ReadOnly is not only sufficient but also more secure. Together, these viewsets deliver efficient, slug-based navigation for semesters and classes, sensible detail vs. list payloads, and targeted endpoints for course materials—all of which appear in DRF’s Browseable API for quick in-browser exploration during development.

Project View Sets

class ProjectViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [ReadOnly]
    lookup_field = "slug"
    queryset = Project.objects.all().prefetch_related("articles")
    serializer_class = ProjectSerializer
    
    @action(detail=True, methods=["get"])
    def articles(self, request, slug=None):
        project = self.get_object()
        ser = self.get_serializer(project, context={"request": request})
        return Response(ser.data["articles"])
    
    @action(detail=True, methods=["get"], url_path="articles/(?P<article_slug>[^/.]+)")
    def article_detail(self, request, slug=None, article_slug=None):
        project = self.get_object()
        article = get_object_or_404(project.articles.all(), slug=article_slug)
        ser = ArticleSerializer(article, context={"request": request})
        return Response(ser.data)

class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [ReadOnly]
    lookup_field = "slug"
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    
    @action(detail=True, methods=["get"], url_path="raw", permission_classes=[permissions.IsAuthenticatedOrReadOnly])
    def raw(self, request, slug=None):
        article = self.get_object()
        f = getattr(article, "file", None)
        if not f:
            return HttpResponse("No file attached", status=404, content_type="text/plain; charset=utf-8")
    
        f.open("rb")
        try:
            content = f.read().decode("utf-8", errors="replace")
        finally:
            f.close()
        return HttpResponse(content, content_type="text/plain; charset=utf-8")

The projects area uses two viewsets, ProjectViewSet and ArticleViewSet, both subclassing ReadOnlyModelViewSet so only list/retrieve routes are exposed (no writes). Each sets permission_classes = [ReadOnly] at the class level, enforcing read-only access on the standard endpoints and on any custom actions that don’t override permissions. ProjectViewSet looks up projects by slug and prefetches articles for efficient list/detail responses. It also exposes two custom routes: GET /projects/{slug}/articles/ returns the project’s articles (via the nested ArticleSerializer), and GET /projects/{slug}/articles/{article_slug}/ uses get_object_or_404 scoped to the project to fetch a single article safely, which is ideal for detail pages and preventing cross-project leakage; both inherit the class-level ReadOnly permissions. ArticleViewSet also uses slug lookups and adds a raw action, GET /articles/{slug}/raw, that streams the markdown file as text/plain. This action sets permission_classes=[permissions.IsAuthenticatedOrReadOnly], which keeps GET publicly accessible while requiring auth for any non-safe methods (though this action only registers GET). Together, these endpoints provide clean, slug-based navigation, efficient prefetching, and a few extras (nested article lists and raw markdown) with clear, per-action permission behavior, all while staying read-only by default.

Registering the Endpoints

With models, serializers, and views in place, the final step in exposing the backend is to register the endpoints that clients can call. In Django REST Framework, this is done through the project’s urls.py file, where a DefaultRouter automatically generates RESTful routes for each registered viewset. By mapping the core viewsets—classes, semesters, documents, projects, and articles—to URL patterns under the /api/ path, a clean and predictable API structure is created. This setup makes it straightforward for the Angular frontend (or any client) to fetch data, whether that means listing all classes, retrieving a specific project by slug, or accessing an article in raw markdown form. The router also integrates seamlessly with DRF’s Browseable API, allowing developers to explore these endpoints directly in the browser during development. All endpoints are registered in a single file called backend/urls.py.

backend/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from portfolio.views import ClassViewSet, SemesterViewSet, DocumentViewSet, ProjectViewSet, ArticleViewSet

router = DefaultRouter()
router.register(r"classes", ClassViewSet, basename="class")
router.register(r"semesters", SemesterViewSet, basename="semesters")
router.register(r"documents", DocumentViewSet, basename="documents")
router.register(r"projects", ProjectViewSet, basename="projects")
router.register(r"articles", ArticleViewSet, basename="articles")

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include(router.urls)), # -> /api/classes/
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

In the urls.py file, all API routes are registered and exposed to clients. A DefaultRouter from Django REST Framework is used to automatically generate RESTful routes for each viewset, saving us from having to manually define paths for list and detail views. Here, the ClassViewSet, SemesterViewSet, DocumentViewSet, ProjectViewSet, and ArticleViewSet are registered, which creating a clean API namespace under /api/. For example, /api/classes/ returns all classes, while /api/projects/<slug>/articles/ returns the articles for a specific project. The urlpatterns also include the default Django admin at /admin/, and finally, the static() helper is appended so that uploaded media files (like PDFs or images) can be served during development. Together, this configuration ties the backend together, making every model accessible through predictable, well-structured API endpoints.

Example API Endpoints:

  1. GET /api/classes/
  2. GET /api/classes/<slug>/
  3. GET /api/classes/<slug>/documents/
  4. GET /api/semesters/
  5. GET /api/semesters/<slug>/
  6. GET /api/projects/
  7. GET /api/projects/<slug>/
  8. GET /api/projects/<slug>/articles/
  9. GET /api/projects/<slug>/articles/<slug>/

Seeding the Database

With the backend fully structured, the final step before development and testing is to seed the database with initial data. Django supports this through fixtures, which are JSON or YAML files that preload the database with model instances. In this project, all seed data is stored in a single file, portfolio/fixtures/seed.json. By loading this fixture, the backend instantly populates with sample semesters, classes, documents, projects, and articles, giving the API meaningful data to work with right away. This not only speeds up development and frontend integration, but also ensures that the portfolio has a consistent, ready-to-use baseline for demos or resets.

Sample Semester Data


[
    {
        "model": "portfolio.semester",
        "pk": 1,
        "fields": { "label": "Fall & Summer 2023", "year": 2023, "sort_index": 202300, "slug": "fall-summer-2023", "expanded": false }
    },
    {
        "model": "portfolio.semester",
        "pk": 2,
        "fields": { "label": "Spring & Winter 2024", "year": 2024, "sort_index": 202401, "slug": "spring-winter-2024", "expanded": false }
    },

The sample semester entries in the seed file establish the academic terms that classes will be grouped under. Each object specifies the model (portfolio.semester), a primary key (pk), and the field values. The label provides a human-readable name such as “Fall & Summer 2023”, while the year and sort_index fields control chronological ordering. The slug offers a URL-friendly identifier for API lookups, and the expanded flag determines whether the semester should default to an expanded state in the frontend. By including multiple terms in the fixture, the backend is immediately able to organize classes under their correct semesters once the data is loaded.

Sample Class Data

    {
        "model": "portfolio.class",
        "pk": 1,
        "fields": { "name": "Intro to Computer Science: C++", "code": "CS11", "slug": "cs11", "description": "", "semester": 1}
    },
    ...
    {
        "model": "portfolio.class",
        "pk": 28,
        "fields": { "name": "Business of IT: Applications", "code": "D336", "slug": "d336", "description": "", "semester": 4}
    },

The class entries in the seed file represent individual courses that belong to a specific semester. Each object maps to the portfolio.class model and defines fields such as the name (e.g., Intro to Computer Science: C++), a short code for quick reference, and a slug for clean URLs. The optional description field is left empty here but can be used for additional course details. Most importantly, the semester field links the class to its parent semester by primary key, ensuring that when the data is loaded, each course is properly grouped under the correct academic term. By including a wide range of courses, the fixture pre-populates the database with meaningful structure for the portfolio’s Courses page.

Sample Document Data

    {
        "model": "portfolio.document",
        "pk": 1,
        "fields": {
            "course": 25,
            "title": "D281: Linux Foundations Notes",
            "tab_label": "Notes",
            "category": "notes",
            "order": 1,
            "file": "classes/d281/001_linux-foundations.pdf",
            "is_public": true
        }
    },
    {
        "model": "portfolio.document",
        "pk": 2,
        "fields": {
            "course": 7,
            "title": "MATH 70: Calculus 2 Notes",
            "tab_label": "Notes",
            "category": "notes",
            "order": 1,
            "file": "classes/math70/001_calculus-2-notes.pdf",
            "is_public": true
        }
    },
    {
        "model": "portfolio.document",
        "pk": 3,
        "fields": {
            "course": 7,
            "title": "MATH 70: Calculus 2 Homework",
            "tab_label": "Homework",
            "category": "homework",
            "order": 2,
            "file": "classes/math70/002_calculus-2-hw.pdf",
            "is_public": true
        }
    },

The document entries in the seed file preload study materials such as notes and homework assignments for specific courses. Each object maps to the portfolio.document model and includes a course field, which links the document back to its parent class by primary key. The title gives a descriptive name, while the shorter tab_label is used in the frontend to organize documents under course tabs (e.g., Notes or Homework). The category allows further grouping, and the order field determines how documents are sorted within a class. The file field provides the relative path to the uploaded PDF in the media directory, and the is_public flag specifies whether the document should be visible to users. By seeding with examples like Linux Foundations notes and Calculus homework, the database is immediately populated with realistic content, enabling the Courses page to showcase both course structure and related files.

Sample Project Data

    {
        "model": "portfolio.project",
        "pk": 1,
        "fields": {
            "name": "Portfolio Website",
            "description": "This is the portfolio website project currently being viewed. It was built to share the courses that I took, host some of the projects that I have worked on, and share some articles related to each project.",
            "category": "application",
            "slug": "portfolio-website",
            "image": "projects/images/portfolio-icon.png",
            "someBool": false,
            "sort_index": 1001
        }
    },

The project entries in the seed file initialize portfolio items that showcase hands-on work. Each object maps to the portfolio.project model and contains descriptive fields such as the name (Portfolio Website), a longer description that explains its purpose, and a category to classify the type of project (e.g., application). The slug provides a clean identifier for project URLs, while the image field links to a thumbnail or icon stored in the media directory. Additional fields like someBool (a placeholder boolean) and sort_index control custom project behavior and ordering on the frontend. By seeding the portfolio website itself as the first project, the database is ready with a concrete example that demonstrates how projects and their related articles will be displayed in the application.

Sample Article Data

    {
        "model": "portfolio.article",
        "pk": 1,
        "fields": {
            "project": 1,
            "title": "Backend Development with Django",
            "slug": "backend-development-with-django",
            "category": "Article: Walkthrough",
            "file": "projects/portfolio-website/backend-development-with-django.md",
            "image": "projects/images/django-badge.png",
            "whatABool": false,
            "sort_index": 1001
        }
    },
...
]

The article entries in the seed file provide detailed writeups connected to specific projects. Each object maps to the portfolio.article model and includes a project field, which ties the article back to its parent project by primary key. Core fields like title, slug, and category define the article’s identity and allow for clean API lookups and grouping. The file field points to a Markdown file in the media directory, enabling articles to store and serve rich, formatted content, while the optional image gives each article a visual icon or badge. As with projects, the whatABool field functions as a placeholder boolean, and the sort_index controls display order. By seeding an example article such as Backend Development with Django, the fixture immediately populates the Projects page with meaningful documentation, demonstrating how projects and articles work together to showcase portfolio content.

Together, the seeded semesters, classes, documents, projects, and articles form a complete dataset that mirrors the real structure of the portfolio. Semesters provide the organizational framework, classes define the individual courses, and documents supply supporting files like notes and homework. Projects serve as standalone showcases of applied work, while articles add depth through detailed explanations and tutorials. By loading this fixture into the database, the backend is instantly populated with interconnected content that demonstrates the full functionality of the system. This not only accelerates frontend development and testing but also provides a consistent baseline for demos and resets, ensuring that the portfolio is always ready to be explored through the API or the browseable interface.

Create media folder in backend/media/classes:

In addition to seeding the database, the media directory also needs to be prepared so that uploaded files—such as class notes, homework, project images, and article markdown—can be stored and served correctly. Django uses the MEDIA_ROOT and MEDIA_URL settings defined earlier to know where to save and retrieve these files. In this project, media is organized under backend/media/, with sub-folders for each type of content. For example, course documents are placed in media/classes/<class_slug>/, while project assets live under media/projects/. To test this setup, create the folders manually by running the following command:

mkdir -p media/classes/d281

and then add a sample file, such as 001_linux-foundations.pdf, to media/classes/d281/. Once this is in place, hitting the corresponding API endpoint for the class will return a file URL that serves this document, demonstrating how media files flow from storage to the frontend.

A simplified folder structure looks like this:

backend/
 └── media/
     ├── classes/
     │    └── d281/
     │         └── 001_linux-foundations.pdf
     └── projects/
          ├── images/
          │    └── portfolio-icon.png
          └── portfolio-website/
               └── backend-development-with-django.md

This structure ensures that every piece of uploaded content—whether tied to a class or a project—has a predictable and organized home in the file system, making it easy for Django to serve files through the API.

loaddata, migrate, and run server

With the models, serializers, views, endpoints, fixtures, and media folders in place, the last step is to bring the backend to life. First, load the seed data with:

python manage.py loaddata portfolio/fixtures/seed.json

This command prepopulated the database with the semesters, classes, documents, projects, and articles defined in the fixture file. Next, generate and apply any pending migrations with:

python manage.py makemigrations
python manage.py migrate

These ensure that the database schema matches the latest model definitions. Finally, launch the development server using:

python manage.py runserver

At this point, the backend is fully operational. The API can be explored through the Browseable API at http://localhost:8000/api/, confirm that seeded data is returned at the various endpoints, and even retrieve media files like PDFs or images directly through their file URLs. This completes the backend setup and provides a solid foundation for connecting to the Angular frontend.

Summary

By walking through each stage of this setup—initializing the backend environment, defining models, serializing data, constructing views, registering endpoints, seeding the database, and preparing media files—we’ve built a complete and functional Django REST Framework backend for the portfolio project. The system is now capable of organizing semesters, classes, and documents, while also showcasing projects and their related articles, all served through clean, predictable API endpoints. With the browseable API available for development and a consistent dataset seeded into the database, the backend provides a stable, extensible foundation that can be readily connected to the Angular frontend. This backend not only demonstrates best practices in structuring a Django project but also serves as a scalable base for future growth, whether that means adding new content, expanding features, or deploying to production.