Skip to main content
โšก Calmops

Django Fundamentals: Models, Views, and Templates (MTV Architecture)

Django Fundamentals: Models, Views, and Templates (MTV Architecture)

Django is a powerful, batteries-included web framework that follows the MTV (Model-View-Template) architectural pattern. Unlike Flask’s minimalist approach, Django provides a complete ecosystem for building robust web applications with built-in features like authentication, admin panels, and an ORM.

Understanding Django’s MTV architecture is fundamental to building effective Django applications. In this guide, we’ll explore each component in depth, see how they interact, and build practical examples that demonstrate real-world usage patterns.


Understanding Django’s MTV Architecture

MTV vs MVC

Django uses MTV (Model-View-Template) instead of the traditional MVC (Model-View-Controller) pattern:

  • Model: Database layer that defines your data structure
  • View: Business logic layer that processes requests and returns responses
  • Template: Presentation layer that renders HTML to the user

The key difference from MVC is that Django’s View handles what MVC calls both the “Controller” and “View” logic, while Django’s Template is purely for presentation.

The Request-Response Cycle

Understanding how these components work together is crucial:

1. User Request โ†’ 2. URL Router โ†’ 3. View (Business Logic) โ†’ 
4. Model (Database) โ†’ 5. Template (HTML Rendering) โ†’ 6. Response to User

When a user visits a URL, Django routes the request to a view, which may query the database through models, then renders a template with the data, and returns the response.


Part 1: Models - Your Data Layer

Models define the structure of your data and how it’s stored in the database. Django’s ORM (Object-Relational Mapping) abstracts away SQL, allowing you to work with Python objects instead.

Creating Your First Model

Models are defined as Python classes in models.py:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name
    
    class Meta:
        ordering = ['-created_at']

Field Types

Django provides various field types for different data:

from django.db import models

class BlogPost(models.Model):
    # Text fields
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    
    # Numeric fields
    views = models.IntegerField(default=0)
    rating = models.FloatField(default=0.0)
    
    # Date/Time fields
    published_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    # Boolean field
    is_published = models.BooleanField(default=False)
    
    # Choice field
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived'),
    ]
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    
    # File fields
    featured_image = models.ImageField(upload_to='blog_images/')
    pdf_file = models.FileField(upload_to='documents/')
    
    # Email field
    author_email = models.EmailField()
    
    # URL field
    external_link = models.URLField(blank=True)

Model Relationships

Models often relate to each other. Django supports three types of relationships:

One-to-Many (ForeignKey)

class Author(models.Model):
    name = models.CharField(max_length=100)

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    # on_delete=models.CASCADE means if author is deleted, posts are too

Many-to-Many

class Tag(models.Model):
    name = models.CharField(max_length=50)

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag)

One-to-One

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()
    profile_picture = models.ImageField()

Model Methods and Properties

Add custom methods to your models:

from django.db import models
from django.utils import timezone

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    published_at = models.DateTimeField(null=True, blank=True)
    
    def publish(self):
        """Publish the blog post"""
        self.published_at = timezone.now()
        self.save()
    
    def is_published(self):
        """Check if post is published"""
        return self.published_at is not None
    
    def get_word_count(self):
        """Calculate word count"""
        return len(self.content.split())
    
    def __str__(self):
        return self.title

Migrations - Managing Database Changes

Migrations track changes to your models:

# Create migrations for changes
python manage.py makemigrations

# Apply migrations to database
python manage.py migrate

# See migration status
python manage.py showmigrations

# Revert to previous migration
python manage.py migrate app_name 0001

A migration file looks like:

from django.db import migrations, models

class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='blogpost',
            name='featured_image',
            field=models.ImageField(upload_to='images/', null=True),
        ),
    ]

Querying Models

The ORM allows you to query models using Python:

from myapp.models import BlogPost, Author

# Get all posts
all_posts = BlogPost.objects.all()

# Filter posts
published_posts = BlogPost.objects.filter(is_published=True)
recent_posts = BlogPost.objects.filter(published_at__gte='2025-01-01')

# Get single object
post = BlogPost.objects.get(id=1)

# Count objects
post_count = BlogPost.objects.count()

# Order results
posts = BlogPost.objects.all().order_by('-published_at')

# Limit results
first_five = BlogPost.objects.all()[:5]

# Complex queries
from django.db.models import Q
posts = BlogPost.objects.filter(
    Q(title__icontains='django') | Q(content__icontains='django')
)

# Relationships
author = Author.objects.get(id=1)
author_posts = author.blogpost_set.all()

Part 2: Views - Your Business Logic Layer

Views handle the request processing and return responses. Django supports two approaches: function-based views (FBV) and class-based views (CBV).

Function-Based Views

Simple, straightforward views for basic tasks:

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from myapp.models import BlogPost

def blog_list(request):
    """Display list of blog posts"""
    posts = BlogPost.objects.filter(is_published=True).order_by('-published_at')
    context = {'posts': posts}
    return render(request, 'blog/post_list.html', context)

def blog_detail(request, post_id):
    """Display single blog post"""
    post = get_object_or_404(BlogPost, id=post_id, is_published=True)
    context = {'post': post}
    return render(request, 'blog/post_detail.html', context)

def search_posts(request):
    """Search blog posts"""
    query = request.GET.get('q', '')
    if query:
        posts = BlogPost.objects.filter(
            title__icontains=query,
            is_published=True
        )
    else:
        posts = []
    context = {'posts': posts, 'query': query}
    return render(request, 'blog/search_results.html', context)

Class-Based Views

More powerful and reusable for complex views:

from django.views import View
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from myapp.models import BlogPost

class BlogListView(ListView):
    """Display list of blog posts"""
    model = BlogPost
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10
    
    def get_queryset(self):
        return BlogPost.objects.filter(is_published=True).order_by('-published_at')

class BlogDetailView(DetailView):
    """Display single blog post"""
    model = BlogPost
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    pk_url_kwarg = 'post_id'
    
    def get_queryset(self):
        return BlogPost.objects.filter(is_published=True)

class CreateBlogPostView(LoginRequiredMixin, CreateView):
    """Create new blog post (requires login)"""
    model = BlogPost
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'featured_image']
    success_url = reverse_lazy('blog:post_list')
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

Handling Different HTTP Methods

from django.views import View
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator

# Function-based view with multiple methods
@require_http_methods(["GET", "POST"])
def handle_post(request):
    if request.method == 'GET':
        return render(request, 'post_form.html')
    elif request.method == 'POST':
        # Process form submission
        title = request.POST.get('title')
        content = request.POST.get('content')
        post = BlogPost.objects.create(title=title, content=content)
        return redirect('blog:post_detail', post_id=post.id)

# Class-based view with multiple methods
class PostAPIView(View):
    def get(self, request, post_id):
        post = get_object_or_404(BlogPost, id=post_id)
        return JsonResponse({
            'id': post.id,
            'title': post.title,
            'content': post.content
        })
    
    def post(self, request):
        data = json.loads(request.body)
        post = BlogPost.objects.create(
            title=data['title'],
            content=data['content']
        )
        return JsonResponse({'id': post.id}, status=201)

URL Routing

Connect views to URLs in urls.py:

from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    # Function-based views
    path('', views.blog_list, name='post_list'),
    path('post/<int:post_id>/', views.blog_detail, name='post_detail'),
    path('search/', views.search_posts, name='search'),
    
    # Class-based views
    path('posts/', views.BlogListView.as_view(), name='post_list_cbv'),
    path('posts/<int:post_id>/', views.BlogDetailView.as_view(), name='post_detail_cbv'),
    path('create/', views.CreateBlogPostView.as_view(), name='create_post'),
]

Part 3: Templates - Your Presentation Layer

Templates render HTML with dynamic content using Django’s Template Language (DTL).

Basic Template Syntax

<!-- Display variables -->
<h1>{{ post.title }}</h1>
<p>By {{ post.author.name }}</p>

<!-- Filters -->
<p>Published: {{ post.published_at|date:"F j, Y" }}</p>
<p>{{ post.content|truncatewords:50 }}</p>

<!-- Conditionals -->
{% if post.is_published %}
    <span class="badge">Published</span>
{% else %}
    <span class="badge">Draft</span>
{% endif %}

<!-- Loops -->
<ul>
{% for tag in post.tags.all %}
    <li>{{ tag.name }}</li>
{% empty %}
    <li>No tags</li>
{% endfor %}
</ul>

Template Inheritance

Create a base template that other templates extend:

templates/base.html:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Blog{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <nav>
        <a href="{% url 'blog:post_list' %}">Home</a>
        <a href="{% url 'blog:search' %}">Search</a>
        {% if user.is_authenticated %}
            <a href="{% url 'blog:create_post' %}">Create Post</a>
            <a href="{% url 'logout' %}">Logout</a>
        {% else %}
            <a href="{% url 'login' %}">Login</a>
        {% endif %}
    </nav>
    
    <main>
        {% block content %}
        Default content
        {% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2025 My Blog</p>
    </footer>
</body>
</html>

templates/blog/post_list.html:

{% extends "base.html" %}

{% block title %}Blog Posts{% endblock %}

{% block content %}
    <h1>Blog Posts</h1>
    
    {% for post in posts %}
        <article>
            <h2><a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a></h2>
            <p class="meta">By {{ post.author.name }} on {{ post.published_at|date:"F j, Y" }}</p>
            <p>{{ post.content|truncatewords:30 }}</p>
            <a href="{% url 'blog:post_detail' post.id %}">Read more</a>
        </article>
    {% empty %}
        <p>No posts found.</p>
    {% endfor %}
{% endblock %}

Common Template Tags and Filters

<!-- URL reversal -->
<a href="{% url 'blog:post_detail' post.id %}">View Post</a>

<!-- Static files -->
<img src="{% static 'images/logo.png' %}" alt="Logo">
<link rel="stylesheet" href="{% static 'css/style.css' %}">

<!-- Conditionals -->
{% if user.is_authenticated %}
    <p>Welcome, {{ user.username }}!</p>
{% endif %}

<!-- Loops with counter -->
{% for item in items %}
    <p>{{ forloop.counter }}: {{ item }}</p>
{% endfor %}

<!-- Filters -->
<p>{{ text|upper }}</p>
<p>{{ text|lower }}</p>
<p>{{ text|title }}</p>
<p>{{ text|truncatewords:10 }}</p>
<p>{{ date|date:"Y-m-d" }}</p>
<p>{{ price|floatformat:2 }}</p>

<!-- Default value -->
<p>{{ author.bio|default:"No bio available" }}</p>

<!-- Pluralize -->
<p>You have {{ count }} comment{{ count|pluralize }}</p>

<!-- Join -->
<p>{{ tags|join:", " }}</p>

Passing Context to Templates

from django.shortcuts import render
from myapp.models import BlogPost, Author

def blog_dashboard(request):
    posts = BlogPost.objects.all()
    authors = Author.objects.all()
    stats = {
        'total_posts': posts.count(),
        'published_posts': posts.filter(is_published=True).count(),
    }
    
    context = {
        'posts': posts,
        'authors': authors,
        'stats': stats,
        'user_name': request.user.username,
    }
    
    return render(request, 'blog/dashboard.html', context)

templates/blog/dashboard.html:

{% extends "base.html" %}

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

{% block content %}
    <h1>Dashboard</h1>
    
    <div class="stats">
        <p>Total Posts: {{ stats.total_posts }}</p>
        <p>Published: {{ stats.published_posts }}</p>
    </div>
    
    <h2>Recent Posts</h2>
    <ul>
    {% for post in posts|slice:":5" %}
        <li>{{ post.title }}</li>
    {% endfor %}
    </ul>
    
    <h2>Authors</h2>
    <ul>
    {% for author in authors %}
        <li>{{ author.name }} ({{ author.blogpost_set.count }} posts)</li>
    {% endfor %}
    </ul>
{% endblock %}

How MTV Components Work Together

Let’s trace a complete request-response cycle:

# 1. User visits: /blog/posts/1/

# 2. urls.py routes to view
path('posts/<int:post_id>/', views.BlogDetailView.as_view(), name='post_detail')

# 3. View processes request
class BlogDetailView(DetailView):
    model = BlogPost
    template_name = 'blog/post_detail.html'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        post = self.get_object()
        context['related_posts'] = BlogPost.objects.filter(
            tags__in=post.tags.all()
        ).exclude(id=post.id)[:3]
        return context

# 4. Model queries database
post = BlogPost.objects.get(id=1)
related_posts = BlogPost.objects.filter(tags__in=post.tags.all())

# 5. Template renders with context
# templates/blog/post_detail.html
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<h3>Related Posts</h3>
{% for related in related_posts %}
    <a href="{% url 'blog:post_detail' related.id %}">{{ related.title }}</a>
{% endfor %}

# 6. HTML response sent to user

Best Practices

Model Best Practices

# โœ“ Good: Descriptive names and docstrings
class BlogPost(models.Model):
    """Represents a blog post"""
    title = models.CharField(max_length=200, help_text="Post title")
    slug = models.SlugField(unique=True)
    
    def __str__(self):
        return self.title
    
    class Meta:
        ordering = ['-created_at']
        verbose_name_plural = "Blog Posts"

# โœ— Bad: Vague names and no documentation
class BP(models.Model):
    t = models.CharField(max_length=200)

View Best Practices

# โœ“ Good: Use class-based views for reusability
class BlogListView(ListView):
    model = BlogPost
    paginate_by = 10

# โœ“ Good: Keep views focused and simple
def get_blog_posts(request):
    posts = BlogPost.objects.filter(is_published=True)
    return render(request, 'blog/list.html', {'posts': posts})

# โœ— Bad: Complex logic in views
def complex_view(request):
    # Multiple database queries
    # Complex calculations
    # Multiple template renders
    # Too much responsibility

Template Best Practices

<!-- โœ“ Good: Use template inheritance -->
{% extends "base.html" %}
{% block content %}
    <!-- Content here -->
{% endblock %}

<!-- โœ“ Good: Use named URLs -->
<a href="{% url 'blog:post_detail' post.id %}">View</a>

<!-- โœ— Bad: Hardcoded URLs -->
<a href="/blog/post/{{ post.id }}/">View</a>

<!-- โœ— Bad: Complex logic in templates -->
{% if user.is_authenticated and user.profile.is_premium and post.is_published %}
    <!-- This logic belongs in the view -->
{% endif %}

Project Structure

myproject/
โ”œโ”€โ”€ manage.py
โ”œโ”€โ”€ myproject/
โ”‚   โ”œโ”€โ”€ settings.py
โ”‚   โ”œโ”€โ”€ urls.py
โ”‚   โ””โ”€โ”€ wsgi.py
โ”œโ”€โ”€ blog/
โ”‚   โ”œโ”€โ”€ migrations/
โ”‚   โ”œโ”€โ”€ templates/
โ”‚   โ”‚   โ””โ”€โ”€ blog/
โ”‚   โ”‚       โ”œโ”€โ”€ post_list.html
โ”‚   โ”‚       โ””โ”€โ”€ post_detail.html
โ”‚   โ”œโ”€โ”€ static/
โ”‚   โ”‚   โ””โ”€โ”€ blog/
โ”‚   โ”‚       โ””โ”€โ”€ css/
โ”‚   โ”œโ”€โ”€ models.py
โ”‚   โ”œโ”€โ”€ views.py
โ”‚   โ”œโ”€โ”€ urls.py
โ”‚   โ”œโ”€โ”€ forms.py
โ”‚   โ””โ”€โ”€ admin.py
โ””โ”€โ”€ templates/
    โ””โ”€โ”€ base.html

Common Pitfalls and Solutions

N+1 Query Problem

# โœ— Bad: Creates N+1 queries
posts = BlogPost.objects.all()
for post in posts:
    print(post.author.name)  # Query for each post

# โœ“ Good: Use select_related for ForeignKey
posts = BlogPost.objects.select_related('author')
for post in posts:
    print(post.author.name)  # No additional queries

# โœ“ Good: Use prefetch_related for ManyToMany
posts = BlogPost.objects.prefetch_related('tags')
for post in posts:
    for tag in post.tags.all():  # No additional queries
        print(tag.name)

Overloading Templates with Logic

# โœ— Bad: Complex logic in template
{% if user.is_authenticated and user.profile.subscription_level == 'premium' and post.is_premium %}
    <p>Premium content</p>
{% endif %}

# โœ“ Good: Move logic to view
def post_detail(request, post_id):
    post = get_object_or_404(BlogPost, id=post_id)
    can_view = request.user.is_authenticated and \
               request.user.profile.subscription_level == 'premium' and \
               post.is_premium
    return render(request, 'blog/post_detail.html', {
        'post': post,
        'can_view': can_view
    })

# Template becomes simple
{% if can_view %}
    <p>Premium content</p>
{% endif %}

Forgetting to Use get_object_or_404

# โœ— Bad: Returns 500 error if post doesn't exist
def post_detail(request, post_id):
    post = BlogPost.objects.get(id=post_id)
    return render(request, 'blog/post_detail.html', {'post': post})

# โœ“ Good: Returns 404 error if post doesn't exist
from django.shortcuts import get_object_or_404

def post_detail(request, post_id):
    post = get_object_or_404(BlogPost, id=post_id)
    return render(request, 'blog/post_detail.html', {'post': post})

Conclusion

Django’s MTV architecture provides a clean separation of concerns that makes building scalable web applications straightforward:

  • Models define your data structure and business logic
  • Views handle request processing and orchestration
  • Templates render the presentation layer

Key Takeaways

  • Models abstract database operations through Django’s ORM
  • Views contain business logic and handle requests
  • Templates render dynamic HTML with context data
  • Use class-based views for reusable, maintainable code
  • Keep logic out of templates
  • Use select_related and prefetch_related to optimize queries
  • Follow Django conventions for consistency and maintainability

Django’s MTV pattern, combined with its rich ecosystem of packages and tools, makes it one of the most productive frameworks for building web applications. Start with these fundamentals, practice building projects, and gradually explore Django’s advanced features like signals, middleware, and custom managers.

Happy coding with Django!

Comments