Project Title
MofidBoard — A mini blog and learning platform in Django
This project is designed to cover all the subjects from Tutorials 21 to 35 in one practical application.
It includes:
- Class-Based Views
- Generic Class-Based Views
- QuerySets and ORM
- Relationships in models
- Model methods and properties
- Slugs and clean URLs
- File uploads
- User authentication
- Advanced user registration
- Login-required pages and permissions
- Sessions and cookies
- Pagination
- Search
- Context processors
- Custom template filters and tags
1. Project Idea
We will build a small platform where:
- users can register and log in
- each user has a profile
- users can create articles
- articles belong to categories
- articles can have tags
- users can upload a cover image for articles
- only authenticated users can access their dashboard
- only authors can edit their own articles
- search works on article title and content
- articles are paginated
- global categories and site info are available in all templates
- custom template filters/tags improve presentation
2. Concepts Covered
Tutorial 1 — Class-Based Views
Used with TemplateView, ListView, DetailView
Tutorial 2 — Generic Class-Based Views
Used with CreateView, UpdateView, DeleteView
Tutorial 3 — QuerySets and ORM Deep Dive
Used in search, filtering, ordering, pagination
Tutorial 4 — Relationships in Django Models
Used with ForeignKey, OneToOneField, ManyToManyField
Tutorial 5 — Model Methods and Properties
Used in Article and Profile
Tutorial 6 — Slugs and Clean URLs
Used in article detail pages
Tutorial 7 — File Uploads
Used with article cover images and profile avatar
Tutorial 8 — User Authentication
Login, logout, protected pages
Tutorial 9 — Advanced Registration System
Custom signup form + profile creation
Tutorial 10 — Login Required Pages and Permissions
Dashboard, author-only editing, permission-based access
Tutorial 11 — Sessions and Cookies
Theme preference cookie + session-based visit counter
Tutorial 12 — Pagination
Article list pagination
Tutorial 13 — Search Functionality
Search in title and content
Tutorial 14 — Context Processors
Global categories, site name, current year
Tutorial 15 — Custom Template Filters and Tags
Custom price/date/text/group helper tags
3. Final Project Structure
mofidboard/
│
├── manage.py
├── mofidboard/
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
│
├── core/
│ ├── views.py
│ ├── urls.py
│ ├── context_processors.py
│ └── templatetags/
│ ├── __init__.py
│ └── custom_tags.py
│
├── accounts/
│ ├── models.py
│ ├── forms.py
│ ├── views.py
│ ├── urls.py
│ └── signals.py
│
├── blog/
│ ├── models.py
│ ├── forms.py
│ ├── views.py
│ ├── urls.py
│ └── admin.py
│
├── templates/
│ ├── base.html
│ ├── core/
│ ├── accounts/
│ └── blog/
│
└── media/4. Create the Project
django-admin startproject mofidboard
cd mofidboard
python manage.py startapp core
python manage.py startapp accounts
python manage.py startapp blogAdd apps in settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'accounts',
'blog',
]5. Global Settings
settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'your-secret-key'
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'accounts',
'blog',
]
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',
]
ROOT_URLCONF = 'mofidboard.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',
'core.context_processors.site_defaults',
],
},
},
]
WSGI_APPLICATION = 'mofidboard.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Africa/Casablanca'
USE_I18N = True
USE_TZ = True
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'home'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'6. Models
accounts/models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone = models.CharField(max_length=20, blank=True)
city = models.CharField(max_length=100, blank=True)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
def __str__(self):
return self.user.username
@property
def full_name(self):
full = f"{self.user.first_name} {self.user.last_name}".strip()
return full if full else self.user.username
blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True, blank=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Article(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles')
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='articles')
tags = models.ManyToManyField(Tag, related_name='articles', blank=True)
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
cover = models.ImageField(upload_to='articles/', blank=True, null=True)
is_published = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
permissions = [
('can_publish_article', 'Can publish article'),
]
ordering = ['-created_at']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('article_detail', kwargs={'slug': self.slug})
def short_content(self):
return self.content[:120] + '...' if len(self.content) > 120 else self.content
@property
def reading_time(self):
words = len(self.content.split())
return max(1, words // 200)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.title)
slug = base_slug
counter = 1
while Article.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f'{base_slug}-{counter}'
counter += 1
self.slug = slug
super().save(*args, **kwargs)7. Signals for Profile Auto-Creation
accounts/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)accounts/apps.py
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
import accounts.signalsAnd in accounts/__init__.py nothing special needed if modern Django reads AppConfig automatically via apps.py.
8. Forms
accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class AdvancedSignUpForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=100, required=True)
last_name = forms.CharField(max_length=100, required=True)
phone = forms.CharField(max_length=20, required=False)
city = forms.CharField(max_length=100, required=False)
bio = forms.CharField(widget=forms.Textarea, required=False)
class Meta:
model = User
fields = [
'username', 'first_name', 'last_name', 'email',
'password1', 'password2', 'phone', 'city', 'bio'
]
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError("This email is already used.")
return emailblog/forms.py
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['category', 'tags', 'title', 'content', 'cover', 'is_published']
widgets = {
'content': forms.Textarea(attrs={'rows': 8}),
}
class SearchForm(forms.Form):
q = forms.CharField(required=False, max_length=100)9. Core Context Processor
core/context_processors.py
from datetime import datetime
from blog.models import Category
def site_defaults(request):
cart = request.session.get('saved_articles', [])
return {
'site_name': 'MofidBoard',
'current_year': datetime.now().year,
'global_categories': Category.objects.all(),
'saved_articles_count': len(cart),
}This covers Tutorial 14.
10. Custom Template Filters and Tags
core/templatetags/custom_tags.py
from django import template
from datetime import datetime
register = template.Library()
@register.filter
def mad(value):
return f"{value} MAD"
@register.filter
def short_text(value, length=80):
value = str(value)
length = int(length)
return value if len(value) <= length else value[:length] + '...'
@register.filter
def has_group(user, group_name):
return user.groups.filter(name=group_name).exists()
@register.simple_tag
def current_year_tag():
return datetime.now().year
@register.simple_tag
def full_name(user):
full = f"{user.first_name} {user.last_name}".strip()
return full if full else user.usernameThis covers Tutorial 15.
11. Views
core/views.py
from django.shortcuts import render
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = 'core/home.html'
def set_theme(request, theme):
response = render(request, 'core/theme_set.html', {'theme': theme})
response.set_cookie('theme', theme, max_age=60 * 60 * 24 * 30)
return response
accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from .forms import AdvancedSignUpForm
def signup_view(request):
if request.method == 'POST':
form = AdvancedSignUpForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.email = form.cleaned_data['email']
user.first_name = form.cleaned_data['first_name']
user.last_name = form.cleaned_data['last_name']
user.save()
profile = user.profile
profile.phone = form.cleaned_data['phone']
profile.city = form.cleaned_data['city']
profile.bio = form.cleaned_data['bio']
profile.save()
login(request, user)
return redirect('dashboard')
else:
form = AdvancedSignUpForm()
return render(request, 'accounts/signup.html', {'form': form})
@login_required
def dashboard(request):
visits = request.session.get('dashboard_visits', 0)
visits += 1
request.session['dashboard_visits'] = visits
return render(request, 'accounts/dashboard.html', {'visits': visits})This covers Tutorials 8, 9, 10, 11.
blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import HttpResponseForbidden
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView
)
from .forms import ArticleForm
from .models import Article, Category
class ArticleListView(ListView):
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 5
def get_queryset(self):
return (
Article.objects.select_related('author', 'category')
.prefetch_related('tags')
.filter(is_published=True)
.annotate(tag_count=Count('tags'))
)
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
slug_field = 'slug'
slug_url_kwarg = 'slug'
class ArticleCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Article
form_class = ArticleForm
template_name = 'blog/article_form.html'
permission_required = 'blog.add_article'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
model = Article
form_class = ArticleForm
template_name = 'blog/article_form.html'
slug_field = 'slug'
slug_url_kwarg = 'slug'
def dispatch(self, request, *args, **kwargs):
article = self.get_object()
if request.user != article.author:
return HttpResponseForbidden("You can edit only your own articles.")
return super().dispatch(request, *args, **kwargs)
class ArticleDeleteView(LoginRequiredMixin, DeleteView):
model = Article
template_name = 'blog/article_confirm_delete.html'
success_url = reverse_lazy('article_list')
slug_field = 'slug'
slug_url_kwarg = 'slug'
def dispatch(self, request, *args, **kwargs):
article = self.get_object()
if request.user != article.author:
return HttpResponseForbidden("You can delete only your own articles.")
return super().dispatch(request, *args, **kwargs)
def search_articles(request):
query = request.GET.get('q', '')
results = Article.objects.none()
if query:
results = (
Article.objects.select_related('author', 'category')
.prefetch_related('tags')
.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(category__name__icontains=query) |
Q(tags__name__icontains=query)
)
.distinct()
)
paginator = Paginator(results, 5)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'blog/search_results.html', {
'query': query,
'page_obj': page_obj,
})
class CategoryArticleListView(ListView):
model = Article
template_name = 'blog/category_articles.html'
context_object_name = 'articles'
paginate_by = 5
def get_queryset(self):
return Article.objects.filter(
category__slug=self.kwargs['slug'],
is_published=True
).select_related('author', 'category').prefetch_related('tags')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = Category.objects.get(slug=self.kwargs['slug'])
return contextThis covers Tutorials 1, 2, 3, 6, 7, 10, 12, 13.
12. URLs
mofidboard/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
path('accounts/', include('accounts.urls')),
path('blog/', include('blog.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)core/urls.py
from django.urls import path
from .views import HomeView, set_theme
urlpatterns = [
path('', HomeView.as_view(), name='home'),
path('theme/<str:theme>/', set_theme, name='set_theme'),
]accounts/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
from .views import signup_view, dashboard
urlpatterns = [
path('signup/', signup_view, name='signup'),
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, name='dashboard'),
]blog/urls.py
from django.urls import path
from .views import (
ArticleListView, ArticleDetailView, ArticleCreateView,
ArticleUpdateView, ArticleDeleteView, search_articles,
CategoryArticleListView
)
urlpatterns = [
path('', ArticleListView.as_view(), name='article_list'),
path('search/', search_articles, name='search_articles'),
path('create/', ArticleCreateView.as_view(), name='article_create'),
path('category/<slug:slug>/', CategoryArticleListView.as_view(), name='category_articles'),
path('<slug:slug>/', ArticleDetailView.as_view(), name='article_detail'),
path('<slug:slug>/edit/', ArticleUpdateView.as_view(), name='article_update'),
path('<slug:slug>/delete/', ArticleDeleteView.as_view(), name='article_delete'),
]13. Templates
templates/base.html
{% load custom_tags %}
<!DOCTYPE html>
<html>
<head>
<title>{{ site_name }}</title>
</head>
<body class="{{ request.COOKIES.theme|default:'light' }}">
<header>
<h1><a href="{% url 'home' %}">{{ site_name }}</a></h1>
<nav>
<a href="{% url 'article_list' %}">Articles</a>
<a href="{% url 'search_articles' %}">Search</a>
<a href="{% url 'dashboard' %}">Dashboard</a>
{% if user.is_authenticated %}
<span>Welcome {% full_name user %}</span>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
<a href="{% url 'signup' %}">Register</a>
{% endif %}
</nav>
<form method="get" action="{% url 'search_articles' %}">
<input type="text" name="q" placeholder="Search articles...">
<button type="submit">Search</button>
</form>
<p>Saved: {{ saved_articles_count }}</p>
<ul>
{% for category in global_categories %}
<li><a href="{% url 'category_articles' category.slug %}">{{ category.name }}</a></li>
{% endfor %}
</ul>
</header>
<hr>
{% block content %}{% endblock %}
<hr>
<footer>
<p>© {{ current_year }} {{ site_name }}</p>
<a href="{% url 'set_theme' 'light' %}">Light</a>
<a href="{% url 'set_theme' 'dark' %}">Dark</a>
</footer>
</body>
</html>templates/core/home.html
{% extends 'base.html' %}
{% block content %}
<h2>Welcome to {{ site_name }}</h2>
<p>A project covering Django tutorials 21 to 35.</p>
<p><a href="{% url 'article_list' %}">Browse articles</a></p>
{% endblock %}templates/accounts/signup.html
{% extends 'base.html' %}
{% block content %}
<h2>Register</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create account</button>
</form>
{% endblock %}
templates/accounts/login.html
{% extends 'base.html' %}
{% block content %}
<h2>Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
{% endblock %}
templates/accounts/dashboard.html
{% extends 'base.html' %}
{% block content %}
<h2>Dashboard</h2>
<p>Hello {{ user.username }}</p>
<p>Dashboard visits in this session: {{ visits }}</p>
{% if user.profile.avatar %}
<img src="{{ user.profile.avatar.url }}" width="120" alt="avatar">
{% endif %}
<p>City: {{ user.profile.city }}</p>
<p>Phone: {{ user.profile.phone }}</p>
<p>Bio: {{ user.profile.bio|short_text:100 }}</p>
{% if perms.blog.add_article %}
<p><a href="{% url 'article_create' %}">Create Article</a></p>
{% endif %}
{% endblock %}
templates/blog/article_list.html
{% extends 'base.html' %}
{% load custom_tags %}
{% block content %}
<h2>Articles</h2>
{% for article in articles %}
<article>
<h3><a href="{% url 'article_detail' article.slug %}">{{ article.title }}</a></h3>
<p>By {{ article.author.username }} | {{ article.category.name }}</p>
<p>{{ article.short_content|short_text:150 }}</p>
<p>Reading time: {{ article.reading_time }} min</p>
<p>Tags: {{ article.tag_count }}</p>
{% if article.cover %}
<img src="{{ article.cover.url }}" width="200" alt="{{ article.title }}">
{% endif %}
</article>
<hr>
{% empty %}
<p>No articles found.</p>
{% endfor %}
{% if is_paginated %}
<div>
{% if page_obj.has_previous %}
<a href="?page=1">First</a>
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
Page {{ page_obj.number }} of {{ paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
<a href="?page={{ paginator.num_pages }}">Last</a>
{% endif %}
</div>
{% endif %}
{% endblock %}templates/blog/article_detail.html
{% extends 'base.html' %}
{% block content %}
<h2>{{ article.title }}</h2>
<p>By {{ article.author.username }} in {{ article.category.name }}</p>
<p>Reading time: {{ article.reading_time }} min</p>
{% if article.cover %}
<img src="{{ article.cover.url }}" width="300" alt="{{ article.title }}">
{% endif %}
<p>{{ article.content }}</p>
<p>
Tags:
{% for tag in article.tags.all %}
<span>{{ tag.name }}</span>
{% empty %}
<span>No tags</span>
{% endfor %}
</p>
{% if user == article.author %}
<a href="{% url 'article_update' article.slug %}">Edit</a>
<a href="{% url 'article_delete' article.slug %}">Delete</a>
{% endif %}
{% endblock %}templates/blog/article_form.html
{% extends 'base.html' %}
{% block content %}
<h2>Article Form</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}templates/blog/article_confirm_delete.html
{% extends 'base.html' %}
{% block content %}
<h2>Delete Article</h2>
<p>Are you sure you want to delete "{{ object.title }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit">Yes, delete</button>
</form>
{% endblock %}
templates/blog/search_results.html
{% extends 'base.html' %}
{% block content %}
<h2>Search Results</h2>
<p>Query: {{ query }}</p>
{% for article in page_obj %}
<h3><a href="{% url 'article_detail' article.slug %}">{{ article.title }}</a></h3>
<p>{{ article.short_content }}</p>
<hr>
{% empty %}
{% if query %}
<p>No results found.</p>
{% endif %}
{% endfor %}
{% if page_obj.paginator.num_pages > 1 %}
<div>
{% if page_obj.has_previous %}
<a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</div>
{% endif %}
{% endblock %}14. Admin Registration
blog/admin.py
from django.contrib import admin
from .models import Category, Tag, Article
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'is_published', 'created_at']
list_filter = ['category', 'is_published']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Tag)15. Migrations and Superuser
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
16. How This Project Covers Tutorials 21–35
Tutorial 21: Class-Based Views
Used in HomeView, ArticleListView, ArticleDetailView
Tutorial 2: Generic Class-Based Views
Used in CreateView, UpdateView, DeleteView
Tutorial 3: QuerySets and ORM
Used in:
select_relatedprefetch_relatedannotateCountQ- filtering/searching
Tutorial 4: Relationships
Profile.user→OneToOneFieldArticle.authorandArticle.category→ForeignKeyArticle.tags→ManyToManyField
Tutorial 5: Model Methods and Properties
Article.short_content()Article.reading_timeProfile.full_name
Tutorial 6: Slugs
- article detail/edit/delete via slug
- category URLs via slug
Tutorial 7: File Uploads
Profile.avatarArticle.cover
Tutorial 8: Authentication
- login/logout
- protected dashboard
Tutorial 9: Advanced Registration
- custom signup form
- extra fields
- profile saving
Tutorial 10: Login Required Pages and Permissions
- dashboard with
login_required - article creation with
PermissionRequiredMixin - author-only edit/delete
Tutorial 11: Sessions and Cookies
- dashboard visit counter in session
- theme preference in cookie
Tutorial 12: Pagination
- article list pagination
- search pagination
Tutorial 13: Search
- keyword search in title/content/category/tags
Tutorial 14: Context Processors
site_defaults
Tutorial 15: Custom Filters and Tags
madshort_texthas_groupfull_namecurrent_year_tag
17. Suggested Test Data
Create:
- 3 categories: Python, Django, Web
- 5 tags: CBV, ORM, Slugs, Search, Auth
- 10–15 articles
- 2 users: one normal author, one editor group user
Create an Editors group in admin and assign:
blog.add_articleblog.change_articleblog.delete_article
Then add one user to that group.
18. Final Result
After finishing, your project will have:
- public homepage
- article list page with pagination
- article detail page with slug URL
- search page
- advanced user registration
- login/logout
- dashboard with session counter
- article creation/edit/delete
- file uploads
- global categories in layout
- custom template helpers
This is a real mini-project that combines all topics from Tutorials 1 to 15 into one coherent application.
19. Next Step for You
The best next improvement would be to extend this project with:
- contact form
- messages framework
- comment system
- email notifications
💬 Comments
No comments yet. Be the first to comment!
Login to comment.