1: Creating a user page


As of right now, we have used Django to create a blog/product showcase; however, let's add some more features to give users a reason to login and register. Let's start by giving users the ability to add pairs of headphones to a personal wish list that they can view on their userpage. In order to accomplish this, we will create a user.html page and a profile model to store information related to users.

 

 

Create a user.html file

env > mysite > main > templates > main > (New File) user.html

Create Django user.htmlGIF

Create a new HTML template in templates > main called user.html.

 

Edit the user.html file

env > mysite > main > templates > main > user.html

{% extends "main/header.html" %}

	{% block content %}
	
	<!--User page HTML-->

	{% endblock %}

Add the code above to setup the user.html file. This page will show the user their profile and a products wish list. As usual, in the following steps we'll connect this template to views.py and urls.py. We setup our document in this order so we can refresh the user.html file as we edit.  








2: Creating a profile model


We'll start by adding new class called Profile. Unlike the other models, this model will have fields to connect a user and to add products for the wishlist.

 

Create a profile model in models.py

env > mysite > main > models.py

from django.db import models
from tinymce.models import HTMLField
from django.contrib.auth.models import User #add this

# Create your models here.
 ...

class Profile(models.Model):   #add this class and the following fields
	user = models.OneToOneField(User, on_delete=models.CASCADE)
	products = models.ManyToManyField(Product)

To connect a Profile to a User, import User from django.contrib.auth.models at the top of the page. Then add the Profile model below the other existing models. The user field has a OneToOneField that forms a direct connection to a user stored in the database. The on_delete=models.CASCADE argument signifies that a Profile object will be deleted if the referenced User is deleted. This practice is typical for a OnetoOneFieldSimilar to how we added tags to articles, we then add the products field as a ManytoManyField to connect multiple products to multiple profiles. 

 

Overwrite the Django user creation and save functions

env > mysite > main > models.py

from django.db import models
from tinymce.models import HTMLField
from django.contrib.auth.models import User
from django.dispatch import receiver #add this
from django.db.models.signals import post_save #add this

# Create your models here.
 ...

class Profile(models.Model):   
	user = models.OneToOneField(User, on_delete=models.CASCADE)
	products = models.ManyToManyField(Product)

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

	@receiver(post_save, sender=User) #add this
	def save_user_profile(sender, instance, **kwargs):
		instance.profile.save()

Next, we need to overwrite the default user creation and user saving functions to automatically add a Profile when a new user is created and save Profile when a user is saved.  First import two new variables (post_save and receiver) from Django at the top of the page.  We include the @receiver decorator to connect our function to a User's post_save signal.  When a user is saved, these functions will be called.  The first function create_user_profile checks if a user is created and then creates a profile to connect the two via the OneToOneField we just added.  The second function save_user_profile saves the User's Profile information whenever a user is saved. Do not worry about the other arguments in the function, such as **kwargs, they are used for passing in additional information.

 

 

Make migrations

macOS Terminal

(env)User-Macbook:env user$python3 manage.py makemigrations
(env)User-Macbook:env user$python3 manage.py migrate

Windows Command Prompt

(env) C:\Users\Owner\Desktop\Code\env>py manage.py makemigrations
(env) C:\Users\Owner\Desktop\Code\env>py manage.py migrate

It's time to update the project's database again with the new Profile model and configuration updates. 

 

Create profiles for existing users

macOS Terminal

(env)User-Macbook:mysite user$ python3 manage.py shell
Python 3.6.8 (default, Oct  7 2019, 12:59:55) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> from main.models import Profile
>>> users = User.objects.filter(profile=None)
>>> for user in users:
...     Profile.objects.create(user=user)
...
<Profile: Profile object (1)>
<Profile: Profile object (2)>
>>>quit()

Windows Command Prompt

(env) C:\Users\Owner\Desktop\code\env\mysite>py manage.py shell
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> from main.models import Profile
>>> users = User.objects.filter(profile=None)
>>> for user in users:
...     Profile.objects.create(user=user)
...
<Profile: Profile object (1)>
<Profile: Profile object (2)>
>>>quit()

As of right now, profiles are only added to users when a user is created. However, we need to add profiles to all of our current, existing users or we will get an error if we try and sign in as one of these users.

Open a Python shell to use Python directly in your CLI.  After each line press Enter to run the line of code.  Start by importing User and then Profile.  Create a queryset of all users without a profile then iterate through the users and create a profile for each one.

Make sure in the for loop you click Tab before entering Profile.objects.create(user=user) and click Enter twice to exit the for loop and create the Profile objects.  If successful, you should see at least one profile object listed in the shell depending on how many users you have added.  Lastly type quit() to exit the shell. Run the server again.

 

Add a user link to navbar.html

env > mysite > main > templates > main > includes > navbar.html

{% load static %}

  <!--Navbar 1-->
  <nav class="navbar navbar-expand-md navbar-light bg-light">
    <div class="navbar-collapse collapse w-100 order-1 order-md-0 dual-collapse2">
      <ul class="navbar-nav mr-auto"> 
      </ul>
    </div>
    <div class="mx-auto order-0">
      <a class="navbar-brand" href="/" style="font-size:30px">
        <img src="{% static "img/logo.png" %}" width="35" height="35" alt="logo"> Encore
      </a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".dual-collapse2">
        <span class="navbar-toggler-icon"></span>
      </button>
    </div>
    <div class="navbar-collapse collapse w-100 order-3 dual-collapse2">
      <ul class="navbar-nav ml-auto">
        {% if user.is_authenticated %}
          <li class="nav-item">
            <a class="nav-link" href="/logout">Logout</a>
          </li>
          <li class="nav-item">
            <a class="btn btn-outline-primary" href="/user">{{user.username|title}}</a>
          </li>
        {% else %}
          <li class="nav-item">
            <a class="nav-link" href="/login">Login</a>
          </li>
          <li class="nav-item">
             <a class="btn btn-outline-primary" href="/register">Register</a>
          </li>
        {% endif %}
      </ul>
    </div>
  </nav>

  <!--Navbar 2-->
  ...

Now that we have added a profile for our users, let's update our navbar template to link to the user page. Open navbar.html and find the anchor element that contains the user's username. Add /user to the href attribute so we can link to the user.html template.








3: Creating user and profile forms


We have our Profile model, now we need to create one form that will allow the user to update their information and another form that allows them to update their profile with products added to a wishlist. 

 

Create a user form in forms.py

env > mysite > main > forms.py

...

# Create your forms here.
class NewUserForm(UserCreationForm):
    ...

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('username','first_name', 'last_name', 'email')

Open the forms.py file so we can create a form to allow users to edit their username, first name, last name, and email. Below NewUserForm add a new form called UserForm. You'll notice that the UserForm takes in forms.ModelForm as a parameter. This allows us to use class Meta to create a form based on a model.  It also saves us time so we don't have to create fields that already exist for the model User.

 

Create a profile form in forms.py

env > mysite > main > forms.py

...
from .models import Profile #import Profile from models.py

# Create your forms here.
class NewUserForm(UserCreationForm):
    ...

class UserForm(forms.ModelForm):
   ...

class ProfileForm(forms.ModelForm):
	class Meta: 
		model = Profile
		fields = ('products',)

Next we'll add a profile form so the user can edit the products in their wishlist. Start by importing Profile from models.py at the top of the page with the other imports. Declare the new form class ProfileForm(forms.ModelForm) below the UserForm class. Specify the model as Profile and set the fields to only include 'products', the ManytoManyField specified in the Profile model.

 

Add user.html to urls.py

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name="homepage"),
    ...
    path("user", views.userpage, name = "userpage"),
]

Edit the main > urls.py file to include a URL path to a user function in views.py

 

Create userpage function in views.py

env > mysite > main > views.py

...
from .forms import NewUserForm, UserForm, ProfileForm #import UserForm and ProfileForm
...

# Create your views here.
...

def userpage(request):
	user_form = UserForm(instance=request.user)
	profile_form = ProfileForm(instance=request.user.profile)
	return render(request=request, template_name="main/user.html", context={"user":request.user, "user_form":user_form, "profile_form":profile_form })

Now that your forms are made, we need to import them into views.py and pass them as a context to user.html so an actual user can submit the forms. Import UserForm and ProfileForm from .forms just like we did NewUserForm. Create and setup the forms as variables while setting their instances to request.user.  This ensures that the forms will display the user's current information, otherwise they would be blank.  Add three new variables to context with "user":request.user, "user_form":userform, and "profile_form":profileform

 

Add variables to user.html

env > mysite > main > templates > main > user.html

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    <div class="row">
	    	<div class="col-sm-12 col-md-12 col-lg-3 pb-4">
		    	<div class="card p-4">
			    	
		    	</div>
		  	</div>
		    <div class="col-sm-12 col-md-12 col-lg-9 pb-4">
		    	
		    </div> 
		</div>
	</div>

	{% endblock %}

Start by adding a row nested within a container. Then add two columns: one with col-lg-3 and the other with col-lg-9. Include col-sm-12 and col-md-12 for both columns. Then add a card with padding to the first column.

 

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    <div class="row">
	    	<div class="col-sm-12 col-md-12 col-lg-3 pb-4">
		    	<div class="card p-4">
			    	<h4 class="card-title font-weight-bold text-center">Hello, {{ user.username|title }}</h4>
			      	<hr>
			    	<div class="card-text">
			    		<p class="text-muted mb-0" style="font-size: 12px">Username:</p>
			    		<p>{{ user.username }}</p>
			    		<p class="text-muted mb-0" style="font-size: 12px">Name:</p>
				  		<p>{{ user.first_name }} {{ user.last_name }}</p>
				  		<p class="text-muted mb-0" style="font-size: 12px">Email:</p>
				  		<p>{{ user.email }} </p>
			        </div>
		    	</div>
		  	</div>
		    <div class="col-sm-12 col-md-12 col-lg-9 pb-4">
		    	...
		    </div> 
		</div>
	</div>

	{% endblock %}

In the first column with the card, add the code above to display the user's profile information.

 

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    <div class="row">
	    	<div class="col-sm-12 col-md-12 col-lg-3 pb-4">
		    	...
		  	</div>
		    <div class="col-sm-12 col-md-12 col-lg-9 pb-4">
		    	<div class="row">
		    		<div class="col-6 my-auto">
		    			<h2 class="font-weight-bold pt-4">Wishlist</h2>
		    		</div>
		    	</div>
		      	<br>
		      	<div class="row">
			      	{% for p in user.profile.products.all %}
				    	<div class="col-sm-12 col-md-6 col-lg-4 pb-4">
			                <div class="h-100">
			                	<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 225px; object-fit: scale-down;">
			                	<div class="card-body">
			                  		<h5 class="card-title">{{ p.product_name }}</h5>
			                    	<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
			                    	<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
			                	</div>
			              	</div>
			            </div>
			    	{% endfor %}
		        </div>
		    </div> 
		</div>
	</div>

	{% endblock %}

In the second column, nest a heading within a row and column that displays the text "Wishlist". Then create another row below that contains a for loop iterating over the products saved to the user profile. Since each user can add multiple products to their wishlist, this for loop needs to display each pair of headphones within its own column.

User page








4: Using Bootstrap modals


With the HTML page formatted, let's use a Bootstrap modal for the forms, meaning the forms will be accessible through a popup window.

 

Connect the UserForm to the user.html

env > mysite > main > templates > main > user.html

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    <div class="row">
	    	<div class="col-sm-12 col-md-12 col-lg-3 pb-4">
		    	<div class="card p-4">
			    	<h4 class="card-title font-weight-bold text-center">Hello, {{ user.username|title }}</h4>
			      	<hr>
			    	<div class="card-text">
			    		<p class="text-muted mb-0" style="font-size: 12px">Username:</p>
			    		<p>{{ user.username }}</p>
			    		<p class="text-muted mb-0" style="font-size: 12px">Name:</p>
				  		<p>{{ user.first_name }} {{ user.last_name }}</p>
				  		<p class="text-muted mb-0" style="font-size: 12px">Email:</p>
				  		<p>{{ user.email }} </p>
						<div class="text-right">
							<button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#profileModal">Edit Profile</button>
						</div>
			        </div>
		    	</div>
		  	</div>
		    <div class="col-sm-12 col-md-12 col-lg-9 pb-4">
		    	...
		    </div> 
		</div>
	</div>


	{% endblock %}

First nest a modal button in a division element with the class attribute text-right below the {{ user.email }} paragraph element. The modal button itself with have two attributes data-toggle and data-target which will connect to the modal/popup.

 

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    ...
	</div>

	<!--Edit user modal-->
	<div class="modal fade" tabindex="-1" id="profileModal" role="dialog">
	  	<div class="modal-dialog" role="document">
		    <div class="modal-content">
		      	<div class="modal-header">
			        <h5 class="modal-title">Edit Profile</h5>
			        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
			          <span aria-hidden="true">&times;</span>
			        </button>
		        </div>
		        <div class="modal-body">
			        <form method="post">
			          	{% csrf_token %}
			          	{{ user_form|crispy }}
		        </div>
		      	<div class="modal-footer">
		        		<button type="submit" class="btn btn-primary">Save changes</button>
		        	</form> 
		        	<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
		        </div>
		    </div>
	  	</div>
	</div>


	{% endblock %}

Now create the content for the modal.  Go to the bottom of the page, add a new comment "Edit user modal", and add a Bootstrap modal containing the user form.  Feel free to copy+paste the example above.  Notice how we render the form with our previously installed package, crispy forms, by loading crispy form tags near the top of the file.

Save the file and refresh the page. There will now be an "Edit Profile" button that when clicked opens a Bootstrap modal with the UserForm.

User page with edit profile formGIF

 

Connect the ProfileForm to the user.html

env > mysite > main > templates > main > user.html

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    <div class="row">
	    	<div class="col-sm-12 col-md-12 col-lg-3 pb-4">
	    	...

		    <div class="col-sm-12 col-md-12 col-lg-9 pb-4">
		    	<div class="row">
		    		<div class="col-6 my-auto">
		    			<h2 class="font-weight-bold pt-4">Wishlist</h2>
		    		</div>
		    		<div class="col-6 my-auto text-right">
		    			<button type="button" class="btn btn-primary font-weight-bold" style="border-radius: 50%" data-toggle="modal" data-target="#wishlistModal">+</button>
		    		</div>
		    	</div>
		      	<br>
		      	<div class="row">
			      	{% for p in user.profile.products.all %}
				    	<div class="col-sm-12 col-md-6 col-lg-4 pb-4">
			                <div class="h-100">
			                	<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 225px; object-fit: scale-down;">
			                	<div class="card-body">
			                  		<h5 class="card-title">{{ p.product_name }}</h5>
			                    	<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
			                    	<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
			                	</div>
			              	</div>
			            </div>
			    	{% endfor %}
		        </div>
		    </div> 
		</div>

	<!--Edit user modal-->
	...

	{% endblock %}

Now let's add the profile form so users can add products to the wishlist. Code another Bootstrap modal button within a new column directly after the column containing the Wishlist heading. Stylize the button to be round using border-radius:50% and add a plus symbol as the text. Notice data-target="#wishlistModal" ensures the button connects to a different modal.

 

{% extends "main/header.html" %}

	{% block content %}

	{% load crispy_forms_tags %}  

	<!--User-->
	<br><br>
	<div class="container mx-auto">
	    ...
	</div>
	    	

	<!--Edit user modal-->
	...

	<!--Wishlist modal-->
	<div class="modal fade" tabindex="-1" id="wishlistModal" role="dialog">
		<div class="modal-dialog" role="document">
		    <div class="modal-content">
		      	<div class="modal-header">
			        <h5 class="modal-title">Edit Profile</h5>
			        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
			          <span aria-hidden="true">&times;</span>
			        </button>
		      	</div>
		      	<div class="modal-body">
			        <form method="post">
			        	{% csrf_token %}
					  	{{ profile_form|crispy }}
					  	<span class="text-muted" style="font-size:12px">Hold down "Control", or "Command" on a Mac, to select more than one.</span>
		      	</div>
		      	<div class="modal-footer">
		        		<button type="submit" class="btn btn-primary">Save changes</button>
		        	</form> 
		        	<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
		      	</div>
		    </div>
	  	</div>

	{% endblock %}  

Add a new comment titled "Wishlist modal" after the "Edit user modal" and paste the ProfileForm code from above within it. Save the file and reload the user page in the browser.

Edit wishlist on user pageGIF

 

Update views.py to save the user and profile form changes

env > mysite > main > views.py

...

def userpage(request):
	if request.method == "POST":
		user_form = UserForm(request.POST, instance=request.user)
		profile_form = ProfileForm(request.POST, instance=request.user.profile)
		if user_form.is_valid():
		    user_form.save()
		    messages.success(request,('Your profile was successfully updated!'))
		elif profile_form.is_valid():
		    profile_form.save()
		    messages.success(request,('Your wishlist was successfully updated!'))
		else:
		    messages.error(request,('Unable to complete request'))
		return redirect ("main:userpage")
	user_form = UserForm(instance=request.user)
	profile_form = ProfileForm(instance=request.user.profile)
	return render(request = request, template_name ="main/user.html", context = {"user":request.user, 
		"user_form": user_form, "profile_form": profile_form })

Currently the forms are being passed as context to the user.html template but we need to add an if statement to detect when a POST HTTP request method is submitted. Otherwise, the forms will not submit and the user page will not update.

After checking the request method, add another if condition to check if the user_form is valid. Then save the changes to the user form and display the message "Your profile was successfully updated!"; else if the profile_form is valid, save the changes to the profile form and display the message "Your wishlist was successfully updated!" Else, if the forms are not valid, display the message "Unable to complete request". Save the changes to the views.py and test the forms again. This time you will receive a message corresponding with the request and an updated page.

User page edit profile and wishlistGIF








5: Adding a wishlist button to the products


The user can update their wishlist from the user.html page but let's make it easier to add products to the wishlist with a button on each product card.

 

Add a wishlist button to the products.html

env > mysite > main > templates > main > products.html

{% extends "main/header.html" %}

    {% block content %}

    <!--Products-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Products</h1>
        <hr>
        <br>
        <div class="row">
             {% for p in page_obj %}          
                <div class="col-sm-12 col-md-6 col-lg-4 pb-4">
                    <div class="card h-100" style="border:none">
                        <img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 200px; object-fit: scale-down;">
                        <div class="card-body">
                            <h5 class="card-title">{{ p.product_name }}</h5>
                            <p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
                            <p class="card-text">{{ p.product_description }}</p>
                            <div class="row">
                                <div class="col-3 text-center">
                                    
                                </div>
                                <div class="col-4 text-center">
                                    
                                </div>
                                <div class="col-5 text-center">
                                    <a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>                              
                                </div>
                            </div> 
                        </div>
                    </div> 
                </div>
            {% endfor %}
        </div>

        <!--Pagination-->
        ...
        <!--end of Pagination-->


    </div>
    {% endblock %}

After the product description, add a division with a row class attribute and nest three division elements: one with col-3 one with col-4 and one with col-5. Add the class text-center to all. Move the anchor element with the affiliate url to the last column.

 

{% extends "main/header.html" %}

    {% block content %}

    <!--Products-->
    <div class="container py-5">
        <h1 class="font-weight-bold">Products</h1>
        <hr>
        <br>
        <div class="row">
             {% for p in page_obj %}          
                <div class="col-sm-12 col-md-6 col-lg-4 pb-4">
                    <div class="card h-100" style="border:none">
                        <img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 200px; object-fit: scale-down;">
                        <div class="card-body">
                            <h5 class="card-title">{{ p.product_name }}</h5>
                            <p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
                            <p class="card-text">{{ p.product_description }}</p>
                            <div class="row">
                                <div class="col-3 text-center">
                                    {% if user.is_authenticated %}
                                        <form method="post">
                                            {% csrf_token %}
                                            <input type="hidden" value="{{p.pk}}" name="product_pk">
                                            <button type="submit" class="btn btn-outline-primary" style="font-size:18px; border-radius: 50%">★</button>
                                        </form>
                                    {% else %}
                                        <a href="/register" class="btn btn-outline-primary" style="font-size:18px; border-radius: 50%">★</a>
                                    {% endif %}
                                </div>
                                <div class="col-4 text-center">
                                    
                                </div>
                                <div class="col-5 text-center">
                                    <a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>                              
                                </div>
                            </div> 
                        </div>
                    </div> 
                </div>
            {% endfor %}
        </div>

        <!--Pagination-->
        ...
        <!--end of Pagination-->


    </div>
    {% endblock %}

In the division element with col-3, check if the user is authenticated. If true, add a form to submit the product. The form will have a hidden input element with the value of the product's id number and the designated name product_pk.  Hidden inputs allow us to get a value we need to collect but users do not need to see.  Afterwards add a submit button with a star icon. Add an else statement if the user is not authenticated that displays a button linking to the register page. Save the file.

 

 

Update the product views.py

env > mysite > main > views.py

...

# Create your views here.
...

def products(request):
	if request.method == "POST":
		product_id = request.POST.get("product_pk")
		product = Product.objects.get(id = product_id)
		request.user.profile.products.add(product)
		messages.success(request,(f'{product} added to wishlist.'))
		return redirect ('main:products')
	products = Product.objects.all()
	paginator = Paginator(products, 18)
	page_number = request.GET.get('page')
	page_obj = paginator.get_page(page_number)
	return render(request = request, template_name="main/products.html", context = { "page_obj":page_obj})

Check if the request method is POST in the products function.  Set the hidden input value that represents a product's id as product_id.  Then get the product object based on product_id and add the product to the user's profile.  Finally add a success message to notify the user that the product has been added to their wishlist. The last thing is to return a redirect to the products page. 

Save the file and reload the browser page. Go to the products page and click on the blue star next to one of the products. If logged in, you will get a message saying the product was added to the wishlist. If logged out, you will be brought to the register page.

Products page with wishlist buttonGIF

 

Add a wishlist button to the products in the home.html

env > mysite > main > templates > main > home.html

{% extends "main/header.html" %}

	{% block content %}
	
	<!--CTA-->
	...

	<!--Headlines-->
	...

	<!--Products-->
	<div class="container py-3">
		<h2>Products</h2>
		<hr>
		<br>
		<div class="row">
			{% for p in product %}
				<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
				  	<div class="card h-100" style="border:none">
						<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 250px; object-fit: scale-down;">
						<div class="card-body">
							<h5 class="card-title">{{ p.product_name }}</h5>
					  		<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
					  		<p class="card-text">{{ p.product_description }}</p>
					  		<div class="row">
								<div class="col-6">
						  			
								</div>
								<div class="col-6">
							  		<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>	
								</div>
							</div>
				  		</div>
					</div>
				</div>
			{% endfor %}
		</div>
		<div class="container text-right">
			<a href="/blog/articles">View more</a>
		</div>
	</div>

	<!--Blog-->
	...

	{% endblock %}

Similar to the products page, add a division element with a row class attribute after the product description. Nest two columns within the row. Move the anchor element with the affiliate url to the last column.

 

{% extends "main/header.html" %}

	{% block content %}
	
	<!--CTA-->
	...

	<!--Headlines-->
	...

	<!--Products-->
	<div class="container py-3">
		<h2>Products</h2>
		<hr>
		<br>
		<div class="row">
			{% for p in product %}
				<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
				  	<div class="card h-100" style="border:none">
						<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 250px; object-fit: scale-down;">
						<div class="card-body">
							<h5 class="card-title">{{ p.product_name }}</h5>
					  		<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
					  		<p class="card-text">{{ p.product_description }}</p>
					  		<div class="row">
								<div class="col-6">
						  			{% if user.is_authenticated %}
						  				<form method="post">
										{% csrf_token %}
											<input type="hidden" value="{{p.pk}}" name="product_pk">
											<button type="submit" class="btn btn-outline-primary" style="font-size:18px; border-radius: 50%">★</button>
						  				</form>
						  			{% else %}
										<a href="/register" class="btn btn-outline-primary" style="font-size:18px; border-radius: 50%">★</a>
						  			{% endif %}
								</div>
								<div class="col-6">
							  		<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>	
								</div>
							</div>
				  		</div>
					</div>
				</div>
			{% endfor %}
		</div>
		<div class="container text-right">
			<a href="/blog/articles">View more</a>
		</div>
	</div>

	<!--Blog-->
	...

	{% endblock %}

Add an if statement that checks if the user is authenticated.  If true, display a form that the user can submit, otherwise display a register button.  Feel free to copy the code above. Save the file. 

 

Code-it-yourself: Update the homepage view for the wishlist

env > mysite > main > views.py

...

# Create your views here.

def homepage(request):
	if request.method == "POST":
		product_id = request.POST.get('product_pk')
		product = Product.objects.get(id = product_id)
		request.user.profile.products.add(product)
		messages.success(request,(f'{product} added to wishlist.'))
		return redirect ('main:homepage')	
	product = Product.objects.all()[:4]
	new_posts = Article.objects.all().order_by('-article_published')[:4]
	featured = Article.objects.filter(article_tags__tag_name='Featured')[:3]
	most_recent = new_posts.first()
	return render(request=request, template_name="main/home.html", context={'product':product, 'most_recent':most_recent, "new_posts":new_posts, "featured":featured})

Now add the same if condition from the products function to the homepage function.  Check if the request method is POST, get the product object, and add the product to the user's list of products.  Display the same success message as the products function then redirect to the homepage.

 

Test the wishlist feature on the homepage

Homepage with wishlistGIF

Save the file and test the wishlist button on the homepage in the browser.

 

Code-it-yourself: Edit the CTA button

env > mysite > main > templates > main > home.html

{% extends 'main/header.html' %}

  {% block content %}

  {% load static %}
       

  <!--CTA-->
  <div class="cta-banner"> 
    <div class="container py-5">
      <div class="row">
        <div class="col-sm-12 col-md-12 col-lg-6 pb-4">
          <h1 class="display-4 font-weight-bold">Elevate your listening</h1>
          <h5>Everyday headphones that make your favorite artists sound like their performing a never-ending encore.</h5>
          {% if user.is_authenticated %}
            <a class="btn btn-primary mt-2" href="/user">VIEW WISHLIST</a>
          {% else %}
            <a class="btn btn-primary mt-2" href="/products">FIND YOUR SET</a>
          {% endif %}
        </div>
      </div>
    </div>
  </div>
	...

  {% endblock %}

The last thing we'll do is edit the CTA button to display different information depending on if a user is logged in. Create a new if condition that checks if a user is logged in.  If true, add a "VIEW WISHLIST" button that links to the user page, else show a button that says "FIND YOUR SET" and links to the products page. 

 

Test the CTA button

CTA button with user authentication GIF

Save the file and test this feature in the browser.






Quiz Questions


1. When should you makemigrations and migrate your project?


2. Which command allows a user to run Python commands directly in the CLI?


Next lesson


Check out the comments and debug buttons if you get stuck.