Aug 3 '20

< all posts

Django Diaries - Database & Users

#django#webdev

Reading Time: 7 mins

This is part 2 of the Django Notes. Today we will implement a database, register and login users.

Again this is all based on what I learned from Corey Schafers Youtube Series. The code for this article from the tutorial is here: (Numbers 5 - 9)

Models

In Django Database tables are defined in 'models.py' we create a model for each table.

For a Blog, we would need a Post model, containing an author, a post title, the content and date.

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

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)
    author = models.ForeignKey(User, on_delete=models.CASCADE)

Here we have created a Post model with a character field of 100 characters, an unrestricted text Field, a date set to timezone.now this will be equal to the time of when the post was created. Finally we use a foreign key to create a one-to-many relationship as a user can have multiple posts but a post should only have one author. 'models.CASCADE' is a way of telling Django that if a user is deleted then the post should be deleted too.

To make the changes to the database, we have to make a migration.

python manage.py makemigrations

If you run make migrations you should see some numbers that show the number of migrations you have run: '0001_initial.py'. We can use this to view to SQL that will be run to make the table. Django uses SQLite by default and Django will do all the SQL for you.

python manage.py sqlmigrate <app_name> <migration_number>
python manage.py sqlmigrate blog 0001

To run the migrations we can run the migrate command:

python manage.py migrate

Let's see how we can query the database in the django shell.

(Assuming users have been created from the admin panel)

python manage.py shell

# Get the needed models
>>> from blog.models import post
>>> from django.contrib.auth.models import User

# Show all users
>>> User.objects.all()
<QuerySet: [<User: UserOne>, <User: USerTwo>]

# Show only first user
>>> User.objects.first()
<User: UserOne>

# Show first user that matched filter
>>> User.objects.filter(username="UserOne").first()
<User: UserOne>

# We can query a user
>>> user = User.objects.filter(username="UserOne").first()
>>> user.id
1

# See all the posts
>>> Post.objects.all()
<QuertSet []>

# Make a new post
>>> post_1 = Post(title="Blog one", content="New blog",author=user)

# Save the post in the database
>>> post_1.save()

We can make a nicer output for printing the post by adding str or repr dunder methods:

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

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f"Post Name: {self.title} by {self.author}"

If we wanted to get all the posts by a specific user, then back in terminal we can import the models:

# all instances are under .modelName_set
>>> user.post_set.all()
<QuerySet [<Post: Post Name: Blog One by user>]>
>>> user.post_set.create(title="Blog 3",content="Post 3")
<Post: Post Name: Blog 3 by user>
# We didn't specify an author as we running the users post set

Using This Post data in the views

blog/views.py

from django.shortcuts import render
from .models import Post

def home(req):
    context = {
       'posts':Post.objects.all()
    }
    return render(req, 'blog/home.html',context)

Here we would be rendering dates in our template. We can change the date format using the | character as a filter.

blog/templates/blog/home.html

<small>{{ post.date_posted | date:"d / m / Y" }}</small>

We have now added a post models. However to view this on the admin page we must register it:

blog/admin.py

from django.contrib import admin
from .models import Post

admin.site.register(Post)

Now it will show up on the admin page and we can update blogs manually from there.

User Registration

It is a good idea to create a new app to hold the user logic:

python manage.py startapp users

Remember to add this to installed apps: 'users.apps.UsersConfig'

The Default Django forms aren't the prettiest, so we can use crispy forms. To use this run a pip install and add it to installed apps and then specify the styles.

users/views.py

from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.forms import UserCreationForm

# Django already has all the necessary logic such as validation handled

def register(request):
    # if the form is submitted validate if not just show the form
    if request.method == "POST":
        Form = UserCreationForm(request.POST)
        # UserCreation and validation comes from django
        if Form.is_valid():
            Form.save()  # save user and hash password
            # Grab the username
            username = Form.cleaned_data.get("username")
            # Render a message. You can grab this and conditionally render in the UI
            messages.success(
                request, f"Your account has been created. You can now log in")
            # Take the user to the blog screen
            return redirect("blog-home")

    else:
        Form = UserCreationForm()
    return render(request, "users/register.html", {'form': Form})

users/templates/users/register.html

Alt Text

Finally we need to add a url pattern to the projects url.py

from django.urls import path, include
from users import views as user_views

urlpatterns = [
    path("register/",user_views.register,name="register")
]

Now all the validation will be handled by Django and styles by crispy forms. NOTE form.save() here will actually add a new user.

Adding an Email field to the form

The Django form doesn't give us an email by default, we can add one manually. We create a file in the users app called 'forms.py'

users/forms.py

# make a custom form

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from .models import Profile

# inherit from userCreationForm
class UserRegister(UserCreationForm):
    # by default required = True
    email = forms.EmailField()

    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']

In meta we define what model the form should interact with. We can then use this instead of the default UserCreation form:

from .forms import UserRegister

def register(request):
    if request.method == "POST":
        Form = UserRegister(request.POST)
        if Form.is_valid():
            Form.save()  # save user and hash password
            username = Form.cleaned_data.get("username")
            messages.success(
                request, f"Your account has been created. You can now log in")
            return redirect("login")

    else:
        Form = UserRegister()
    return render(request, "users/register.html", {'form': Form})

User Login

Django has most of this taken care of, so we can just import the remade views into the project 'urls.py' and render our own templates.

from django.contrib.auth import views as auth_views

urlpatterns = [
    path("login/", auth_views.LoginView.as_view(template_name="users/login.html"), name="login"),
    path("logout/", auth_views.LogoutView.as_view(template_name="users/logout.html"), name="logout")
]

These are class based views to we use the as_view() method. The login template will be very similar to the register template.

Notice the format of the href attributes. We open a code block and define a url and a url name which was defined in the path.

Another thing Django needs to login a user is somewhere to take them when they are logged in. By default I looks for an accounts view, but we can change this in settings.py by specifying a LOGIN_REDIRECT. We can set this to '/' to go to the home page.

If we wanted to change the navigation based on whether a user is logged in we can do this using DTL. By opening a code block and access the is_authenticated attribute of the user.

if user.is_authenticated

Restricting Routes.

If a user who isn't logged in we can stop them from accessing certain views. To do this Django has a decorator:

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, "users/profile.html", context)

If the user tries to access the profile without being authenticated. Django looks for a route at 'accounts/login/?next=/profile/'. We can again change this by adding a LOGIN_URL variable to 'settings.py'

Notice the parameter of next. Django is keeping track of where the user wanted to go and will redirect them when they login.

Uploading Images

To upload images use need to extend the default user model.

users/models.py

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

class Profile(models.Model):
    # CASCASE if delete user delete profile too
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    # profile_pics is a folder that will be created, if there is no image a default will be used.
    image = models.ImageField(default="default.jpg", upload_to="profile_pics")

    def __str__(self):
        return f"{self.user.username} Profile"

    def save(self):
        super().save()
        img = Image.open(self.image.path)
        if img.height > 300 or img.width > 300:
            output_size = (300, 300)
            img.thumbnail(output_size)
            img.save(self.image.path)

Image Field needs Pillow installed, this is a python library to manipulate images, so you can run pip install Pillow Save the database by running migrations.

Remember to register this model with the admin page.

The save method is used to save the image on submit and then shrink the image, if the image is too big.

We can change where the images are saves by adding a MEDIA_ROOT and MEDIA_URL to the settings.py file. For example:

# Where uploaded files will be located.
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = '/media/'

We now need to add the media to urls.py

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)

This allows usage only in Debug mode, we can use a different method in production.

When we make a user, currently a Profile won't be created. This is why we use signals to trigger an action. We want to make a profile when a user registers.

Make a new file called 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

# this allows to create a profile
# it sends a signal if a user was created


@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()

# this has to go into ready in user app.py

app.py

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'

    def ready(self):
        import users.signals

Thank you for reading part Two. Next Up CRUD functionality.


✨If you would like to follow my day to day development journey be sure to check my Instagram.