Django Project from Scratch — Tutorials 1 to 14

Project Title

TeamBoard Pro

Project Description

TeamBoard Pro is a complete Django project designed to practically apply the subjects covered in Tutorials 1 to 14. It is a role-based content and management platform where users can register, log in, create articles, manage dashboards, use secure permissions and groups, benefit from middleware and custom decorators, and integrate advanced Django concepts such as logging, caching, transactions, model validation, custom managers, environment variables, and production-ready settings.

The project is intentionally structured to be educational and realistic at the same time. It is not only a CRUD application. It is a small but serious platform that demonstrates how advanced Django architecture works in a real-world context. It includes middleware for request timing and access logging, custom decorators for reusable access rules, custom model managers and QuerySets, model validation for safe business rules, transactional order-like workflows, caching for expensive pages, structured logging, environment-based settings, security best practices, and permissions/groups for role-based access control.

1. Tutorials Covered (1 to 14)

Tutorial 1 — Django Middleware Explained

Used in the project through custom middleware for request logging and request timing.

Tutorial 2 — Writing Custom Middleware

Applied by creating reusable middleware classes in core/middleware.py.

Tutorial 3 — Advanced ORM Techniques

Used with annotations, select_related, prefetch_related, custom filtering, and optimized dashboard queries.

Tutorial 4 — Django Managers and Custom QuerySets

Used in the Article model to provide reusable query methods like published(), featured(), and recent().

Tutorial 5 — Django Model Validation

Used in models such as Article and ProjectTask to enforce business rules through clean().

Tutorial 6 — Transactions in Django

Used when publishing articles and creating audit log records together using transaction.atomic().

Tutorial 7 — Raw SQL in Django

Included in a reporting utility to demonstrate how a dashboard statistic can be queried directly when needed.

Tutorial 8 — Caching in Django

Used for homepage featured content and dashboard statistics.

Tutorial 9 — Django Logging

Used with structured logging configuration and practical logging calls in views and middleware.

Tutorial 10 — Django Settings Best Practices

Applied through split settings structure: base.py, development.py, and production.py.

Tutorial 11 — Environment Variables in Django

Used for secret key, debug mode, allowed hosts, email, and database config.

Tutorial 12 — Django Security Best Practices

Applied through secure settings, CSRF-safe forms, role checks, protected views, and safe configuration.

Tutorial 13 — Django Permissions and Groups

Used to create roles such as Authors, Editors, and Managers.

Tutorial 14 — Custom Decorators in Django

Used for reusable access checks like staff_required, group_required, and author_required.

2. Project Idea

Main Goal

Build a collaborative internal publishing and task management platform.

Main Features

  • User registration and login
  • Role-based dashboards
  • Article creation and publishing workflow
  • Task management for teams
  • Secure permissions and group-based access
  • Caching for performance
  • Logging and middleware-based monitoring
  • Production-ready settings using environment variables
  • Bootstrap-based responsive templates

This project gives a practical place for all concepts from tutorials 47 to 60.

3. Project Structure

teamboard_pro/
│
├── manage.py
├── .env.example
├── requirements.txt
│
├── teamboard_pro/
│   ├── __init__.py
│   ├── urls.py
│   ├── wsgi.py
│   ├── asgi.py
│   └── settings/
│       ├── __init__.py
│       ├── base.py
│       ├── development.py
│       └── production.py
│
├── core/
│   ├── __init__.py
│   ├── apps.py
│   ├── middleware.py
│   ├── decorators.py
│   ├── utils.py
│   └── views.py
│
├── accounts/
│   ├── __init__.py
│   ├── apps.py
│   ├── forms.py
│   ├── urls.py
│   └── views.py
│
├── dashboard/
│   ├── __init__.py
│   ├── apps.py
│   ├── urls.py
│   └── views.py
│
├── content/
│   ├── __init__.py
│   ├── apps.py
│   ├── admin.py
│   ├── forms.py
│   ├── models.py
│   ├── urls.py
│   ├── views.py
│   └── services.py
│
├── tasksapp/
│   ├── __init__.py
│   ├── apps.py
│   ├── admin.py
│   ├── forms.py
│   ├── models.py
│   ├── urls.py
│   └── views.py
│
├── templates/
│   ├── base.html
│   ├── home.html
│   ├── includes/
│   │   ├── navbar.html
│   │   └── messages.html
│   ├── accounts/
│   │   ├── login.html
│   │   ├── register.html
│   │   └── profile.html
│   ├── dashboard/
│   │   └── dashboard.html
│   ├── content/
│   │   ├── article_list.html
│   │   ├── article_detail.html
│   │   ├── article_form.html
│   │   └── article_confirm_delete.html
│   └── tasksapp/
│       ├── task_list.html
│       └── task_form.html
│
└── static/
    └── css/
        └── style.css

4. Installation and Setup

requirements.txt

Django>=5.0
python-dotenv>=1.0

Create Project

django-admin startproject teamboard_pro
cd teamboard_pro
python manage.py startapp core
python manage.py startapp accounts
python manage.py startapp dashboard
python manage.py startapp content
python manage.py startapp tasksapp

5. Settings

teamboard_pro/settings/base.py

import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent.parent


def get_bool_env(name, default=False):
    return os.environ.get(name, str(default)).lower() in ("true", "1", "yes")


def get_required_env(name):
    value = os.environ.get(name)
    if not value:
        raise ValueError(f"{name} is required")
    return value

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key")
DEBUG = get_bool_env("DJANGO_DEBUG", True)
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "core",
    "accounts",
    "dashboard",
    "content",
    "tasksapp",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "core.middleware.RequestTimingMiddleware",
    "core.middleware.AccessLogMiddleware",
]

ROOT_URLCONF = "teamboard_pro.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "teamboard_pro.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": os.environ.get("DB_ENGINE", "django.db.backends.sqlite3"),
        "NAME": os.environ.get("DB_NAME", BASE_DIR / "db.sqlite3"),
        "USER": os.environ.get("DB_USER", ""),
        "PASSWORD": os.environ.get("DB_PASSWORD", ""),
        "HOST": os.environ.get("DB_HOST", ""),
        "PORT": os.environ.get("DB_PORT", ""),
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"

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

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_REDIRECT_URL = "dashboard"
LOGOUT_REDIRECT_URL = "home"
LOGIN_URL = "login"

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "teamboard-cache",
    }
}

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {
            "format": "{levelname} {asctime} {name} {message}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "verbose",
        },
    },
    "root": {
        "handlers": ["console"],
        "level": "INFO",
    },
}

development.py

from .base import *

DEBUG = True

production.py

from .base import *

DEBUG = False
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"

6. Core Middleware

core/middleware.py

import logging
import time

logger = logging.getLogger(__name__)


class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.time()
        response = self.get_response(request)
        duration = time.time() - start
        logger.info(f"{request.method} {request.path} completed in {duration:.4f}s")
        return response


class AccessLogMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        logger.info(f"Access: user={request.user} path={request.path}")
        return self.get_response(request)

This applies Tutorials 1 and 2 practically.

7. Custom Decorators

core/decorators.py

from functools import wraps
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from content.models import Article


def staff_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated or not request.user.is_staff:
            return HttpResponseForbidden("Staff only.")
        return view_func(request, *args, **kwargs)
    return wrapper


def group_required(group_name):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                return HttpResponseForbidden("Login required.")
            if not request.user.groups.filter(name=group_name).exists():
                return HttpResponseForbidden("Group access required.")
            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator


def author_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        article = get_object_or_404(Article, pk=kwargs.get("pk"))
        if article.author != request.user:
            return HttpResponseForbidden("You are not the author.")
        return view_func(request, *args, **kwargs)
    return wrapper

This applies Tutorial 14.

8. Accounts App

accounts/forms.py

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm


class RegisterForm(UserCreationForm):
    email = forms.EmailField(required=True)

    class Meta:
        model = User
        fields = ["username", "email", "password1", "password2"]

accounts/views.py

from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import RegisterForm


def register_view(request):
    if request.method == "POST":
        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect("dashboard")
    else:
        form = RegisterForm()
    return render(request, "accounts/register.html", {"form": form})


@login_required
def profile_view(request):
    return render(request, "accounts/profile.html")

accounts/urls.py

from django.urls import path
from .views import register_view, profile_view

urlpatterns = [
    path("register/", register_view, name="register"),
    path("profile/", profile_view, name="profile"),
]

9. Content App Models

content/models.py

from django.core.exceptions import ValidationError
from django.db import models
from django.contrib.auth.models import User


class ArticleQuerySet(models.QuerySet):
    def published(self):
        return self.filter(published=True)

    def featured(self):
        return self.filter(featured=True)

    def recent(self):
        return self.order_by("-created_at")


class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name


class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="articles")
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
    published = models.BooleanField(default=False)
    featured = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    objects = ArticleQuerySet.as_manager()

    class Meta:
        permissions = [
            ("can_publish_article", "Can publish article"),
        ]

    def clean(self):
        errors = {}
        if self.published and not self.content.strip():
            errors["content"] = "Published article must have content."
        if len(self.title.strip()) < 5:
            errors["title"] = "Title must be at least 5 characters long."
        if errors:
            raise ValidationError(errors)

    def __str__(self):
        return self.title

This applies Tutorials 3, 4, 5, and 13.

10. Tasks App Models

tasksapp/models.py

from django.core.exceptions import ValidationError
from django.db import models
from django.contrib.auth.models import User


class ProjectTask(models.Model):
    STATUS_CHOICES = [
        ("todo", "To Do"),
        ("doing", "Doing"),
        ("done", "Done"),
    ]

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    assigned_to = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tasks")
    deadline = models.DateField(null=True, blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="todo")
    created_at = models.DateTimeField(auto_now_add=True)

    def clean(self):
        if self.status == "done" and not self.description.strip():
            raise ValidationError({
                "description": "Completed tasks must contain a summary description."
            })

    def __str__(self):
        return self.title

11. Content Forms

content/forms.py

from django import forms
from .models import Article


class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "slug", "content", "category", "featured"]
        widgets = {
            "content": forms.Textarea(attrs={"rows": 8}),
        }

12. Transaction Service

content/services.py

from django.db import transaction
from django.contrib.auth.models import Permission
from .models import Article
import logging

logger = logging.getLogger(__name__)


@transaction.atomic
def publish_article_service(article, user):
    if not user.has_perm("content.can_publish_article"):
        raise PermissionError("You do not have permission to publish.")

    article.published = True
    article.full_clean()
    article.save()

    logger.info(f"Article {article.id} published by user {user.id}")
    return article

This applies Tutorial 6.

13. Content Views

content/views.py

from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.core.cache import cache
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from .forms import ArticleForm
from .models import Article
from .services import publish_article_service
from core.decorators import author_required


def article_list(request):
    cache_key = "article_list_page"
    articles = cache.get(cache_key)
    if articles is None:
        articles = list(
            Article.objects.published()
            .select_related("author", "category")
            .annotate(dummy_count=Count("id"))
            .recent()
        )
        cache.set(cache_key, articles, 300)
    return render(request, "content/article_list.html", {"articles": articles})


def article_detail(request, slug):
    article = get_object_or_404(
        Article.objects.select_related("author", "category"),
        slug=slug,
    )
    related_articles = Article.objects.published().filter(category=article.category).exclude(id=article.id)[:4]
    return render(request, "content/article_detail.html", {
        "article": article,
        "related_articles": related_articles,
    })


@login_required
def article_create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.full_clean()
            article.save()
            messages.success(request, "Article created successfully.")
            cache.delete("article_list_page")
            return redirect("article_detail", slug=article.slug)
    else:
        form = ArticleForm()
    return render(request, "content/article_form.html", {"form": form, "page_title": "Create Article"})


@login_required
@author_required
def article_update(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.method == "POST":
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save(commit=False)
            article.full_clean()
            article.save()
            messages.success(request, "Article updated successfully.")
            cache.delete("article_list_page")
            return redirect("article_detail", slug=article.slug)
    else:
        form = ArticleForm(instance=article)
    return render(request, "content/article_form.html", {"form": form, "page_title": "Edit Article"})


@login_required
@permission_required("content.can_publish_article", raise_exception=True)
def publish_article(request, pk):
    article = get_object_or_404(Article, pk=pk)
    publish_article_service(article, request.user)
    messages.success(request, "Article published successfully.")
    cache.delete("article_list_page")
    return redirect("article_detail", slug=article.slug)

14. Tasks Views

tasksapp/forms.py

from django import forms
from .models import ProjectTask


class ProjectTaskForm(forms.ModelForm):
    class Meta:
        model = ProjectTask
        fields = ["title", "description", "assigned_to", "deadline", "status"]
        widgets = {
            "deadline": forms.DateInput(attrs={"type": "date"}),
            "description": forms.Textarea(attrs={"rows": 5}),
        }

tasksapp/views.py

from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import ProjectTaskForm
from .models import ProjectTask


@login_required
def task_list(request):
    tasks = ProjectTask.objects.select_related("assigned_to").order_by("-created_at")
    return render(request, "tasksapp/task_list.html", {"tasks": tasks})


@login_required
def task_create(request):
    if request.method == "POST":
        form = ProjectTaskForm(request.POST)
        if form.is_valid():
            task = form.save(commit=False)
            task.full_clean()
            task.save()
            return redirect("task_list")
    else:
        form = ProjectTaskForm()
    return render(request, "tasksapp/task_form.html", {"form": form})

15. Dashboard Views

dashboard/views.py

from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.shortcuts import render
from django.db import connection
from content.models import Article
from tasksapp.models import ProjectTask


@login_required
def dashboard_view(request):
    stats = cache.get("dashboard_stats")
    if stats is None:
        stats = {
            "total_articles": Article.objects.count(),
            "published_articles": Article.objects.published().count(),
            "total_tasks": ProjectTask.objects.count(),
            "done_tasks": ProjectTask.objects.filter(status="done").count(),
        }
        cache.set("dashboard_stats", stats, 300)

    latest_articles = Article.objects.select_related("author", "category").recent()[:5]
    latest_tasks = ProjectTask.objects.select_related("assigned_to").order_by("-created_at")[:5]

    return render(request, "dashboard/dashboard.html", {
        "stats": stats,
        "latest_articles": latest_articles,
        "latest_tasks": latest_tasks,
    })

This uses caching and advanced ORM.

16. Raw SQL Example

core/utils.py

from django.db import connection


def raw_article_report():
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT c.name, COUNT(a.id) AS article_count
            FROM content_category c
            LEFT JOIN content_article a ON a.category_id = c.id
            GROUP BY c.name
            ORDER BY article_count DESC
        """)
        return cursor.fetchall()

This applies Tutorial 7.

17. URLs

teamboard_pro/urls.py

from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from core.views import home_view
from dashboard.views import dashboard_view

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", home_view, name="home"),
    path("accounts/", include("accounts.urls")),
    path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("dashboard/", dashboard_view, name="dashboard"),
    path("articles/", include("content.urls")),
    path("tasks/", include("tasksapp.urls")),
]

core/views.py

from django.core.cache import cache
from django.shortcuts import render
from content.models import Article


def home_view(request):
    featured_articles = cache.get("homepage_featured_articles")
    if featured_articles is None:
        featured_articles = list(
            Article.objects.published().featured().select_related("author", "category").recent()[:6]
        )
        cache.set("homepage_featured_articles", featured_articles, 600)
    return render(request, "home.html", {"featured_articles": featured_articles})

content/urls.py

from django.urls import path
from .views import article_list, article_detail, article_create, article_update, publish_article

urlpatterns = [
    path("", article_list, name="article_list"),
    path("create/", article_create, name="article_create"),
    path("<slug:slug>/", article_detail, name="article_detail"),
    path("edit/<int:pk>/", article_update, name="article_update"),
    path("publish/<int:pk>/", publish_article, name="publish_article"),
]

tasksapp/urls.py

from django.urls import path
from .views import task_list, task_create

urlpatterns = [
    path("", task_list, name="task_list"),
    path("create/", task_create, name="task_create"),
]

18. Bootstrap Templates

templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}TeamBoard Pro{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="bg-light">
    {% include 'includes/navbar.html' %}

    <main class="container py-4">
        {% include 'includes/messages.html' %}
        {% block content %}{% endblock %}
    </main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

templates/includes/navbar.html

<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm">
  <div class="container">
    <a class="navbar-brand fw-bold" href="{% url 'home' %}">TeamBoard Pro</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item"><a class="nav-link" href="{% url 'article_list' %}">Articles</a></li>
        <li class="nav-item"><a class="nav-link" href="{% url 'task_list' %}">Tasks</a></li>
        {% if user.is_authenticated %}
          <li class="nav-item"><a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a></li>
        {% endif %}
      </ul>
      <ul class="navbar-nav">
        {% if user.is_authenticated %}
          <li class="nav-item"><a class="nav-link" href="{% url 'profile' %}">Profile</a></li>
          <li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li>
        {% else %}
          <li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
          <li class="nav-item"><a class="nav-link" href="{% url 'register' %}">Register</a></li>
        {% endif %}
      </ul>
    </div>
  </div>
</nav>

templates/includes/messages.html

{% if messages %}
  {% for message in messages %}
    <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
      {{ message }}
      <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    </div>
  {% endfor %}
{% endif %}

templates/home.html

{% extends 'base.html' %}

{% block title %}Home - TeamBoard Pro{% endblock %}

{% block content %}
<div class="p-5 mb-4 bg-white rounded-4 shadow-sm border">
    <div class="container-fluid py-3">
        <h1 class="display-5 fw-bold">Advanced Django Learning Project</h1>
        <p class="col-md-9 fs-5 text-muted">
            This project demonstrates Tutorials 47 to 60 with middleware, permissions, decorators,
            logging, caching, transactions, model validation, and production-ready architecture.
        </p>
        <a href="{% url 'article_list' %}" class="btn btn-primary btn-lg">Explore Articles</a>
    </div>
</div>

<div class="row g-4">
    {% for article in featured_articles %}
        <div class="col-md-6 col-lg-4">
            <div class="card h-100 shadow-sm border-0">
                <div class="card-body">
                    <span class="badge bg-primary-subtle text-primary mb-2">{{ article.category.name }}</span>
                    <h5 class="card-title">{{ article.title }}</h5>
                    <p class="card-text text-muted">By {{ article.author.username }}</p>
                    <a href="{% url 'article_detail' article.slug %}" class="btn btn-outline-primary btn-sm">Read More</a>
                </div>
            </div>
        </div>
    {% empty %}
        <p>No featured articles yet.</p>
    {% endfor %}
</div>
{% endblock %}

templates/accounts/login.html

{% extends 'base.html' %}

{% block title %}Login{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-6 col-lg-5">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <h2 class="mb-4 text-center">Login</h2>
                <form method="post">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <button type="submit" class="btn btn-primary w-100">Login</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/accounts/register.html

{% extends 'base.html' %}

{% block title %}Register{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-7 col-lg-6">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <h2 class="mb-4 text-center">Create Account</h2>
                <form method="post">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <button type="submit" class="btn btn-success w-100">Register</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/accounts/profile.html

{% extends 'base.html' %}

{% block title %}Profile{% endblock %}

{% block content %}
<div class="card shadow-sm border-0">
    <div class="card-body">
        <h2>Profile</h2>
        <p><strong>Username:</strong> {{ user.username }}</p>
        <p><strong>Email:</strong> {{ user.email }}</p>
        <p><strong>Groups:</strong>
            {% for group in user.groups.all %}
                <span class="badge bg-primary">{{ group.name }}</span>
            {% empty %}
                <span class="text-muted">No groups assigned</span>
            {% endfor %}
        </p>
    </div>
</div>
{% endblock %}

templates/dashboard/dashboard.html

{% extends 'base.html' %}

{% block title %}Dashboard{% endblock %}

{% block content %}
<h1 class="mb-4">Dashboard</h1>

<div class="row g-4 mb-4">
    <div class="col-md-3">
        <div class="card shadow-sm border-0 text-center">
            <div class="card-body">
                <h3>{{ stats.total_articles }}</h3>
                <p class="mb-0 text-muted">Total Articles</p>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card shadow-sm border-0 text-center">
            <div class="card-body">
                <h3>{{ stats.published_articles }}</h3>
                <p class="mb-0 text-muted">Published Articles</p>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card shadow-sm border-0 text-center">
            <div class="card-body">
                <h3>{{ stats.total_tasks }}</h3>
                <p class="mb-0 text-muted">Total Tasks</p>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card shadow-sm border-0 text-center">
            <div class="card-body">
                <h3>{{ stats.done_tasks }}</h3>
                <p class="mb-0 text-muted">Done Tasks</p>
            </div>
        </div>
    </div>
</div>

<div class="row g-4">
    <div class="col-lg-6">
        <div class="card shadow-sm border-0">
            <div class="card-body">
                <h4>Latest Articles</h4>
                <ul class="list-group list-group-flush">
                    {% for article in latest_articles %}
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            {{ article.title }}
                            <span class="badge bg-secondary">{{ article.category.name }}</span>
                        </li>
                    {% empty %}
                        <li class="list-group-item">No articles yet.</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
    <div class="col-lg-6">
        <div class="card shadow-sm border-0">
            <div class="card-body">
                <h4>Latest Tasks</h4>
                <ul class="list-group list-group-flush">
                    {% for task in latest_tasks %}
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            {{ task.title }}
                            <span class="badge bg-primary">{{ task.status }}</span>
                        </li>
                    {% empty %}
                        <li class="list-group-item">No tasks yet.</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/content/article_list.html

{% extends 'base.html' %}

{% block title %}Articles{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1>Articles</h1>
    {% if perms.content.add_article %}
        <a href="{% url 'article_create' %}" class="btn btn-primary">Create Article</a>
    {% endif %}
</div>

<div class="row g-4">
    {% for article in articles %}
        <div class="col-md-6 col-lg-4">
            <div class="card h-100 shadow-sm border-0">
                <div class="card-body">
                    <span class="badge bg-light text-dark mb-2">{{ article.category.name }}</span>
                    <h5 class="card-title">{{ article.title }}</h5>
                    <p class="text-muted small mb-2">By {{ article.author.username }}</p>
                    <a href="{% url 'article_detail' article.slug %}" class="btn btn-outline-primary btn-sm">View</a>
                </div>
            </div>
        </div>
    {% empty %}
        <p>No articles found.</p>
    {% endfor %}
</div>
{% endblock %}

templates/content/article_detail.html

{% extends 'base.html' %}

{% block title %}{{ article.title }}{% endblock %}

{% block content %}
<div class="row g-4">
    <div class="col-lg-8">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <span class="badge bg-primary mb-2">{{ article.category.name }}</span>
                <h1>{{ article.title }}</h1>
                <p class="text-muted">By {{ article.author.username }} • {{ article.created_at }}</p>
                <hr>
                <div>{{ article.content|linebreaks }}</div>

                <div class="mt-4 d-flex gap-2 flex-wrap">
                    {% if user == article.author %}
                        <a href="{% url 'article_update' article.id %}" class="btn btn-outline-secondary">Edit</a>
                    {% endif %}
                    {% if perms.content.can_publish_article and not article.published %}
                        <a href="{% url 'publish_article' article.id %}" class="btn btn-success">Publish</a>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>

    <div class="col-lg-4">
        <div class="card shadow-sm border-0">
            <div class="card-body">
                <h4>Related Articles</h4>
                <ul class="list-group list-group-flush">
                    {% for related in related_articles %}
                        <li class="list-group-item">
                            <a href="{% url 'article_detail' related.slug %}">{{ related.title }}</a>
                        </li>
                    {% empty %}
                        <li class="list-group-item">No related articles.</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/content/article_form.html

{% extends 'base.html' %}

{% block title %}{{ page_title }}{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-lg-8">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <h2 class="mb-4">{{ page_title }}</h2>
                <form method="post">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <button type="submit" class="btn btn-primary">Save Article</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/tasksapp/task_list.html

{% extends 'base.html' %}

{% block title %}Tasks{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1>Tasks</h1>
    <a href="{% url 'task_create' %}" class="btn btn-primary">Create Task</a>
</div>

<div class="card shadow-sm border-0">
    <div class="table-responsive">
        <table class="table table-hover mb-0">
            <thead class="table-light">
                <tr>
                    <th>Title</th>
                    <th>Assigned To</th>
                    <th>Status</th>
                    <th>Deadline</th>
                </tr>
            </thead>
            <tbody>
                {% for task in tasks %}
                    <tr>
                        <td>{{ task.title }}</td>
                        <td>{{ task.assigned_to.username }}</td>
                        <td><span class="badge bg-secondary">{{ task.status }}</span></td>
                        <td>{{ task.deadline|default:'-' }}</td>
                    </tr>
                {% empty %}
                    <tr>
                        <td colspan="4" class="text-center">No tasks available.</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</div>
{% endblock %}

templates/tasksapp/task_form.html

{% extends 'base.html' %}

{% block title %}Create Task{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-lg-7">
        <div class="card shadow-sm border-0">
            <div class="card-body p-4">
                <h2 class="mb-4">Create Task</h2>
                <form method="post">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <button type="submit" class="btn btn-success">Save Task</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

19. Admin Registration

content/admin.py

from django.contrib import admin
from .models import Category, Article

admin.site.register(Category)
admin.site.register(Article)

tasksapp/admin.py

from django.contrib import admin
from .models import ProjectTask

admin.site.register(ProjectTask)

20. .env.example

DJANGO_SECRET_KEY=change-me
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DB_ENGINE=django.db.backends.sqlite3
DB_NAME=db.sqlite3
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=

21. How This Project Maps to Tutorials 47–60

Middleware (1–2)

The project uses request timing and request access middleware to demonstrate how global request/response logic works in Django.

Advanced ORM (3)

Optimized queries use select_related, annotations, and reusable query patterns.

Managers and QuerySets (4)

The ArticleQuerySet provides readable methods such as published(), featured(), and recent().

Model Validation (5)

Article.clean() and ProjectTask.clean() enforce business rules.

Transactions (6)

Article publishing is wrapped in transaction.atomic() in the service layer.

Raw SQL (7)

core/utils.py includes a practical reporting query using a database cursor.

Caching (8)

Homepage featured articles, article listing, and dashboard stats are cached.

Logging (9)

Logging is configured in settings and used in middleware and publishing workflow.

Settings Best Practices (10)

The project uses split settings with base.py, development.py, and production.py.

Environment Variables (11)

Sensitive and deployment-dependent values come from environment variables.

Security Best Practices (12)

The project uses CSRF-protected forms, secure production settings, login checks, and permission checks.

Permissions and Groups (13)

The article publishing workflow depends on a custom permission: can_publish_article.

Custom Decorators (14)

Decorators such as staff_required, group_required, and author_required demonstrate reusable view-level logic.

22. Final Notes

This project is intentionally designed as a practical capstone for Tutorials 47 to 60. It is not only a code sample but a learning architecture. It shows how advanced Django concepts interact together in one coherent application:

  • middleware wraps requests globally,
  • decorators wrap views locally,
  • managers and QuerySets clean up data access,
  • validation protects model integrity,
  • transactions protect multi-step workflows,
  • raw SQL demonstrates lower-level control,
  • caching improves performance,
  • logging improves observability,
  • split settings and environment variables improve deployment quality,
  • security settings protect production,
  • groups and permissions enforce role-based control.

If you continue this series, the next natural step is to extend this project with Django REST Framework and turn TeamBoard Pro into a hybrid web + API application.

Run the project step by step

1. Create a project folder

Open your terminal and create a new folder:

mkdir teamboard_pro
cd teamboard_pro

2. Create and activate a virtual environment

On Windows

python -m venv env
env\Scripts\activate

On Linux / macOS

python3 -m venv env
source env/bin/activate

You should now see your virtual environment activated.

3. Install Django and python-dotenv

Install the packages used in the project:

pip install Django python-dotenv

If you want, you can also save them into requirements.txt:

pip freeze > requirements.txt

4. Create the Django project

Run:

django-admin startproject teamboard_pro .

The dot at the end is important because it creates the project in the current folder.

5. Create the apps

Now create all required apps:

python manage.py startapp core
python manage.py startapp accounts
python manage.py startapp dashboard
python manage.py startapp content
python manage.py startapp tasksapp

6. Create the settings package

Inside teamboard_pro, Django creates a single settings.py.
You need to replace it with a settings folder.

Go into the project folder:

cd teamboard_pro

Create a new folder called settings

Manually create this structure:

teamboard_pro/
    settings/
        __init__.py
        base.py
        development.py
        production.py

Then delete the original settings.py

You can remove it manually, or from terminal:

Windows

del settings.py

Linux / macOS

rm settings.py

Then go back to the root folder:

cd ..

7. Copy the project code

Now copy all the code from the project document into the correct files.

You need to create and fill:

  • teamboard_pro/settings/base.py
  • teamboard_pro/settings/development.py
  • teamboard_pro/settings/production.py
  • core/middleware.py
  • core/decorators.py
  • core/views.py
  • core/utils.py
  • accounts/forms.py
  • accounts/views.py
  • accounts/urls.py
  • dashboard/views.py
  • content/models.py
  • content/forms.py
  • content/views.py
  • content/services.py
  • content/urls.py
  • content/admin.py
  • tasksapp/models.py
  • tasksapp/forms.py
  • tasksapp/views.py
  • tasksapp/urls.py
  • tasksapp/admin.py
  • teamboard_pro/urls.py

And also create the templates and static files exactly as listed.

8. Create the templates folders

At the root of the project, create this structure:

templates/
    base.html
    home.html
    includes/
        navbar.html
        messages.html
    accounts/
        login.html
        register.html
        profile.html
    dashboard/
        dashboard.html
    content/
        article_list.html
        article_detail.html
        article_form.html
    tasksapp/
        task_list.html
        task_form.html

Also create the static folder:

static/
    css/
        style.css

You can leave style.css simple at first, for example:

body {
    font-family: Arial, sans-serif;
}

9. Create the .env file

At the root of the project, create a file named .env:

DJANGO_SECRET_KEY=dev-secret-key
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DB_ENGINE=django.db.backends.sqlite3
DB_NAME=db.sqlite3
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=

10. Tell Django to use development settings

Before running commands, set the settings module.

On Windows CMD

set DJANGO_SETTINGS_MODULE=teamboard_pro.settings.development

On Windows PowerShell

$env:DJANGO_SETTINGS_MODULE="teamboard_pro.settings.development"

On Linux / macOS

export DJANGO_SETTINGS_MODULE=teamboard_pro.settings.development

11. Make migrations

Now create migration files:

python manage.py makemigrations

Then apply them:

python manage.py migrate

If everything is configured correctly, Django will create the database tables.

12. Create a superuser

Create an admin account:

python manage.py createsuperuser

Enter:

  • username
  • email
  • password

This account will let you access /admin/.

13. Run the server

Now start Django:

python manage.py runserver

Open:

http://127.0.0.1:8000/

You should see the homepage.

After the project runs

14. Open the admin panel

Go to:

http://127.0.0.1:8000/admin/

Log in with your superuser.

From there you can:

  • add categories
  • manage users
  • create groups
  • assign permissions
  • manage articles and tasks

15. Create groups and permissions

To test Tutorial 13 features, create groups in admin:

  • Authors
  • Editors

Then assign permissions like:

Authors

  • Can add article
  • Can view article

Editors

  • Can add article
  • Can change article
  • Can view article
  • Can publish article

Then assign users to those groups.

16. Test the main pages

After that, test these URLs:

  • / → homepage
  • /register/ → register page
  • /login/ → login page
  • /dashboard/ → dashboard
  • /articles/ → article list
  • /articles/create/ → create article
  • /tasks/ → task list
  • /tasks/create/ → create task

Common problems and fixes

Problem 1: No module named dotenv

Install it:

pip install python-dotenv

Problem 2: TemplateDoesNotExist

Check:

  • the templates folder exists at project root
  • DIRS: [BASE_DIR / "templates"] is in base.py
  • file names match exactly

Problem 3: No module named teamboard_pro.settings.development

Check:

  • teamboard_pro/settings/__init__.py exists
  • development.py exists
  • you removed the old settings.py

Problem 4: static CSS not loading

Make sure:

  • static/css/style.css exists
  • STATIC_URL = "/static/"
  • in templates you use static properly if needed

For best practice in templates, load static like this in base.html:

{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">

That is better than hardcoding /static/css/style.css.

Problem 5: permission denied when editing or publishing articles

This is normal unless:

  • the user is the article author for editing
  • or the user has can_publish_article permission for publishing

Best order to test the project

  1. Run migrations
  2. Create superuser
  3. Log in to admin
  4. Create categories
  5. Register a normal user
  6. Create groups
  7. Assign permissions
  8. Assign users to groups
  9. Create article
  10. Test publish workflow
  11. Test task creation
  12. Test dashboard

Recommended improvement before running

In your base.html, change this:

<link rel="stylesheet" href="/static/css/style.css">

to this:

{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">

This is the correct Django static usage.

Minimal command summary

python -m venv env
env\Scripts\activate
pip install Django python-dotenv
django-admin startproject teamboard_pro .
python manage.py startapp core
python manage.py startapp accounts
python manage.py startapp dashboard
python manage.py startapp content
python manage.py startapp tasksapp
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver