Part V - Creating the models.py, views.py, urls.py, admin.py & the superuser

It's good to visualize our models before defining them in our models.py to see how our model structure would be laid out. The figure below shows how we will determine the database of the blog we're building.

Vicente Reyes

Vicente Reyes

In writing models in Django, we have to follow the coding standards that are stated in the docs. A few pointers to remember are all field names should be in lower case, and should be using underscores instead of camelCase. The next tip is that the class Meta: should always be first after defining the fields, then the standard methods after. Like this:


class Meta:
    ...

  def __str__(self)
      ...

  def save()
      ...

  def get_absolute_url()
      ...

And since our app has choices, as shown in the figure above, Django's coding style suggests that we define the choice as a list tuple with an all-uppercase name as a class attribute on the model.

Now let's start.

# blog_tutorial/main/models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse

STATUS_CHOICES = [
    ("draft", "Draft"),
    ("published", "Published"),
  ]


class Project(models.Model):
    """ This model defines our Project class which will
    handles the portfolio of the user.
    """
    title = models.CharField(max_length=100)
    slug = models.SlugField(max_length=140, default=title)
    image = models.ImageField(upload_to="projects/", blank=True)
    live_site = models.CharField(max_length=255)
    github_link = models.CharField(max_length=255)
    description = models.TextField()

    class Meta:
    """ Meta for the naming in the django admin that
    describes a model if the object is singular or
    plural
    """
        verbose_name = _("Project")
        verbose_name_plural = _("Project")

    def __str__(self):
      """ Returns the title of Project models instead
      of a primary key
      """
      return self.title


class Category(models.Model):
    """ This model defines the categories field in the Post model
        with a ManyToManyField.
    """
    title = models.CharField(max_length=140)
    slug = models.SlugField(max_length=140, default=title)

    class Meta:
      """ Meta for the naming in the django admin that
        describes a model if the object is singular or
        plural
      """
      verbose_name = _("Category")
      verbose_name_plural = _("Category")

    def __str__(self):
      """ Returns the title of Project models instead
      of a primary key
      """
      return self.title

    def get_context_data(self, **kwargs):
      context = super(self).get_context_data(**kwargs)
      context['posts'] = Post.objects.filter('category')
      return context

    def get_absolute_url(self):
      return reverse("category-list", kwargs={"slug": self.slug})


class Post(models.Model):
    """ The main model that defines the Post class which
        has a relationship with the Category class
    """
    title = models.CharField(max_length=140)
    slug = models.SlugField(max_length=140, default=title)
    overview = models.CharField(max_length=255)
    body = models.TextField()
    image = models.ImageField(upload_to="blog/")
    created_on = models.DateField()
    updated_on = models.DateField()
    categories = models.ManyToManyField(Category, related_name='posts')
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="draft")

    class Meta:
       """ Meta for the naming in the django admin that
         describes a model if the object is singular or
         plural
       """
       verbose_name = _("Post")
       verbose_name_plural = _("Post")

    def __str__(self):
       """ Returns the title of Project models instead
       of a primary key
       """
       return self.title


class Contact(models.Model):
     """ The Contact model that accepts name, email and message
       in the contact page.
     """
     name = models.CharField(max_length=100)
     email = models.EmailField()
     message = models.TextField()

    class Meta:
      """ Meta for the naming in the django admin that
        describes a model if the object is singular or
        plural
      """
      verbose_name = _("Contact")
      verbose_name_plural = _("Contact")

    def __str__(self):
      """ Returns the title of Project models instead
      of a primary key
      """
      return self.name


In writing views.py for a Django app, Class Based Views are written much simpler than the Function Based views. Here's an example:


# FBV
# views.py
def blog_index(request):    
    blogs = Post.objects.all()    
    context = {        
    "blogs": blogs,    
    }    
    return render(request, "pages/blog_index.html", context)

# blog_index.html
{% for blog in blogs %}
    {{ blog.title }}
    ...
{% endfor %}

# CBV
# views.py

class BlogListView(ListView):    
    model = Post
    template_name = 'pages/blog.html'
    context_object_name = 'posts'

# blog_list.html
{% for post in object_list %}
    {{ post.title }}
    ...
{% endfor }}

Both views above render the same data. But can you see which one's simpler? Yep, that's how concise Class-Based Views are. Now let's write our views.py for our blog

# main/views.py

from django.contrib import messages
from django.shortcuts import render
from django.views.generic import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import FormView
from django.views.generic import TemplateView

from blog_tutorial.main.models import (
    Post,
    Category,
    Project,
    Contact,
   )

from blog_tutorial.main.forms import ContactForm

class ProjectListView(ListView):
    model = Project
    paginate_by = 5
    template_name = 'pages/projects.html'
    context_object_name = 'projects'


class ProjectDetailView(DetailView):
    model = Project
    template_name = 'pages/project_details.html'


class BlogListView(ListView):
    model = Post
    paginate_by = 4
    template_name = 'pages/home.html'
    context_object_name = 'posts'
    ordering = ['-created_on']


class BlogDetailView(DetailView):
    model = Post
    template_name = 'pages/post_detail.html'


def blog_category(request, category):
    posts = Post.objects.filter(
        categories__slug__contains=category
    )
    context = {
        "category": category,
        "posts": posts
    }
    return render(request, "pages/category_list.html", context)


class ContactFormView(FormView):
    template_name = 'pages/contact.html'
    form_class = ContactForm
    success_url = '/contact/'

    def form_valid(self, form):
       name = form.cleaned_data['name']
       email = form.cleaned_data['email']
       message = form.cleaned_data['message']

       m = Contact(
       name=name,
       email=email,
       message=message,
       )
       m.save()

       messages.success(self.request, 'Your message has been sent.')

       return super().form_valid(form)


class AboutView(TemplateView):
     template_name = 'pages/about.html'

Let's create the forms.py

from django import forms
from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Field


class ContactForm(forms.Form):
   def __init__(self, *args, **kwargs):
      super(ContactForm, self).__init__(*args, **kwargs)
      self.helper = FormHelper()

      self.helper.form_method = 'post'
      self.helper.form_action = '/contact/'
      self.helper.form_class = "form-group"
      self.helper.form_id = 'contact-form'

      self.helper.add_input(Submit('submit', 'Submit'))

      name = forms.CharField(max_length=100, label="Your name", widget=forms.TextInput(attrs={'class': 'col', 'placeholder':'Vicente Reyes'}))
      email = forms.CharField(label="Your email", widget=forms.EmailInput(attrs={'class': 'col', 'placeholder':'[email protected]'}))
      message = forms.CharField(max_length=500, label="Your inquiry", widget=forms.Textarea(attrs={'placeholder':'I need to...'}))

To view our models' data that are passed to the views, we have to pass the views to our URLs. 

# config/urls.py
...

from blog_tutorial.main.views import (
    BlogListView,
    BlogDetailView,
    blog_category,
    ContactFormView,
    ProjectListView,
    ProjectDetailView
)

urlpatterns = [
    path("", BlogListView.as_view(), name="home"),
    path( "about/", TemplateView.as_view(template_name="pages/about.html"), name="about"),
    # Django Admin, use {% url 'admin:index' %}
    path(settings.ADMIN_URL, admin.site.urls),
    # User management
    path("users/", include("blog_tutorial.users.urls", namespace="users")),
    path("accounts/", include("allauth.urls")),
    # Your stuff: custom urls includes go here
    path("blog/<slug:slug>/", BlogDetailView.as_view(), name="blog-detail"),
    path("blog/category/<slug:category>/", blog_category, name='categories'),
    path("portfolio/", ProjectListView.as_view(), name="portfolio"),
    path("portfolio/<slug:slug>", ProjectDetailView.as_view(), name="project-details"),
    path("contact/", ContactFormView.as_view(), name="contact"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Now let's migrate our models and create our superuser to access the admin.

$ python manage.py makemigrations && python manage.py migrate && python manage.py createsuperuser
Username: admin
Email address:
Password:
Password (again):
Superuser created successfully.

Now for the admin, we're using one of the included features of Cookiecutter-Django that can generate an admin.py file called django-extensions

We'll run one command to generate the file:

$ python manage.py admin_generator main

The command will generate the output in the shell:

# -*- coding: utf-8 -*-
from django.contrib import admin

from .models import Project, Category, Post, Contact


@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = (
        'id',
        'title',
        'slug',
        'image',
        'live_site',
        'github_link',
        'description',
    )
    search_fields = ('slug',)


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'slug')
    search_fields = ('slug',)


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = (
        'id',
        'title',
        'slug',
        'overview',
        'body',
        'image',
        'created_on',
        'updated_on',
        'status',
    )
    list_filter = ('created_on', 'updated_on')
    raw_id_fields = ('categories',)
    search_fields = ('slug',)


@admin.register(Contact)
class ContactAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'email', 'message')
    search_fields = ('name',)

Let's run the Django development server and head over to http://localhost:8000

Watching for file changes with StatReloader
INFO 2020-07-22 14:59:07,112 autoreload 47052 4566134208 Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
July 22, 2020 - 14:59:15
Django version 3.0.8, using settings 'config.settings.local'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.