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_pro2. Create and activate a virtual environment
On Windows
python -m venv env
env\Scripts\activateOn Linux / macOS
python3 -m venv env
source env/bin/activateYou should now see your virtual environment activated.
3. Install Django and python-dotenv
Install the packages used in the project:
pip install Django python-dotenvIf you want, you can also save them into requirements.txt:
pip freeze > requirements.txt4. 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 tasksapp6. 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_proCreate a new folder called settings
Manually create this structure:
teamboard_pro/
settings/
__init__.py
base.py
development.py
production.pyThen delete the original settings.py
You can remove it manually, or from terminal:
Windows
del settings.pyLinux / macOS
rm settings.pyThen 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.pyteamboard_pro/settings/development.pyteamboard_pro/settings/production.pycore/middleware.pycore/decorators.pycore/views.pycore/utils.pyaccounts/forms.pyaccounts/views.pyaccounts/urls.pydashboard/views.pycontent/models.pycontent/forms.pycontent/views.pycontent/services.pycontent/urls.pycontent/admin.pytasksapp/models.pytasksapp/forms.pytasksapp/views.pytasksapp/urls.pytasksapp/admin.pyteamboard_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.htmlAlso create the static folder:
static/
css/
style.cssYou 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.developmentOn Windows PowerShell
$env:DJANGO_SETTINGS_MODULE="teamboard_pro.settings.development"On Linux / macOS
export DJANGO_SETTINGS_MODULE=teamboard_pro.settings.development11. Make migrations
Now create migration files:
python manage.py makemigrationsThen apply them:
python manage.py migrateIf everything is configured correctly, Django will create the database tables.
12. Create a superuser
Create an admin account:
python manage.py createsuperuserEnter:
- username
- password
This account will let you access /admin/.
13. Run the server
Now start Django:
python manage.py runserverOpen:
You should see the homepage.
After the project runs
14. Open the admin panel
Go to:
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-dotenvProblem 2: TemplateDoesNotExist
Check:
- the
templatesfolder exists at project root DIRS: [BASE_DIR / "templates"]is inbase.py- file names match exactly
Problem 3: No module named teamboard_pro.settings.development
Check:
teamboard_pro/settings/__init__.pyexistsdevelopment.pyexists- you removed the old
settings.py
Problem 4: static CSS not loading
Make sure:
static/css/style.cssexistsSTATIC_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_articlepermission for publishing
Best order to test the project
- Run migrations
- Create superuser
- Log in to admin
- Create categories
- Register a normal user
- Create groups
- Assign permissions
- Assign users to groups
- Create article
- Test publish workflow
- Test task creation
- 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
💬 Comments
No comments yet. Be the first to comment!
Login to comment.