Activity Solutions For Web Development With Django
Activity Solutions For Web Development With Django
2 | Appendix
1. The index view in views.py should now just be one line, which returns the
rendered template:
def index(request):
return render(request, "base.html")
<head>
<meta charset="UTF-8">
<title>Welcome to Bookr</title>
</head>
The <body> tag should contain an h1 tag with the same message:
<body>
<h1>Welcome to Bookr</h1>
</body>
The index page of your site should now look like Figure 1.54:
You have created a welcome splash page for Bookr, to which we will be able to add
links to other parts of the site, as we build them.
Chapter 1: Introduction to Django | 3
Use the same search_text variable in an <h1> tag inside the body:
<body>
<h1>Search Results for <em>{{ search_text }}</em></h1>
</body>
def book_search(request):
search_text = request.GET.get("search", "")
path('book-search', reviews.views.book_search)
Try changing test to other values to see it in action, for example, Web
Development with Django. Also, try the text </html> to see how HTML
entities are automatically escaped when rendered in a template. Refer to Figure 1.52
and Figure 1.53 to see how your page should look for the latter two examples.
Chapter 2: Models and Migrations | 5
1. Create the juggler project by running the following command in the shell:
INSTALLED_APPS = ['django.contrib.admin',\
'django.contrib.auth',\
'django.contrib.contenttypes',\
'django.contrib.sessions',\
'django.contrib.messages',\
'django.contrib.staticfiles',\
'projectm']
models.py
5. Create the migration scripts and migrate the models by executing the following
commands separately:
7. In the shell, add the following code to import the model classes:
>>> project.task_set.all()
<QuerySet [<Task: Buy paint and tools>, <Task: Paint kitchen>, <Task:
Paint living room>, <Task: Paint other rooms>]>
11. Using a different query method, list all the tasks associated with the project:
Like the books_list.html template, this template also inherits from the
base.html file by using the following code:
{% extends 'base.html' %}
{% block content %}
{% endblock %}
Because it inherited from base.html, you will be able to see the navigation bar
on the book details page as well. The remaining part of the template uses the
context to display the details of the book.
You will also need to add the following import statement at the top of the file:
The book_detail function is the book details view. Here, request is the HTTP
request object that will be passed to any view function upon invocation. The next
parameter, pk, is the primary key or the ID of the book. This will be part of the URL
path as invoked when we open the book's detail page. We have already added this
code in bookr/reviews/templates/reviews/books_list.html:
The preceding code snippet represents a button called Reviews on the books
list page. Upon clicking this button, /book/<id> will be invoked. Note that
{{ item.book.id }} refers to the ID or the primary key of a book. The
Reviews button should look like this:
Here, <int:pk> represents an integer URL pattern. The newly added URL path
is to identify and map the /book/<id>/ URL of the book. Once it is matched,
the book_detail view will be invoked.
4. Save all the modified files and once the Django service restarts, open
http://0.0.0.0:8000/ or http://127.0.0.1:8000/ in the browser to
see the book's details view with all the review comments:
In this activity, we implemented the book details view, template, and URL mapping to
display all the details of the book and its review comments.
10 | Appendix
1. Create the Django project app, run the migrations, create the superuser, and run
the app:
{% block content %}
{% endblock %}
class Comment8orAdminSite(admin.AdminSite):
index_title = 'c8admin'
title_header = 'c8 site admin'
site_header = 'c8admin'
logout_template = 'comment8or/logged_out.html'
12 | Appendix
class MessageboardConfig(AppConfig):
name = 'messageboard'
class MessageboardAdminConfig(AdminConfig):
default_site = 'admin.Comment8orAdminSite'
TEMPLATES = [{'BACKEND':\
'django.template.backends.django.DjangoTemplates',\
'DIRS':\
[os.path.join(BASE_DIR, 'comment8or/templates')],\
…}]
Chapter 4: Introduction to Django Admin | 13
By completing this activity, you have created a new project and successfully
customized the admin app by sub-classing AdminSite. The customized admin
app in your Comment8r project should look something like this:
1. The default list display for the Contributors change list uses the
Model.__str__ method's representation of class name followed by id,
such as Contributor (12), Contributor(13), and so on, as follows:
This code gets a string of initials of the contributor's First Names and then
returns a string with the Last Names followed by the initials:
def initialled_name(self):
""" self.first_names='Jerome David',
self.last_names='Salinger' => 'Salinger, JD'
"""
initials = ''.join([name[0] for name
in self.first_names.split(' ')])
return "{}, {}".format(self.last_names, initials)
3. Replace the __str__ method for Contributor with one that calls
initialled_name():
def __str__(self):
return self.initialled_name()
Chapter 4: Introduction to Django Admin | 15
class Contributor(models.Model):
"""A contributor to a Book, e.g. author, editor, co-author."""
first_names = models.CharField(max_length=50,
help_text=\
"The contributor's first"\
"name or names.")
last_names = models.CharField(max_length=50,
help_text=\
"The contributor's last"\
"name or names.")
email = models.EmailField\
(help_text="The contact email for the contributor.")
def initialled_name(self):
""" self.first_names='Jerome David',
self.last_names='Salinger'=> 'Salinger, JD'
"""
initials = ''.join([name[0] for name
in self.first_names.split(' ')])
return "{}, {}".format(self.last_names, initials)
def __str__(self):
return self.initialled_name()
Figure 4.66: Contributors change list with the last name and initial of the first name
16 | Appendix
class ContributorAdmin(admin.ModelAdmin):
5. Modify it so that on the Contributors change list, records are displayed with
two sortable columns – Last Names and First Names:
6. Add a search bar that searches on Last Names and First Names. Modify it
so that it only matches the start of Last Names:
list_filter = ('last_names',)
8. Modify the register statement for the Contributor class to include the
ContributorAdmin argument:
admin.site.register(Contributor, ContributorAdmin)
class ContributorAdmin(admin.ModelAdmin):
list_display = ('last_names', 'first_names')
list_filter = ('last_names',)
search_fields = ('last_names__startswith', 'first_names')
admin.site.register(Contributor, ContributorAdmin)
Chapter 4: Introduction to Django Admin | 17
With the two sortable columns, the filter, and the search bar, the list will look
like this:
1. Open base.html in the main templates directory. Find the <style> tags in
<head> and add this rule at the end (after the .navbar-brand rule):
.navbar-brand > img {
height: 60px;
}
After adding this rule, your <style> element should look like Figure 5.25:
It will be just inside the start of <body>. Change it to add {% block brand
%} and {% endblock %} wrappers around the text:
<a class="navbar-brand" href="/">{% block brand %}
Book Review{% endblock %}</a>
3. Create a directory named static, inside the reviews app directory. Inside
this static directory, create a directory named reviews. Put logo.png from
https://packt.live/2WYlGjP inside this directory. Your final directory structure should
look like this:
4. Create a templates directory inside the Bookr project directory. Move the
reviews/templates/reviews/base.html file into this directory.
Your directory structure should look like this:
TEMPLATES = [{'BACKEND':\
'django.template.backends.django.DjangoTemplates',\
'DIRS': [os.path.join(BASE_DIR, 'templates')],\
'APP_DIRS': True,\
'OPTIONS': {'context_processors':
['django.template.context_processors.debug',\
20 | Appendix
'django.template.context_processors.request',\
'django.contrib.auth.context_processors.auth',\
'django.contrib.messages.context_processors.messages',
],},\
},
]
Remember that you may need to add import os to the top of this file if you do
not have it already. Save and close settings.py.
The file will open. Add an extends template tag to the file, to extend from
base.html:
{% extends 'base.html' %}
7. We will use the {% static %} template tag to generate the img URL, so
we need to make sure the static library is loaded. Add this line after the
extends line:
{% load static %}
Then override the {% block brand %} content, and use the {% static
%} template tag to generate the URL to the reviews/logo.png file:
{% block brand %}<img src="{% static 'reviews/logo.png' %}">
{% endblock %}
There should now be three lines in this new base.html file. You can save and
close it. The completed file is available at http://packt.live/3qxs52f.
8. Open views.py in the reviews app. Change the render call in the index
view to render base.html instead of reviews/base.html. Once you've
made this change, your view function should look like this:
def index(request):
return render(request, "base.html")
You can then save and close views.py. The completed views.py file can be
found at http://packt.live/38XTc0y.
Start the Django dev server, if it's not already running. The pages you should check
are the home page, which should look the same, and the books list page and book
detail page – these should have the Bookr Reviews logo displayed.
Chapter 5: Serving Static Files | 21
2. Open the main base.html file, then find the style element in head. Copy
its contents (but not the <style> and </style> tags themselves) into the
main.css file. Then at the end, add the CSS snippet given in the activity
instructions. Your file should contain this content when finished:
.navbar {
min-height: 100px;
font-size: 25px;
}
.navbar-brand {
font-size: 25px;
}
body {
font-family: 'Source Sans Pro', sans-serif;
background-color: #e6efe8;
color: #393939;
22 | Appendix
You can save and close main.css. Then return to base.html and remove the
entire style element.
3. Load the static library on the second line of base.html, by adding this:
{% load static %}
Then you can use the {% static %} tag to generate the URL for main.css,
to be used in a link element:
Insert this into the head element of the page, where <style> was before you
removed it.
4. Underneath the <link> instance you added in the previous step, add this to
include the Google fonts CSS:
You can now save and close base.html. The completed file can be found at
http://packt.live/3iqGVop.
6. Start the Django dev server – you may need to restart it to load the changes to
settings.py. Then visit http://127.0.0.1:8000/ in your browser. You
should see updated fonts and background colors on all the Bookr pages:
Figure 5.29: Book list with the new font and background color
24 | Appendix
2. Move the logo into the project static directory (not the reviews
static directory):
Figure 5.30 shows the Project pane in PyCharm illustrating the correct location
for logo.png.
Change the content to be an <img> tag. The src attribute should use the
static template tag to generate the URL of logo.png, like this:
{% block brand %}<img src="{% static 'logo.png' %}">{% endblock %}
You have already loaded the static template tag library (by adding the load
template tag) in Exercise 5.05, Finding Files Using findstatic.
4. Start the Django dev server if it is not already running and then open
http://127.0.0.1:8000/ in your browser. You should see that the main
Bookr page now has the Bookr logo, as in the following figure:
Chapter 5: Serving Static Files | 25
In this activity, you added a global logo in the base template (base.html). The
logo shows on all non-review pages, but the existing reviews logo still shows on the
reviews pages.
You saw that with the use of namespacing, we can refer to each logo.png by the
directory in which they reside (logo.png for the global logo and reviews/logo.
png for the reviews specific logo). You used the static template tag to generate
the URL to the logo file. This will allow for flexibility when deploying to production
and gives us the option to easily change the static URL by updating the
STATIC_URL setting.
Chapter 6: Forms | 27
Chapter 6: Forms
Activity 6.01: Book Searching
Perform the following steps to complete this activity:
1. Open forms.py in the reviews app. Create a new class called SearchForm
that inherits from forms.Form. The class definition should look like this:
class SearchForm(forms.Form):
This will ensure that the field does not need to be filled in, but if data is entered,
it must be at least three characters long before the form is valid.
Note that this could be a list of lists, a tuple of tuples, or any combination of the
two – this is also valid:
Ideally, though, you would be consistent in your choice of objects; that is, all
tuples or all lists.
class SearchForm(forms.Form):
search = forms.CharField(required=False, min_length=3)
search_in = forms.ChoiceField(required=False,
choices=(("title", "Title"),
("contributor",\
"Contributor")))
28 | Appendix
4. Open the reviews app's views.py file. First, make sure SearchForm
is imported. You will already be importing ExampleForm, so just add
SearchForm to the import line. Take the following line:
from .forms import ExampleForm
def book_search(request):
search_text = request.GET.get("search", "")
form = SearchForm(request.GET)
5. In the book_search view, you will need to add a placeholder books empty set
variable, which will be used in the render context if the form is not valid. We
will also use it to build the results in the next step:
def book_search(request):
# Code from Step 4 truncated
books = set()
Then we should only proceed with a search if the form is valid and some search
text has been entered (remember, the form is valid even if search is empty).
6. We will then check whether the form's cleaned_in value is "title", and
if so, filter the Book objects using the title__icontains argument, to
perform a case-insensitive search. Putting this all together, the book_search
view up to this point should look like this:
def book_search(request):
# Code from Step 4/6 truncated
Change it to this:
contributors = Contributor.objects.filter(
first_names__icontains=search,
last_names__icontains=search)
But in this case, Django will perform this as an AND search, so the search term
would need to be present for the first_names and last_names values of
the same author.
Instead, we will perform two queries and iterate them separately. Every
contributor that matches has each of their Book instances added to the books
set that was created in the previous step:
fname_contributors = \
Contributor.objects.filter(first_names__icontains=search)
lname_contributors = \
Contributor.objects.filter(last_names__icontains=search)
Note that you could also convert the query results to lists by passing them to the
list constructor function and then combining them with the + operator. Then,
just iterate over the single combined list:
contributors = list(Contributor.objects.filter\
(first_names__icontains=search)) + \
list(Contributor.objects.filter\
(last_names__icontains=search))
This is still not ideal, as we make two separate database queries. Instead, you
can combine queries with an OR operator using the | (pipe) character. This will
make just one database query:
contributors = Contributor.objects.filter\
(first_names__icontains=search) |\
Contributor.objects.filter\
(last_names__icontains=search)
def book_search(request):
search_text = request.GET.get("search", "")
form = SearchForm(request.GET)
books = set()
Contributor.objects.filter\
(first_names__icontains=search)
lname_contributors = \
Contributor.objects.filter\
(last_names__icontains=search)
{% extends 'base.html' %}
{% extends 'base.html'%}
{% block title %}
{% if form.is_valid and search_text %}
32 | Appendix
10. After the title's endblock template tag, add the opening content block
template tag:
{% block content %}
Add the <h2> element with the static text Search for Books:
Then, add a <form> element and render the form inside using the
as_p method:
<form>
{{ form.as_p }}
Your <button> element should be similar to the submit buttons you added in
Activity 6.01, Book Searching, with submit as type and btn btn-primary for
class. Its text content should be Search:
<button type="submit" class="btn btn-primary">Search</button>
As in the view, we need to check both of these because the form is valid even if
search_text is blank. Add the <h3> element underneath:
<h3>Search Results for <em>{{ search_text }}</em></h3>
We won't close the if template tag yet as we use the same logic to show or hide
the results, so we will close it in the next step.
Chapter 6: Forms | 33
12. First under <h3>, add an opening <ul> tag, with a list-group class:
<ul class="list-group">
Then, add the for template tag to iterate over the books variable:
<li class="list-group-item">
<span class="text-info">Title:
</span> <a href="{% url 'book_detail' book.pk %}">{{ book }}</a>
Use a <br> element to put the contributors on a new line, then add another
span with the text-info class to show the Contributors leading text:
<br/>
<span class="text-info">Contributors: </span>
</li>
We then will display the message if there are no results, using the empty
template tag, and then close the for template tag:
{% empty %}
<li class="list-group-item">No results found.</li>
{% endfor %}
34 | Appendix
Finally, we'll close all the HTML tags and template tags that we opened from
step 11 until now – first the results </ul>, then the if template tag that checks
whether we have results, and finally the content block:
</ul>
{% endif %}
{% endblock %}
13. Open the project base.html template (not the reviews app's base.html
template). Find the opening <form> tag (there is only one in the file) and set
its action attribute to the URL of the book_search view, using the url
template tag to generate it. After updating this tag, it should look like this:
14. The final steps are to set the name of the search <input> to search. Then,
display the value of search_text in the value attribute. This is done using
standard variable interpolation. Also, add a minlength attribute set to 3.
This will display the search text on the search page in the top-right search field.
On other pages, when there is no search text, the field will be blank.
15. Locate the <title> element (it should be just before the closing </head> tag).
Inside it, add a {% block title %} instance with the text Bookr. Make sure
to include the {% endblock %} closing template tag:
Start the Django dev server if it is not already running. You can visit the main
page at http://127.0.0.1:8000/ and perform a search from there, and
you will be taken to the search results page:
Chapter 6: Forms | 35
We are still taken to the search results page to see the results (Figure 6.35):
Figure 6.35: The search results (for the title) are the same regardless
of which search field was used
Clicking the title link will take you to the book detail page for the book.
36 | Appendix
<div class="container-fluid">
{% block content %}
<h1>Welcome to Bookr!</h1>
{% endblock %}
</div>
Figure 7.32: Create a new file inside the reviews templates directory
There is no need to select the HTML File option, as we do not need the
automatically generated content. Just selecting File is fine.
{% extends 'reviews/base.html' %}
5. In step 13, you will render this template with the context variables form,
instance, and model_type. You can write the code in the template to use
them already though. Add the {% block title %} and {% endblock %}
template tags. Between them, implement the logic to change the text based on
instance being None or not:
{% block title %}
{% if instance %}
Editing {{ model_type }} {{ instance }}
{% else %}
New {{ model_type }}
{% endif %}
{% endblock %}
7. Add an <h2> element after the {% block content %}. You can reuse
the logic from step 5; however, wrap the {{ instance }} display in an
<em> element:
{% endblock %} {# from step 5 #}
{% block content %}
<h2>
{% if instance %}
Editing {{ model_type }} <em>{{ instance }}</em>
{% else %}
New {{ model_type }}
{% endif %}
</h2>
8. After the closing </h2> tag, add a pair of empty <form> elements. The method
attribute should be post:
<form method="post">
</form>
10. Render the form using the {{ form.as_p }} tag. Do this inside the <form>
element, after {% csrf_token %}. The code will look like this:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
11. After the code added in step 10, add a <button> element. Use similar logic as
steps 5 and 7 to change the content of the button based on instance being set.
The full button definition is like this:
</button>
{% endblock %}
Chapter 7: Advanced Form Validation and Model Forms | 39
12. Open reviews/views.py. Update the second argument to the render call
to be "reviews/instance-form.html".
13. Update the context dictionary (the third argument to render). Add the key
instance with the value publisher (the Publisher instance that was
fetched from the database, or None for a creation). Also add the key model_
type, set to the string Publisher. You should retain the key form that was
already there. The key method can be removed. After completing the previous
steps and this one, your render call should look like this:
render(request, "reviews/instance-form.html",
{"form": form, "instance": publisher,
"model_type": "Publisher"})
After completing the activity, you should be able to create and edit Publisher
instances. Create a new Publisher by visiting http://127.0.0.1:8000/
publishers/new/. The page should look like Figure 7.33:
1. Open forms.py inside the reviews app. At the top of the file, make sure you
are importing the Review model. You will already be importing Publisher,
like this:
2. Open views.py inside the reviews app. First, make sure to import the
Review model. Consider the line:
from .models import Book, Contributor, Publisher
Change it to:
Then, create a new view function called review_edit. It should take three
arguments: request (required), book_pk (required), and review_pk
(optional; it should default to None):
Note
Note that since review_pk is unique, then we could fetch the review by
just using review_pk. The reason that we use book_pk as well is to
enforce the URLs so that a user cannot try to edit a review for a book that it
does not belong to. This could get confusing if you could edit any review in
the context of any book.
44 | Appendix
3. Make sure you have imported ReviewForm near the start of the file. You will
already have a line importing PublisherForm:
4. Open forms.py inside the reviews app. At the top of the file, make sure you
are importing the Review model. You will already be importing Publisher,
like this:
5. Open views.py inside the reviews app. First, make sure to import the
Review model. Consider the line:
from .models import Book, Contributor, Publisher
Change it to:
Then, create a new view function called review_edit. It should take three
arguments: request (required), book_pk (required), and review_pk
(optional; it should default to None):
Note
Note that since review_pk is unique, then we could fetch the review by
just using review_pk. The reason that we use book_pk as well is to
enforce the URLs so that a user cannot try to edit a review for a book that it
does not belong to. This could get confusing if you could edit any review in
the context of any book.
46 | Appendix
6. Make sure you have imported ReviewForm near the start of the file. You will
already have a line importing PublisherForm:
Check the validity of the form using the is_valid() method. If it is valid, then
save the form using the save() method. Pass False to save() (this is the
commit argument), so that Review is not yet committed to the database. We
do this because we need to set the book attribute of Review. Remember the
book value is not set in the form, so we set it to Book whose context we are in.
The save() method returns the new or updated Review and we store it in a
variable called updated_review:
You can check whether we are creating or editing a Review based on the value
of the review variable. This is the Review you fetched from the database – or
was set to None. If it is None, then you must be creating a Review (as the view
could not load one to edit). Otherwise, you are editing a Review. So, if review
is not None, then set the date_edited attribute of updated_review to the
current date and time. Make sure you are importing the timezone module
from django.utils (you should put this line near the start of the file):
Then implement the edit/create check (it is an edit if review is not None), and
set the date_edited attribute:
updated_review.save()
8. Now, register the success messages inside the if/else branch created in
step 3. Use the messages.success function and the text should be either
Review for "<book>" created or Review for "<book>" updated.
Regardless of an edit or create, you should return a redirect HTTP response back
to the book_detail URL using the redirect function. You'll also need to
pass book.pk to this function as it is required to generate the URL:
9. Now create the non-POST branch. This is simply instantiating ReviewForm and
passing in the Review instance (again, this might be None):
10. The last line of the function is to call the render function and return the result.
This code will be called if the method is not POST or the form is not valid.
12. Open the reviews app's urls.py file. Add these two mappings
to urlpatterns:
path('books/<int:book_pk>/reviews/new/',
views.review_edit, name='review_create'),\
path('books/<int:book_pk>/reviews/<int:review_pk>/', \
views.review_edit, name='review_edit'),
Note that the order of URL patterns does not matter in this case, so you may
have put the new maps in a different place in the list. This is fine. The complete
urls.py file can be found at http://packt.live/2XSxmoX.
13. Open book_detail.html. Locate the {% endblock %} closing template
tag for the content block. This should be the last line of the file. On the line
before this, add a link using an <a> element. Its href attribute should be set
using the url template tag, with the name of the URL as defined in the map:
{% url 'review_create' book.pk %}. The classes on <a> should be
btn and btn-primary. The full link code is:
<a class="btn btn-primary" href="{% url
'review_create' book.pk %}">Add Review</a>
50 | Appendix
14. Locate the reviews iterator template tag {% for review in reviews
%}, and then its corresponding {% endfor %} closing template tag.
Before this closing template tag is a closing </li>, which closes the list
item that contains all the Book data. You should add the new <a> element
before the closing </li>. Generate the href attribute content using
the url template tag and the name of the URL defined in the map:
{% url 'review_edit' book.pk review.pk %}. The full link code is:
<a href="{% url 'review_edit' book.pk review.pk %}">
Edit Review</a>
Start the Django dev server if it is not already running. The first URL
assumes you already have at least one book in your database. Visit
http://127.0.0.1:8000/books/1/. You should see your new
Add Review button:
Clicking it will take you to the Review creation view for the book. If you try
submitting the form with missing fields, your browser should use its validation
rules to prevent submission (Figure 7.37):
After creating the form, you are taken back to the Book Details page,
and you can see the review you added, along with a link to go back and edit
the Review:
Click the Edit Review link and make some changes to the form. Save it, and
you will be redirected back to the Book Details view again – this time, the
Modified on field should show the current date and time (see Figure 7.39):
In this activity, we used a ModelForm to add a page where users could save a
review. Some custom validation rules on ModelForm ensured that the review's
rating was between 0 and 5. We also created a generic instance form template
that can render different types of ModelForm.
54 | Appendix
1. Open the project's settings.py file. Down the bottom, add these two lines to
set the MEDIA_ROOT and MEDIA_URL:
The file can now be saved and closed. Your file should now look like this:
http://packt.live/3p1wABV.
2. Open urls.py. Above the other imports, add these two lines:
These will import the Django settings and the built-in static view, respectively.
3. Open the reviews app's models.py. To the book model, add the cover field
attribute with this code:
This will add an ImageField that is not required, and stores uploads to the
book_covers subdirectory of the MEDIA_ROOT.
The sample field is added in a similar manner:
This field is also not required, and allows uploads of any file type, storing them to
the book_samples subdirectory of MEDIA_ROOT.
4. Open a terminal and navigate to the Bookr project directory. Run the
makemigrations management command:
python3 manage.py makemigrations
This will generate the migration to add the cover and sample fields to
the Book.
5. Back in PyCharm, open the reviews app's forms.py. You first need to
import the Book model at the top of the file. Consider the following line:
class BookMediaForm(forms.ModelForm):
class Meta:
model = Book
fields = ["cover", "sample"]
You can save and close this file. The complete file should look like this:
http://packt.live/35Qqm0k.
6. Open the reviews app's views.py. Import the image manipulation libraries
(refer to step 4 of Exercise 8.05, Image Uploads using Django Forms to install Pillow,
if you haven't already installed it in your virtual environment):
You'll also need to import the BookMediaForm class you just created. Change
the import line from this:
To this:
At the end of the file, create a view function called book_media, which accepts
two arguments – request and pk:
7. The view will follow a pattern similar to what we have done before. First, fetch
the Book instance with the get_object_or_404 shortcut:
Check if the form is valid, and if so, save the form. Make sure to pass False as
the commit argument to save, since we want to update and resize the image
before saving the data:
if form.is_valid():
book = form.save(False)
Create a reference to the uploaded cover. This is mostly just as a shortcut, so you
don't have to type form.cleaned_data["cover"] all the time:
cover = form.cleaned_data.get("cover")
Next, do the image resize. Use the cover variable as a reference to the
uploaded file. You only want to perform operations on the image if it is
not None:
if cover:
This code is similar to that demonstrated in the section Writing PIL Images to
ImageField. You should refer to that section for a full explanation. Essentially, you
are using PIL to open the uploaded image file, then resizing it so its maximum
dimension is 300 pixels. You then write the data to BytesIO and save it on the
model using a Django ImageFile:
image = Image.open(cover)
image.thumbnail((300, 300))
image_data = BytesIO()
image.save(fp=image_data, \
format=cover.image.format)
image_file = ImageFile(image_data)
book.cover.save(cover.name, image_file)
After doing the updates, save the Book instance. The form might have been
submitted with the clear option set on the cover or sample fields, so this
will clear those fields in that case:
book.save()
58 | Appendix
Then we register the success message and redirect back to the book_detail
view:
This else branch is for the non-POST request case. Instantiate the form with
just the Book instance:
else:
form = BookMediaForm(instance=book)
You will use the is_file_upload flag in the next step. Once you have
completed steps 6 to 8, your book_media function should look like
http://packt.live/3ipgaRe.
Save and close the file. It should look like this: http://packt.live/2XVplzr.
Chapter 8: Media Serving and File Uploads | 59
10. Open the reviews app's urls.py. In urlpatterns, add a mapping like this:
urlpatterns = […\
path('books/<int:pk>/media/', \
views.book_media, name='book_media')
You should now be able to start the Django dev server, if it's not already
running, then view the Book Media page in your browser, for example, at
http://127.0.0.1:8000/books/2/media/. The following figure shows this:
Try uploading some files and checking to see them in the media directory. Check the
sizes of the images that you upload too, and notice they have been resized.
60 | Appendix
1. Open the reviews app's book_detail.html template. After the first <hr>
tag, add an if template tag that checks for the presence of book.cover.
Inside it, put your <img> tag. Its src should be set from book.cover.url.
Add a <br> tag after the <img> tag. The code you add should look like this:
{% if book.cover %}
<img src="{{ book.cover.url }}">
<br>
{% endif %}
{% if book.sample %}
<span class="text-info">Sample: </span>
<span><a href="{{ book.sample.url }}">Download</a></span>
<br>
{% endif %}
3. Scroll to the end of the file where there the Add Review link is located.
Underneath it, add another <a> tag, and generate its href content using
the url template tag. This should use 'book_media' and book.pk as its
arguments. Make sure you include the same classes on the <a> tag as the
existing Add Review link, so that the link displays as a button.
Once again, here is the code that was added in context with the existing code.
The complete file should look like this: http://packt.live/38W5dDK.
Chapter 8: Media Serving and File Uploads | 61
Now you can start the Django dev server if it is not already running. You can view
a Book Details page (for example, http://127.0.0.1:8000/books/1/)
and then click on the new Media link to get to the media page for the Book. After
uploading a cover image or sample file, you will be taken back to the Book Details
page where you will see the cover and a link to the sample file (Figure 8.36):
In this activity, we added the display of a book's cover image and a link to its sample
to the Book Details page, as well as a link from the Book Details page to the
Book Media page. This enhances the look of Bookr by showing an image and gives
readers a more in-depth look into the Book that was reviewed.
62 | Appendix
{% if user.is_authenticated %}
<a class="btn btn-primary" href="{% url 'review_create' book.
pk
%}">Add Review</a>
<a class="btn btn-primary" href="{% url 'book_media' book.pk
%}">Media</a>
{% endif %}
3. In the same template, make the Edit Review link only appear for staff or
the user that wrote the review. The conditional logic for the template block is
very similar to the conditional logic that we used in the review_edit view in
Exercise 9.03, Adding Authentication Decorators to the Views. We can make use of
the is_staff property of the user. We need to compare the user's id with that
of the review's creator to determine that they are the same user:
4. From the templates directory in the main bookr project, modify base.html
so that it displays the currently authenticated user's username to the right of the
search form in the header, linking to the user profile page. Again, we can use the
user.is_authenticated attribute to determine whether the user is logged
in and the user.username attribute for the username. This conditional block
follows the search form in the header:
{% if user.is_authenticated %}
<a class="nav-link" href="/accounts/profile">User:
{{ user.username }}</a>
{% endif %}
Activity 9.02: Using Session Storage for the Book Search Page
The following steps will help you complete this activity:
def book_search(request):
search_text = request.GET.get("search", "")
search_history = request.session.get('search_history', [])
2. If the form has received valid input and a user is authenticated, append the
search option, search_in, and search text, search, to the session's
search_history list:
if form.is_valid() and form.cleaned_data["search"]:
search = form.cleaned_data["search"]
search_in = form.cleaned_data.get("search_in") or "title
if search_in == "title":
…
if request.user.is_authenticated:
search_history.append([search_in, search])
request.session['search_history'] = search_history
64 | Appendix
3. In the case that the form hasn't been filled (for example, when the page is first
visited), render the form with the previously used search option selected, either
Title or Contributor:
elif search_history:
initial = dict(search=search_text,
search_in=search_history[-1][0])
form = SearchForm(initial=initial)
bookr/reviews/views.py
1 def book_search(request):
2 search_text = request.GET.get("search", "")
3 search_history = request.session.get('search_history', [])
4
5 form = SearchForm(request.GET)
6
7 books = set()
8
9 if form.is_valid() and form.cleaned_data["search"]:
You can view the complete code at http://packt.live/3oXpApJ
</style>
</div>
</div>
5. List the search history as a series of links to the book search page. The complete
Search History division will look like this:
<div class="infocell" >
<p>Search History</p>
<p>
{% for search_in, search in request.session.search_history %}
<a href="{% url 'book_search'
%}?search={{search|urlencode}}&search_in={{ search_
in}}" >
{{ search }} ({{ search_in }})</a>
<br>
{% empty %}
No search history found.
{% endfor %}
</p>
</div>
Once you have completed these changes and made some searches, the profile
page will look something like this:
Figure 9.21: The profile page with the Search History infocell
66 | Appendix
If it is not already running, you will need to start the Django server using
this command:
To view the profile page, you will need to log in and click the User link in the top-right
corner of the webpage.
Keeping form preferences in session data is a useful technique for improving user
experience. We naturally expect settings to still be the way that we last saw them
and it is frustrating to find all the values cleared on a complicated form that we are
using repeatedly.
Chapter 10: Advanced Django Admin and Customizations | 67
2. With the admin site application now created, open the admin.py file under the
bookr_admin directory and replace its contents with the following code:
from django.contrib import admin
class BookrAdmin(admin.AdminSite):
site_header = "Bookr Administration Portal"
site_title = "Bookr Administration Portal"
index_title = "Bookr Administration"
In the preceding code sample, you first created a class named BookrAdmin,
which inherits from the AdminSite class provided by Django's admin
module. You have also customized the site properties to make the admin
panel display the text you want. Your admin.py file should look like this:
http://packt.live/3sCgtNg.
3. Now, with the class created, the next step involves making sure that your
application will be recognized as a default admin site application. You should
have already done this in Exercise 10.02, Overriding the Default Admin Site.
Consequently, your apps.py file under the bookr_admin directory should
look like this:
class BookrAdminConfig(AdminConfig):
default_site = 'bookr_admin.admin.BookrAdmin'
If not, replace the contents of the file with the code provided in the preceding
code block.
4. The next step involves making sure that Django uses bookr_admin as the
administration application. To check this, open the settings.py file under
bookr and validate if the following line is present in the INSTALLED_APPS
section. If not, add it at the top of the section:
'bookr_admin.apps.BookrAdminConfig'
INSTALLED_APPS = ['bookr_admin.apps.BookrAdminConfig',\
'django.contrib.auth',\
'django.contrib.contenttypes',\
‹django.contrib.sessions',\
‹django.contrib.messages',\
‹django.contrib.staticfiles',\
'reviews']
Note
The preceding screen will vary depending on the version of the admin.py
file (in the reviews app) you are using. If you're continuing from Chapter 9,
Sessions and Authentication, unlike in Figure 10.9, the Book model should
be visible. If it is, you can skip step 6. Still, it is recommended that you use
that step to cross-validate your code.
6. If there are no models registered to your admin site, the list of books still won't
show up. To now view the list of books in the admin panel, open the admin.py
file under the reviews app directory and validate whether the highlighted code
blocks are present therein. If not, add them:
In the preceding code sample, you imported the Book model from our reviews
app and registered it with the admin site. The Book model contains the records
of all of the books that you have registered in Bookr, along with the details of
their publishers and publication dates.
70 | Appendix
Following this change, if you reload the admin site, you will see that the Book
model starts to show up in the admin panel, and clicking it should show you
something similar to the following screenshot:
7. Now, to make sure that Bookr admins can search books by their name or
the publisher's name in the admin site, you need to create a ModelAdmin
interface for the Book model such that the behavior of the Book model can be
customized inside the admin site. To do this, reopen the admin.py file under
the reviews app directory and add the following class to the file:
class BookAdmin(admin.ModelAdmin):
model = Book
list_display = ('title', 'isbn',\
'get_publisher',\
'publication_date')
search_fields = ['title', 'publisher__name']
The preceding class defines the ModelAdmin interface for our Book model.
Inside this class, we define a couple of properties.
You begin by mentioning the model to which this ModelAdmin interface applies
by specifying the model property to point to our Book model:
model = Book
The next thing to do is to select the fields that are to be shown in the admin site
table when someone clicks the Book model.
Chapter 10: Advanced Django Admin and Customizations | 71
As you can see, you selected the title, isbn, and publication_date fields
from the Book model. But what is this get_publisher field?
The list_display property can take an input that includes the list of
attributes of the Model class, as well as the name of any callable, and print
the value returned by the callable. In this case, get_publisher is a callable
defined inside the BookAdmin class.
The next thing you did was to add a search_fields property and point
it to use the title and publisher__name fields. This adds the capability
in the admin site to search for books either by their title or by the name of
the publisher.
The last thing you did in the class involved defining the get_publisher()
callable. This callable is responsible for returning the name of the publisher from
a book record. This callable is required because Publisher is mapped as a
foreign key inside the Book model, and hence you need to retrieve the name
field of the publisher using the object reference you obtained.
The callable takes a single parameter, obj, which refers to the current object
being parsed (the method is called for every object that we iterate upon), and
returns the name field from the publisher object inside our result:
8. Once the preceding step is complete, you need to make sure that ModelAdmin
is assigned to the Book model for use in our admin site. To do this, edit the
admin.site.register call inside the admin.py file under the reviews app
to make it look like the one shown here:
admin.site.register(Book, BookAdmin)
The admin.py file under the reviews app should now look like this: http://
packt.live/39LjGS1.
9. Now that you are done with making changes, let's run the application by
running the following command:
Once you are on the page, you can click the Book model being shown and it
should redirect you to a page that should resemble the following screenshot:
If you notice the difference between our earlier page and the page we just
loaded, we can see that the page now lists the fields that we selected in our
ModelAdmin class definition, as well as providing us with an option to search
the books (in the form of a search box). We can enter either the name of a book
or its publisher in the search box. Upon selecting a book from the results, we
have an option to either modify or delete it:
If you are getting results that resemble those in Figure 10.12, you have successfully
completed this activity.
Chapter 11: Advanced Templating and Class-Based Views | 73
1. For this activity, you are going to reuse a lot of work that you have done in the
previous chapters regarding building the Bookr app. Instead of recreating
things from scratch, let's focus on those areas that are unique to this activity.
Now, to begin introducing a custom inclusion tag, which will be used to render
the list of books read by our user in the user profile, first create a directory
named templatetags under the reviews app directory and then create a
new file named profile_tags.py inside it.
2. With the file creation complete, the next step is to build the template tag. For
this, open the profile_tags.py file, which you created in step 1, and add the
following code to it:
register = template.Library()
@register.inclusion_tag('book_list.html')
def book_list(username):
"""Render the list of books read by a user.
In this code, you did a few interesting things. Let's walk through them step
by step.
74 | Appendix
You first imported Django's template module, which will be used to set up the
template tags library:
After that, you imported the model, which is used to store all the reviews by the
user. This is done based on the assumption that a review is written by a user
only for the books that they have read, and hence you used this Review model
to derive the books read by a user:
Next, you initialized an instance of the template library. This instance will be
used to register your custom template tags with Django's templating system:
register = template.Library()
Finally, you created the custom template tag named book_list. This tag will
be an inclusion tag because, based on the data it generates, it renders its own
template to show the results of the query for the books read based on the
username of the user.
To render the results, you used the template code located inside the
book_list.html template file:
@register.inclusion_tag('book_list.html')
def book_list(username):
Inside the template tag, you first retrieved all the reviews provided by the user:
reviews = Review.objects.filter(creator__username__contains=username)
Since the user field is mapped to the Review model as a foreign key, you used
the creator attribute (which maps to the User model) from the Review
object and then filtered based on the username.
Once you had the list of reviews, you then created a list of books read by the
user by iterating upon the objects returned from the database:
Once this list was generated, you wrote the following line to return a template
context object. This is nothing but a dictionary of the book_list variable you
can now render inside the template:
In this case, you will be able to access the list of books read by referencing the
books_read variable inside the template.
3. With the template tag created, you can now move on to create the book_list
template (if not already created in the previous exercises). In case you have
created it already, you can skip to step 4. For this, create a new file named
book_list.html under the templates directory of the reviews app. Once
this file is created, add the following code inside it:
<p>Books read</p>
<ul>
{% for book in books_read %}
<li>{{ book }}</li>
{% endfor %}
</ul>
This is a plain HTML template where you are rendering the list of books as
provided by the custom inclusion tag that we built in step 2.
As you can see, you have created a for loop that iterates over the books_read
variable provided by the inclusion tag, and then, for every element inside the
books_read variable, you create a new list item.
4. With the book_list template completed, it's time to integrate the template
tag into the user profile page. To do this, open the profile.html (responsible
for rendering the user profile page) file located under the templates directory
present in the root directory of the project and make the following changes:
{% extends "base.html" %}
You need to load the template tag by adding the following statement:
{% load profile_tags %}
This will load the tags from the profile_tags file. The order of these
statements is important because Django requires that the extends tag comes
first before any other tag in the template file. Failing to do so will result in a
template rendering failure while trying to render the template.
76 | Appendix
5. Now, with the tag loaded, add the book_list tag. For this, replace the code
under the profile.html file inside the templates directory with the
following code:
{% extends "base.html" %}
{% load profile_tags %}
{% block content %}
<ul>
<li>Username: {{ user.username }} </li>
<li>Name: {{ user.first_name }} {{ user.last_name }}</li>
<li>Date Joined: {{ user.date_joined }} </li>
<li>Email: {{ user.email }}</li>
<li>Last Login: {{ user.last_login }}</li>
<li>Groups: {{ groups }}{% if not groups %}None{% endif %} </li>
</ul>
{% book_list user.username %}
{% endblock %}
The code segment marked in bold instructs Django to use the book_list
custom tag to render the list of books read by the user.
6. With this, you have successfully built the custom inclusion tag and integrated
it with the user profile page. To see how your work renders on the browser, run
the following command:
If you are seeing a page that resembles the one shown in Figure 11.11, you have
completed the activity successfully.
78 | Appendix
class ContributionSerializer(serializers.ModelSerializer):
book = BookSerializer()
class Meta:
model = BookContributor
fields = ['book', 'role']
class ContributorSerializer(serializers.ModelSerializer):
bookcontributor_set = ContributionSerializer\
(read_only=True, many=True)
number_contributions = serializers.ReadOnlyField()
Chapter 12: Building a REST API | 79
class Meta:
model = Contributor
fields = ['first_names', 'last_names',\
'email', 'bookcontributor_set',\
'number_contributions']
There are two things to note regarding this new serializer. The first is
that the field bookcontributor_set is going to give us the list of
contributions made by a specific contributor. Another point to note is that
number_contributions uses the method we defined in the Contributor
class. For this to work, we need to set this as a read-only field. This is because it
does not make sense to directly update the number of contributions; instead,
you would add books to the contributor.
class ContributorView(generics.ListAPIView):
queryset = Contributor.objects.all()
serializer_class = ContributorSerializer
path('api/contributors/'),\
path(api_views.ContributorView.as_view()),\
path(name='contributors')
80 | Appendix
You should now be able to run a server and access your API view at
http://0.0.0.0:8000/api/contributors/:
In this activity, you created an API endpoint to provide data to a frontend application.
You used class-based views and model serializers to write clean code.
Chapter 13: Generating CSV, PDF and Other Binary Files | 81
2. To export the reading history of the user, the first step will be to fetch the
reading history for the currently logged-in user. For this, create a new method
named get_books_read() inside the utils.py file under the bookr
directory you created as a part of Exercise 13.06, Visualizing a User's Reading
History on the User Profile Page, as shown in the following code snippet:
def get_books_read(username):
"""Get the list of books read by a user.
In the preceding method, you take in the username of the currently logged-in
user for whom the book reading history needs to be retrieved. Based on the
username, you filter on the Review object, which is used to store the records of
the books reviewed by the user.
Once the objects are filtered, the method creates a list of dictionaries, mapping
the name of the book and the date on which the user finished reading the book,
and returns this list.
82 | Appendix
3. With the helper method created, open the views.py file under the bookr
directory and import the utility method you created in step 2 as well as the
BytesIO library and the XlsxWriter package, as shown in the following
code snippet:
4. Once you have set up the required imports, create a new view function named
reading_history() as shown in the following code snippet:
@login_required
def reading_history(request):
user = request.user.username
books_read = get_books_read(user)
In the preceding code snippet, you first created a view function named
reading_history, which will be used to serve the requests for exporting the
reading history of the currently logged-in user as an XLSX file. This view function
is decorated with the @login_required decorator, which enforces that only
logged-in users can access this view inside the Django application.
Once the username is obtained, you then pass the username to the
get_books_read() method created in step 2 to obtain the reading
history of the user.
Chapter 13: Generating CSV, PDF and Other Binary Files | 83
5. With the reading history obtained, you now need to build an XLSX file with this
reading history. For this use case, there is no need to create a physical XLSX file
on disk; you can use an in-memory file. To create this in-memory XLSX file, add
the following code snippet to the reading_history function:
temp_file = BytesIO()
workbook = xlsxwriter.Workbook(temp_file)
worksheet = workbook.add_worksheet()
In the preceding code snippet, you first created an in-memory binary file using
the BytesIO() class. The object returned by the BytesIO() class represents
an in-memory file and supports all the operations that you would do on a regular
binary file in Python.
With the file object in place, you passed this file object to the Workbook class of
the xlsxwriter package to create a new workbook that will store the reading
history of the user.
Once the workbook was created, you added a new worksheet to the Workbook
object, which helps in organizing your data into a row-and-column format.
6. With the worksheet created, now you can parse the data you obtained as a
result of the get_books_read() method call done in step 4. For this, add the
following code snippet to your reading_history() function:
data = []
for book_read in books_read:
data.append([book_read['title'], \
str(book_read['completed_on'])])
workbook.close()
In the preceding code snippet, you first created an empty list object to store the
data to be written to the XLSX file. The object is populated by iterating over the
books_read list of dictionaries and formatting it such that it represents a list of
lists, where every element in the sub-list is a value for a specific column.
84 | Appendix
Once this list of lists was created, you then iterated over it to write the values for
the individual columns present in a specific row.
Once all the values were written, you then went ahead and closed the workbook
to make sure all the data was written correctly, and no data corruption occurred.
7. With the data now present in the in-memory binary file, you need to read that
data and send it as an HTTP response to the user. For this, add the following
code snippet to the reading_history() view function:
data_to_download = temp_file.getvalue()
response = HttpResponse(content_type='application/vnd.ms-excel')
response['Content-Disposition'] = 'attachment; filename=reading_
history.xlsx'
response.write(data_to_download)
return response
In the preceding code snippet, you first retrieved the data stored inside the
in-memory binary file by using the getvalue() method of the in-memory
file object.
You then prepared an HTTP response with the ms-excel content type and
indicated that this response should be treated as a downloadable file by setting
the Content-Disposition header.
Once the header was set up, you then wrote the content of the in-memory file
to the response object and then returned it from the view function, essentially
resulting in a file download starting for the user.
8. With the view function created, the last step is to map this view function to a
URL. For this, open the urls.py file under the bookr application directory and
add the bold code in the following code snippet to the file:
import books.views
urlpatterns = [...,
path('accounts/profile/reading_history'),\
(bookr.views.reading_history),\
(name='reading_history'),\
...]
Chapter 13: Generating CSV, PDF and Other Binary Files | 85
If you see a file downloading, similar to what you see in Figure 13.10, you have
successfully completed the activity.
Note
In case you don't have any reading history associated with your user
account, the downloaded file will be a blank Excel file.
86 | Appendix
mkdir tests
Once this is done, you need to make sure that Django recognizes this directory
as a module and not as a regular directory. To make this happen, create an
empty file with the name __init__.py under the tests directory you created
just now by running the following command:
touch __init__.py
Once this is done, you can remove the tests.py file from the reviews
directory since we are now moving towards the pattern of using modularized
test cases.
2. Once the tests directory is created, it is time to write test cases for the
components, starting with the writing of test cases for the models inside the
reviews application. To do this, create a new file named test_models.
py under the tests directory you created in step 1 by running the
following command:
touch test_models.py
test_models.py
In the preceding code snippet, you have written a couple of test cases for the
models that you have created for the reviews application.
You started by importing Django's TestCase class and the models you are
going to test in this exercise:
With the required classes imported, you defined the test cases. To test the
Publisher model, you first created a new class, TestPublisherModel,
which inherits from Django's TestCase class. Inside this class, you added the
following test, which checks whether the Publisher model objects are being
created successfully or not:
def test_create_publisher(self):
publisher = Publisher.objects.create\
(name='Packt',website='www.packt.com',\
email='[email protected]')
self.assertIsInstance(publisher, Publisher)
touch test_views.py
Note
On Windows, you can create this file using Windows Explorer.
88 | Appendix
Once the file is created, you are going to write the test cases inside it. For testing,
you are going to use Django's RequestFactory to test the views. Add the
following code to your test_views.py file:
class TestIndexView(TestCase):
def setUp(self):
self.factory = RequestFactory()
def test_index_view(self):
request = self.factory.get('/index')
request.session = {}
response = index(request)
self.assertEquals(response.status_code, 200)
Let us examine the code in detail. In the first two lines, you imported
the required classes for writing our test cases, including TestCase and
RequestFactory. Once the base classes were imported, you then imported
the index method from the reviews.views module to be tested. Next up,
you created TestCase by creating a new class named TestIndexView,
which will encapsulate the test cases for the view functions. Inside this
TestIndexView, you added the setUp() method, which will help you create
a request factory instance for use in every test case:
def setUp(self):
self.factory = RequestFactory()
With the setUp() method defined, you wrote a test case for the index view.
Inside this test case, you first created a request object as if you were trying to
make an HTTP GET call to the '/index' endpoint:
request = self.factory.get('/index')
Chapter 14: Testing | 89
Once the request object is available, you set the session object to point to
an empty dictionary since you currently do not have a session available:
request.session = {}
With the session object now pointing to an empty dictionary, you could now
test the index view function by passing the request object to it as follows:
response = index(request)
Once the response is generated, you validate the response by checking the
status code of the response using the assertEquals method:
self.assertEquals(response.status_code, 200)
4. With the test cases now in place, run and check whether they pass successfully.
To do this, run the following command:
Once the command finishes, you should expect to see the following
output generated:
In the preceding output, you can see that all the test cases that you implemented
have passed successfully, validating that the components work in the way they
are expected to.
90 | Appendix
class InstanceForm(forms.ModelForm):
It should be the first class defined in the file as other classes will be inheriting
from it.
2. At the start of the file, you should already be importing the FormHelper class
from crispy_forms.helper:
self.helper = FormHelper()
This is the helper object that django-crispy-forms will query to find the
form attributes. Since this form is to be submitted with POST, which is the
default method of FormHelper, we don't need to set any attributes.
if kwargs.get("instance"):
button_title = "Save"
Chapter 15: Django Third Party Libraries | 91
else:
button_title = "Create"
self.helper.add_input(Submit("", button_title))
class PublisherForm(models.ModelForm):
Change it to this:
class PublisherForm(InstanceForm):
class ReviewForm(models.ModelForm):
Change it to this:
class ReviewForm(InstanceForm):
class BookMediaForm(models.ModelForm):
Change it to this:
class BookMediaForm(InstanceForm):
No other lines need to be changed. You can then save and close forms.py.
92 | Appendix
{% load crispy_forms_tags %}
Then scroll to the bottom of the file where the <form> tag is located.
You can delete the entire form code, including the opening <form> and
closing </form> tags. Then replace it with:
{% crispy form %}
This will render the form, including the <form> tags, CSRF token, and submit
button. You can save and close instance-form.html.
You can now start the Django dev server and try using the Publisher,
Review, and Book Media forms. You should see they look nice since they are
using the proper Bootstrap classes. The submit buttons should automatically
update based on the create or edit mode you are in for that form. Verify that
you can update book media, which will indicate that the enctype is being set
properly on the form:
The New Review form and the Book Media page should appear as follows:
Here, the state attribute is being created with the review that will be passed
in using the review attribute.
render () {
const review = this.state.review;
Note that it is not necessary to create a review alias variable; the data
could be accessed through the state throughout the HTML. For example,
{ review.book } could be replaced with { this.state.review.book }.
The completed class should now look like this: http://packt.live/2XSvJaN.
This reads the currentUrl from the one provided in props and other values
are set as defaults.
fetchReviews() {
if (this.state.loading)
return;
this.setState( {loading: true} );
}
98 | Appendix
7. After setting the state in the previous step, add the call to the fetch function:
fetch(this.state.currentUrl, {
method: 'GET',
headers: {
Accept: 'application/json'
}
}).then((response) => {
return response.json()
}).then((data) => {
this.setState({
loading: false,
reviews: data.results,
nextUrl: data.next,
previousUrl: data.previous
})
})
This will fetch the currentUrl, and then update nextUrl and previousUrl
to the values of next and previous that the Django server generates. We also
set our reviews to the data.results we retrieved and set loading back to
false since the load is complete.
8. The componentDidMount method simply calls the fetchReviews method:
componentDidMount() {
this.fetchReviews()
}
this.state.currentUrl = this.state.nextUrl;
this.fetchReviews();
}
Chapter 16: Using a Frontend JavaScript Library with Django | 99
loadPrevious() {
if (this.state.previousUrl == null)
return;
this.state.currentUrl = this.state.previousUrl;
this.fetchReviews();
}
11. The render method is quite long. We will look at it in chunks. First, it should
return some loading text if state.loading is true:
if (this.state.loading) {
return <h5>Loading...</h5>;
}
12. Still inside the render method, we'll now create the previous and next
buttons and assign them to the previousButton and nextButton variables
respectively. Their onClick action is set to trigger the loadPrevious or
loadNext method. We can disable them by checking if previousUrl or
nextUrl are null. Here is the code:
const previousButton = <button
className="btn btn-secondary"
onClick={ () => { this.loadPrevious() } }
disabled={ this.state.previousUrl == null }>
Previous
</button>;
Note that nextButton has the additional class float-right, which displays
it on the right of the page.
100 | Appendix
13. Next, we define the code that displays a list of ReviewDisplay elements:
if (this.state.reviews.length === 0) {
reviewItems = <h5>No reviews to display.</h5>
} else {
reviewItems = this.state.reviews.map((review) => {
return <ReviewDisplay key={review.pk} review={review}/>
})
}
If the length of the reviews array is 0, then the content to display is set
to <h5>No reviews to display.</h5>. Otherwise, we iterate
over the reviews array using its map method. This will build an array
of ReviewDisplay elements. We give each of these a unique key of
review.pk, and also pass in the review itself as a property.
14. Finally, all the content we built is bundled together inside some <div>
instances and returned, as per the example given:
return <div>
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{ reviewItems }
</div>
<div>
{previousButton}
{nextButton}
</div>
</div>;
15. Open the project's base.html (not the reviews-specific one). Between the
{% block content %} and {% endblock %} template tags, after the
closing {% endif %} containing the previously viewed book history, add the
Recent Reviews heading:
<h4>Recent Reviews</h4>
Chapter 16: Using a Frontend JavaScript Library with Django | 101
16. Underneath the <h4> added in the previous step, create a <div> element, with
a unique id attribute:
<div id="recent_reviews"></div>
17. Under the <div> that was just added, but still before the {% endblock %},
add your <script> elements:
18. Finally, add another <script> element to render the React component:
<script type="text/babel">
ReactDOM.render(<RecentReviews url="{% url 'api:review-list' %}?
limit=6" />,
document.getElementById('recent_reviews')
);
</script>
The url that is passed into the component's prop is generated by Django's
url template tag. We manually append the ?limit=6 argument to limit the
number of reviews that are returned. In the document.getElementById call,
make sure you use the same ID string that you gave to your <div> in step 16.
102 | Appendix
2. To update the list of available packages, use the apt update command. It
must be run with sudo, and you may have to enter your password. Your output
will be similar to the following:
Remember that this is just updating the list of available packages. To upgrade
packages to the latest version(s), you would use the apt upgrade command.
104 | Appendix
3. Install glances with the sudo apt install glances command. Since
you've just run sudo recently, you shouldn't have to enter your password again.
It will show you which packages it needs to install (quite a few), and then you'll
have to just press Enter to continue. You'll see an output as in Figure 17.63:
After it has been installed, you will return to the command prompt.
Chapter 17: Deployment of a Django Application (Part 1 – Server Setup) | 105
After you've had a look at its interface, quit glances by pressing Ctrl + C or Esc.
106 | Appendix
5. Run man glances to view the glances manual page (or man page for short).
When it starts, it looks as in Figure 17.65:
You can navigate up and down through the file by using the cursor keys, and
type q to quit. You can also search inside the file by typing /, then the search
term, then press Enter to perform the search. Matches will be highlighted,
and you can type n to move forward through the results or Shift + N to
move backward:
This can make it easy to find what you're looking for, by searching for disable
and theme. Figure 17.67 shows the section of the manual that explains how to
disable the memory swap module, using --disable-memswap:
Figure 17.67: Disabling the memory swap module with the –disable-memswap argument
Chapter 17: Deployment of a Django Application (Part 1 – Server Setup) | 109
Figure 17.68 shows the section for setting the white theme, using the
--theme-white argument:
You should see glances display now without the swap memory information,
and it should be more readable:
Figure 17.69: glances with a white theme and without the memory swap module shown
Once again, you can quit glances and disconnect and shut down your virtual
machine, if you've finished using it.
Chapter18:DeploymentofaDjangoApplication(Part2–ConfigurationandCodeDeployment)|111
bookr@bookr:/home/ben$ cd
bookr@bookr:~$
4. Activate the virtual environment in the usual way, by using source to load the
activate script:
bookr@bookr:~$ source bookr-venv/bin/activate
(bookr-venv) bookr@bookr:~$
Notice that your command prompt changes to include the virtual environment
name (bookr-venv) after it has been activated.
5. The settings need to be exported into the current environment using the
export command we saw in Exercise 18.04, Python Virtual Environment and
Django Setup, step 9:
First, you should cd into the bookr directory before running it, otherwise, the
path to production.conf will not be correct:
6. Now that the virtual environment is activated and the configuration is loaded,
we can run Django management commands in the usual manner. The loadcsv
command should be run with the path to the CSV file containing the test data:
Now load up the Bookr hosted on your server in a web browser. You should see
it populated with test data:
Figure 18.14 shows Bookr running on a virtual machine with some test data. Note that,
depending on the CSV import, your data may be ordered differently.
114 | Appendix
You should also be able to log in to the Django admin section with the credentials you
created in step 7.
In this activity, you saw the steps needed to log in to your server and execute Django
management commands. Other commands are executed in the same way, and
you can refer to these steps if you need to run other commands, such as database
migrations or running collectstatic again.
background-color: #f0f0f0;
<h2>Welcome to Bookr!</h2>
3. Make sure your virtual machine has been booted up, then connect to it using
FileZilla. Upload main.css and index.html. You can just put them in your
home directory without any directory structure as you will be moving them in the
next step:
Chapter18:DeploymentofaDjangoApplication(Part2–ConfigurationandCodeDeployment)|115
4. SSH into your virtual machine and use mv (with sudo) to move the files to the
correct places:
Then update their ownership – this can be done with one command:
cd
source bookr-venv/bin/activate
cd bookr
/var/www/bookr/static
6. Switch back to your normal user with the exit command. Then restart
gunicorn-bookr using systemctl:
sudo systemctl restart gunicorn-bookr
Load Bookr hosted on your virtual machine using your web browser. You should
see the updated color and welcome messages as follows: