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:

1. Project Idea

We will build a small platform where:

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 blog

Add 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.signals

And 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 email

blog/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.username

This 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 context

This 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>&copy; {{ 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:

Tutorial 4: Relationships

Tutorial 5: Model Methods and Properties

Tutorial 6: Slugs

Tutorial 7: File Uploads

Tutorial 8: Authentication

Tutorial 9: Advanced Registration

Tutorial 10: Login Required Pages and Permissions

Tutorial 11: Sessions and Cookies

Tutorial 12: Pagination

Tutorial 13: Search

Tutorial 14: Context Processors

Tutorial 15: Custom Filters and Tags

17. Suggested Test Data

Create:

Create an Editors group in admin and assign:

Then add one user to that group.

18. Final Result

After finishing, your project will have:

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: