Complete App

Notes Single

In this article we are going to create a complete project for a Company called Beasttool (this name was selected randomly, it is not a real case) using Django from zero, step by step.

This tutorial is done using Windows 11, some terminal instructions can change depending on the OS you use.

First steps: Installing the framework

Let's create the virtual enviroment, for this enter in the terminal:

path-to-the-project>python -m venv env

Activate the virtual enviroment and go back to the original directory

path-to-the-project>cd env/Scripts
path-to-the-project\env\Scripts>activate
(env) path-to-the-project\env\Scripts>cd../..
(env) path-to-the-project>

Install Django and create the project:

(env) path-to-the-project>pip install django
(env) path-to-the-project>django-admin startproject beasttool

Run the server:

(env) path-to-the-project>cd beasttool
(env) path-to-the-project\beasttool>python manage.py runserver

Now we know the project is running, the server is running at localhost:8000, you can open the web server and verify the Django's default page can be reached:

Create Blog application

For blog application we must create few apps:
1. Users: For managing the users who will manage the posts.
2. Posts: For managing the posts.
3. Categories: For ordering the posts in categories.
4. Comments: For managing the comments the users can leave on the posts.

Create apps

Let's create those 3 apps at once:

(env) path-to-the-project\beasttool>py manage.py startapp user
(env) path-to-the-project\beasttool>py manage.py startapp post
(env) path-to-the-project\beasttool>py manage.py startapp category
(env) path-to-the-project\beasttool>py manage.py startapp comment

Now, we must register them in "beasttool/settings.py"

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # APPS
    'user',
    'post',
    'category',
    'comment'
]

Create the models for each app

At "user/models.py" define the following model:

"""User"""


from django.contrib.auth.models import User
from django.db import models


class Profile(models.Model):
    """Profile model.

    Proxy model that extends the base data with other
    information.
    """
    user = models.OneToOneField(User, on_delete=models.PROTECT)

    website = models.URLField(max_length=200, blank=True)

    photo = models.ImageField(
        upload_to='user/picture',
        blank=True,
        null=True
    )

    date_modified = models.DateTimeField(auto_now=True)


    def __str__(self):
        """Return username."""
        return self.user.username

Some comments: We are importing the Django's User model, protect the users, deleting a user is not allowed to avoid loose the posts and comments which depend on users.

At "user/post.py" define the following model:

"""Post"""

# Django
from django.db import models

from django.utils.text import slugify
from django.contrib.auth.models import User
from ckeditor.fields import RichTextField
from category.models import Category

class Post(models.Model):
    """Post model."""
    
    user = models.ForeignKey(User, on_delete=models.PROTECT)
    profile = models.ForeignKey('user.Profile', on_delete=models.PROTECT)

    title = models.CharField(max_length=255)
    image_header = models.ImageField(upload_to='post/photo')
    post = RichTextField()

    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    is_draft = models.BooleanField(default=True)
    url = models.SlugField(max_length=255, unique=True)
    views = models.PositiveIntegerField(default=0)
    category = models.ManyToManyField(Category)

    class Meta:
        ordering = ('title',)


    def __str__(self):
        """Return title and username."""
        return '{} by @{}'.format(self.title, self.user.username)


    def save(self, *args, **kwargs):
        self.url = slugify(self.title)
        super(Post, self).save(*args, **kwargs)

In this model we are using "ckeditor", we need to install this library and Pillow:

(env) path-to-the-project\beasttool>pip install django-ckeditor
(env) path-to-the-project\beasttool>pip install Pillow

At "user/category.py" define the following model:

"""Category"""

from django.db import models

# Models

# Create your models here.
class Category(models.Model):
    """Category model."""
    
    name = models.CharField(max_length=100,unique=True)

    class Meta:
        ordering = ('name',)

    def __str__(self):
        return self.name

At "user/comment.py" define the following model:

"""Comment"""

from django.db import models
from django.contrib.auth.models import User
from post.models import Post

# Model

# Create your models here.
class Comment(models.Model):
    """Comment model."""
    
    user = models.ForeignKey(User, on_delete=models.PROTECT)
    profile = models.ForeignKey('user.Profile', on_delete=models.PROTECT)
    post = models.ForeignKey(Post, on_delete=models.PROTECT)
    comment = models.CharField(max_length=5000)

    def __str__(self):
        return self.comment

Set the database

Create a database MariaDB called "beasttool" and set the connection in "settings.py":

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'beasttool',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

Install the mysql driver:

(env) path-to-the-project\beasttool>pip install mysqlclient

Now is time to execute the migrations:

(env) path-to-the-project\beasttool>py manage.py makemigrations

The console should answer something like:

Migrations for 'category':
  category\migrations\0001_initial.py
    - Create model Category
Migrations for 'user':
  user\migrations\0001_initial.py
    - Create model Profile
Migrations for 'post':
  post\migrations\0001_initial.py
    - Create model Post
Migrations for 'comment':
  comment\migrations\0001_initial.py
    - Create model Comment

Next, use the command "migrate":

(env) path-to-the-project\beasttool>py manage.py migrate

Set the admin section

Create a superuser:

(env) path-to-the-project>\beasttool>python manage.py createsuperuser

In this example, I'm using user:"admin" and password:"12345", but in a real case please use a secure password.

Now, after run the server, we can browse to "localhost:8000/admin" to access to the admin section:

At the moment, the admin section just offer to models: Groups and Users, that's the default behavor, now we are going to register our customizations.

At "user/admin.py" we can register the settings we need for the admin section respect user model. Later we will set the same in each app.

"""User admin classes."""

# Django
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib import admin

# Models
from django.contrib.auth.models import User
from user.models import Profile


@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    """Profile admin."""

    list_display = ('pk', 'user', 'photo')
    list_display_links = ('pk', 'user',)
    list_editable = ('photo',)

    search_fields = (
        'user__email',
        'user__username',
        'user__first_name',
        'user__last_name',
    )

    list_filter = (
        'user__is_active',
        'user__is_staff',
        'date_modified',
    )

    fieldsets = (
        ('Profile', {
            'fields': (('user', 'photo', 'website'),),
        }),
        ('Extra info', {
            'fields': (('date_modified'),),
        })
    )

    readonly_fields = ('date_modified',)

class ProfileInline(admin.StackedInline):
    """Profile in-line admin for users."""
    
    model = Profile
    can_delete = False
    verbose_name_plural = 'profiles'


class UserAdmin(BaseUserAdmin):
    """Add profile admin to base user admin."""

    inlines = (ProfileInline,)
    list_display = (
        'username',
        'email',
        'first_name',
        'last_name',
        'is_active',
        'is_staff'
    )


admin.site.unregister(User)
admin.site.register(User, UserAdmin)

At "post/admin.py":

from django.contrib import admin

# Register your models here.
from post.models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    """Post admin."""

    list_display = ('id', 'user', 'title', 'image_header')
    search_fields = ('title', 'user__username', 'user__email')
    list_filter = ('created', 'modified')
        
        
    def get_form(self, request, obj=None, **kwargs):
        self.exclude = ('url', )
        form = super(PostAdmin, self).get_form(request, obj, **kwargs)
        form.base_fields['user'].initial = request.user
        form.base_fields['profile'].initial = request.user.profile
        return form

At "category/admin.py":

from django.contrib import admin

# Register your models here.
from category.models import Category

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    """Category admin."""

    list_display = ('id', 'name')

At "comment/admin.py":

from django.contrib import admin

# Register your models here.
from comment.models import Comment

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    """Comment admin."""

    list_display = ('id', 'user', 'post', 'comment')

After those changes we can refresh the admin section, and this should be the result:

Thanks for reading :)
I invite you to continue reading other entries and visiting us again soon.

Related Posts: