# Fluent in Django: Improving your application

>This is the second part of [Fluent in Django series](https://girlthatlovestocode.com/series/django).
>We're going to upgrade the code we created in [Part 1](https://girlthatlovestocode.com/fluent-in-django-first-steps).
>You can find the finished code from the first part [here](https://gitlab.com/GirlThatLovesToCode/complete-django-tutorial-part-1).

## Prerequisites

- Python
- Basics of Django (Creating a project, Model, View, Templates, Forms)
- Basic understanding of HTML, CSS, and Bootstrap
- Finished project from [Part 1](https://gitlab.com/GirlThatLovesToCode/complete-django-tutorial-part-1)

If you don't want to do go through the first part, you'll need to get the project we created in Part 1 running.

>How to get the poject from Part 1 running:
>   - clone the [repo](https://gitlab.com/GirlThatLovesToCode/complete-django-tutorial-part-1)
>   - create a virtual enviorenment
>   - run `pip install -r requirements.txt`
>   - run `python manage.py migrate`
>   - run `python manage.py createsuperuser`
>   - start the server with `python manage.py runserver`
>   - go to http://127.0.0.1:8000/admin/ and create 4 books - two with a past date and two in the future.
>
> If you're struggling with getting the project running, I encourage you to start with [Part 1](https://girlthatlovestocode.com/fluent-in-django-first-steps).

## What are you going to learn

- You'll create an additional field for your form that will allow users to upload their photo when posting their opinion.
- Your BookCLub page could look a little better - that's why you're going to add some custom CSS (don't worry, the emphasis is not on CSS but on connecting it with Django).
- Why do it by yourself, if someone else has already done it - you'll use two Django packages to make your page better.
- You'll learn how to tailor Django admin to your needs.

## Uploading an image

To enable users to be more personal, you'll add a possibility to upload a photo of them reading the book they're commenting on.
To do this, you'll need a directory that will store the uploaded files, and connect it with Django.

Create a directory called `media` in the root directory.
Open `settings.py` and add `MEDIA_ROOT` and `MEDIA_URL`.

```python
#settings.py

MEDIA_ROOT = BASE_DIR.joinpath('media/')
MEDIA_URL = '/media/'
```

`MEDIA_ROOT` is the absolute path to the directory where the files uploaded by users will be held.
`MEDIA_URL` is the url from where the images will be served.

To be able to serve user-uploaded media files from MEDIA_ROOT, you need to use `django.conf.urls.static` in your `urls.py` file.

```python
# tutorial/urls.py
from django.conf.urls.static import static
from tutorial import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('bookclub.urls')),
]

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

This method is inefficient and insecure and is only suitable for development. 
That's why we only add the static url pattern if `DEBUG`  is set to `TRUE` in your `settings.py`.
For the production, you'll need [to do things differently](https://docs.djangoproject.com/en/3.1/howto/static-files/deployment/).
We'll cover pushing to production in the latter part of the series.

Django has two model fields that allow user uploads - `FileField` and `ImageField`.
`ImageField` is a specialized version of `FileField` that uses Pillow to confirm that a file is an image.
Pillow is an image processing library for Python, build on the unmaintained *Python Imaging Library* (*PIL*).

Install Pillow with:
```sh
(env)$ python -m pip install Pillow
```

Add image field to you model:
```python
class Discussion(models.Model):
    book = models.ForeignKey('bookclub.Book', related_name='discussion', on_delete=models.CASCADE)
    author = models.ForeignKey('auth.User', related_name='records', on_delete=models.CASCADE)
    opinion = models.TextField()
    image = models.ImageField(upload_to='images', blank=True)
```

The image field has to contain the information where the images will get stored (`media/images`). We also added a `blank=True`, which means that the image is not a required field.

Migrate the changes:
```sh
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
```

You need to add an enctype attribute to the form if you want the file to be uploaded correctly:
```html
<!--bookclub/templates/book_detail.html-->

<form method="post" enctype="multipart/form-data"> <!--this is new-->
    {% csrf_token %}
    {{ discussion_form.as_table }}
    <br>
    <button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>
```

[enctype](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype) has to be set to `multipart/form-data` to allow the `<input>`element to upload file data.

To display the image in the discussion card, you'll have to rearrange the html tags and bootstrap classes, but the basic idea stays the same:
```html
<!--bookclub/templates/book_detail.html-->

<!--...-->
<div class="container"> <!-- this is also new -->
            <div class="row"> <!-- this is also new -->
                {% for opinion in book.discussion.all %}

                    <div class="card mb-3" style="width: 500px;">
                        <div class="row no-gutters">
                            <div class="col-md-4">
                                {% if opinion.image %}
                                    <img src="{{ opinion.image.url }}" class="card-img">
                                {% else %}
                                    <img src="http://www.jennybeaumont.com/wp-content/uploads/2015/03/placeholder.gif" class="card-img">
                                {% endif %}
                            </div>
                            <div class="col-md-8">
                                <div class="card-body">
                                    <h5 class="card-title">{{ opinion.author }}</h5>
                                    <p class="card-text">{{ opinion.opinion }}</p>
                                </div>
                            </div>
                        </div>
                    </div>
                {% empty %}
                    <div class="alert alert-secondary mt-4" role="alert">
                        There is no opinions yet for this book.
                    </div>
                {% endfor %}
            </div> <!-- this is also new -->
        </div> <!-- this is also new -->
<!--...-->
```

You can access the uploaded images url with `{{ opinion.image.url }}`. Because the image is optional (`blank=True` in our model), you need to check if the image exists with `{% if opinion.image %}`.
Otherwise, you provide a placeholder.

## Adding custom CSS

Things look fine but they could look much better.
Since this is not a CSS tutorial, we're going to change just a few small things, just enough for you to learn how to add static files.

At the bottom of the `settings.py` file, you can see the static url defined: `STATIC_URL = '/static/'`.
This tells Django the first part of url your static files can be found.

> As with `MEDIA_URL`, this is not suitable for production.

Create a new folder named `static` inside the `bookclub` folder.
```
BookClub
└─── bookclub
     └─── static
```
Django will automatically load static files from any folder named `static`. 


> `MEDIA_URL` and `STATIC_URL` **must have** different values. So do `MEDIA_ROOT` and `STATIC_ROOT`.

To avoid the collision of two files with the same name inside different apps, organize it the same way as templates - inside the `static` folder create a folder with the same name as your app.

Create a static folder inside the `bookclub` app. Inside it, create another folder, named `bookclub`. 
To make it even more organized, create another folder, called `css`. 
You want to keep your code neatly organized, so you'll be able to navigate through it even when the project grows big.
Inside the `css` folder, create a file `style.css`.

So this is what you have so far:

```
BookClub
└─── bookclub
     └─── static
         └─── bookclub
              └─── css
                   └─── style.css
```

Now open the `style.css` and add the following code:
```css
/*bookclub.css*/

body {
    padding-bottom: 50px;
    background-color: whitesmoke;
}

.card.mb-3:nth-child(even) {
    margin-left: 20px
}

.card-img {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
}

form {
    margin-bottom: 50px;
}
```

This doesn't work yet - you have to connect the `css` file with the `html`.
Open `base.html` and add the tag to load static filed and link to your css file inside the `<head>`.

```html
{% load static %} <!--this is new -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>BookClub</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<link rel="stylesheet" href="{% static '/bookclub/css/bookclub.css' %}"> <!--this is new -->
</head>
```
You're using the `static` template tag to build the url for the given relative path (`{% static '/css/bookclub.css' %}`), but before using it, you need to import it (`{% load static %}`).

This looks a little better:

![with_css.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801535766/y5U7cSMET.png)

> If you like CSS, feel free to add a little more pizzaz to your `BookClub`.


## Adding Django packages

Why write it by yourself if someone else already put a lot of time to create the thing you need?
There are quite some packages for Django that can make your life easier.
You're going to use two of them:
- [Crispy forms](https://django-crispy-forms.readthedocs.io/en/latest/) to make the opinion form prettier
- [CKEditor](https://github.com/django-ckeditor/django-ckeditor) so the  book description and opinions can be more than just a plain text

> When including Django packages, be careful. Always check on GitHub that the package is well maintained (good: updated in last 3 months / bare minimum: updated in the last year) and that users are content with it (widely used package: at least 1000 star / marginal package: 250 stars).
> When it comes to the packages, be like Coco Chanel - less is more.

### Crispy forms

`django-crispy-forms` lets you control the rendering behavior of your Django forms in a very elegant and DRY way.
You can make your forms prettier by adding just two simple things to the template.

Install the crispy forms in your shell:
```sh
(env)$ pip install django-crispy-forms
```

Add it to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
    # ...
    'crispy_forms',
]
```

> Each Django-related package you install, you need to add to `INSTALLED_APPS` if you want to use it.

Open `book_detail.html` and switch the `{{ discussion_form.as_table }}` part for `{{ discussion_form|crispy }}`.
For this to work, you need to load `crispy_forms_tags` prior to that, so the code looks like that:

```html
<!--book_detail.html-->
{% if discussion_open %}

{% load crispy_forms_tags %} <!-- this is new -->

<b>What are your thoughts on the book?</b>
{% if user.is_authenticated %}
    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ discussion_form|crispy }} <!-- this is changed -->
        <br>
        <button type="submit" class="btn btn-primary mt-2">Submit</button>
    </form>
{% else %}
    <div class="alert alert-warning">Only registered users are allowed to post their opinion.</div>
{% endif %}

```

Refresh the page and the form should look way prettier:

![crispy_form.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801556169/ZazakPLHR.png)

### CKEditor

Django CKEditor is a package, that adds a WYSIWYG (What-You-See-Is-What-You-Get) editor to your admin fields and your forms.

Install the CKEditor in your shell:
```sh
(env)$ pip install django-ckeditor
```

Add it to `INSTALLED_APPS`:
```python
INSTALLED_APPS = [
    # ...
    'crispy_forms',
    'ckeditor',
]
```

Now you have to copy static CKEditor required media resources into the directory given by the STATIC_ROOT setting.
You don't have `STATIC_ROOT` set yet.

In the bottom of the `settings.py`, near the `STATIC_URL`, add
```python
#settings.py

STATIC_ROOT = BASE_DIR.joinpath('staticfiles')
```

Now run the following command in your shell:
```sh
(env)$ python manage.py collectstatic
```
Check the folders in your project. There's an additional folder now, called `staticfiles` that contains all the static files that exist in your project.

Open `models.py` and change the `description` field in the `Book` class:
```python
# models.py 
from ckeditor.fields import RichTextField

class Book(models.Model):
# ...
description = RichTextField()
# ...
```

The description is now a `RichTextField`. `RichTextField` behaves like a standard `TextField`, except it includes possibilities to edit the text.
Don't forget to import it.
Migrate the changes with `python manage.py makemigrations` and `python manage.py migrate`.

Open one of the books at `http://127.0.0.1:8000/admin/bookclub/book/` and check it out.

![rich_field.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801585473/00fl3byGb.png)

Play around a little and edit the text.
If you want the rich text to display properly, you need to add one more thing to the template:

```html
<!--book_detail.html-->

<div class="card">
    <div class="card-body">
        {{ book.description|safe}} <!-- this is changed -->

        {% if not discussion_open %}
            <div class="alert alert-primary mt-4" role="alert">
                Read this book by: {{book.read_by}}
            </div>
        {% endif %}
    </div>
</div>
```
`|safe` Marks a string as not requiring further HTML escaping prior to output, thus making it render correctly.

Open the book you edited in the browser and now your book description has a little more style:

![rich_field_rendered.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801601925/xWa1f_dIG.png)

Users are still not able to use the CKEditor in the form.

Change the `TextField()` to a `RichTextField()` for `opinion` filed on the `Discussion` class, as you did for the `Book`.

```python
# models.py 
from ckeditor.fields import RichTextField

class Discussion(models.Model):
    book = models.ForeignKey('bookclub.Book', related_name='discussion', on_delete=models.CASCADE)
    author = models.ForeignKey('auth.User', related_name='records', on_delete=models.CASCADE)
    opinion = RichTextField() # this is changed
    image = models.ImageField(upload_to='images/', blank=True)
```

Migrate the change with `python manage.py makemigrations` and `python manage.py migrate`.
For the form to show the correct field, you just need to make sure all form media is present.
Add `{{ discussion_form.media }}` to the `book_detail.html` prior to the `{{ discussion_form|crispy }}`.

```html
<!--book_detail.html-->

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ discussion_form.media }} <!-- this is changed -->
    {{ discussion_form|crispy }}
    <br>
    <button type="submit" class="btn btn-primary mt-2">Submit</button>
</form>
```

Refresh the page and there it is.

![ck_form.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801618278/ctB_8RLen.png)

You have to add `|safe` for the opinions in the `book_detail.html` to render correctly. 

```html
<!--book_detail.html-->
{% for opinion in book.discussion.all %}
    <div class="card mb-3" style="width: 500px;">
        <div class="row no-gutters">
            <div class="col-md-4">
                {% if opinion.image %}
                    <img src="{{ opinion.image.url }}" class="card-img">
                {% else %}
                    <img src="http://www.jennybeaumont.com/wp-content/uploads/2015/03/placeholder.gif" class="card-img">
                {% endif %}
            </div>
            <div class="col-md-8">
                <div class="card-body">
                    <h5 class="card-title">{{ opinion.author }}</h5>
                    <p class="card-text">{{ opinion.opinion|safe }}</p> <!-- this is changed -->
                </div>
            </div>
        </div>
    </div>
{% empty %}
    <div class="alert alert-secondary mt-4" role="alert">
        There is no opinions yet for this book.
    </div>
{% endfor %}
```

** Don't forget the `all_books.html`.**
If you go to http://127.0.0.1:8000/, you'll see that text has escaped HTML. As on the book detail page, you need to add `|safe` - in three places.

For the showcased book:

```html
<!--all_books.html-->
<div class="jumbotron">
    <p class="lead">What were your thoughts on the latest book?</p>
    <h3>{{previous_books.0.book}}</h3>
    <hr class="my-4">
    <p>{{previous_books.0.description|truncatewords:150|safe}}</p> <!-- |safe added -->
    <a class="btn btn-primary btn-lg" href="{% url 'book_detail' pk=previous_books.0.pk %}" role="button">See the discussion</a>
</div>
```

For the upcoming books:

```html
<!--all_books.html-->
{% for upcoming_book in upcoming_books %}
    <a href="{% url 'book_detail' pk=upcoming_book.pk %}" class="list-group-item list-group-item-action">
        <div class="d-flex w-100 justify-content-between">
            <h5 class="mb-1">
                {{ upcoming_book.book }}
            </h5>
            <small class="text-muted">
                Read by: {{ upcoming_book.read_by }}
            </small>
        </div>
        <p class="mb-1">
            {{ upcoming_book.description|safe }} <!-- |safe added -->
        </p>
    </a>
{% endfor %}
```

And for the previous books:

```html
<!--all_books.html-->

{% for previous_book in previous_books %}
    <a href="{% url 'book_detail' pk=previous_book.pk %}" class="list-group-item list-group-item-action">
        <div class="d-flex w-100 justify-content-between">
            <h5 class="mb-1">
                {{ previous_book.book }}
            </h5>
        </div>
        <p class="mb-1 center">
            {{ previous_book.description|safe }} <!-- |safe added -->
        </p>
    </a>
{% endfor %}
```

Refresh and everything look good.

![all_books_ck.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801638600/ffO1DyTkT.png)

## Pagination

Currently, the user can only see the last 3 previous books and the first three upcoming books. But what if they want to see other books? Maybe they're looking for an idea of what to read next.
Provide them with an option to browse through all the books.

Create an url `'book-list'`:

```python
# bookclub/urls.py

urlpatterns = [
    path('', views.all_books, name="all_books"),
    path('book-list', views.book_list, name="book_list"), # this is new
    path('book/<int:pk>/', views.book_detail, name="book_detail"),
]
```

Now for the view:
```python
# bookclub/views.py

from django.core.paginator import Paginator


def book_list(request):
    books = Book.objects.all()
    paginator = Paginator(books, 5)

    page_number = request.GET.get('page')
    page_books = paginator.get_page(page_number)

    return render(request, 'bookclub/book_list.html', {'page_books': page_books})
```

- You retrieve all the `Book` instances, as you did at the beginning of Part 1.
- You break them into groups of five with `Paginator(books, 5)`.
- You get the info about on which page the user is from the url with `request.GET.get('page')`.
- You retrieve the books on this page from the `Paginator` - `paginator.get_page(page_number)`.
- You render only the books from that page to the template

Just dumping them in the template is [quite easy](https://docs.djangoproject.com/en/3.1/topics/pagination/#paginating-a-listview), but combining them with [bootstrap](https://getbootstrap.com/docs/4.0/components/pagination/) can look complicated.
Inside the `templates/bookclub` directory, create a `book_list.html`.

```html
<!--book_list.html-->

{% extends 'bookclub/base.html' %}

{% block title %}
    All the books
{% endblock %}}

{% block content %}
    <div class="list-group">
        {% for book in page_books %}
        <a href="{% url 'book_detail' pk=book.pk %}" class="list-group-item list-group-item-action mt-4">
            <div class="d-flex w-100 justify-content-between">
                <h5 class="mb-1">
                    {{ book.book }}
                </h5>
            </div>
            <p class="mb-1">
                {{ book.description|safe }}
            </p>
        </a>
        {% endfor %}
    </div>
    
    <nav class="mt-3">
        <ul class="pagination justify-content-center">
            {% for page in page_books.paginator.page_range %}
            <li class="page-item {% if page == page_books.number %}active{% endif %}">
                <a class="page-link" href="?page={{ page }}">
                    {{ page }}
                </a>
            </li>
            {% endfor %}
        </ul>
    </nav>
    
    <a href="{% url 'all_books'%}" class="float-right"> Home</a>
{% endblock %}
```

The first part is more-or-less copy-pasted from the `all_books` template, only you're looping through `page_books`.

As for the part inside the `<nav>`:

All the html is taken from [Bootstrap](https://getbootstrap.com/docs/4.0/components/pagination/#disabled-and-active-states).
The home link takes the user to the main page.

With Django, you loop through this:
```html
page_books.paginator.page_range
```

`page_books` is the queryset you passed from the `view`.
The `paginator` in the template is not the same as the one you created in the `view`. They have the same name, but you could name it differently in the `book_list` and the `paginator` in the template would still work (I encourage you to try that, it's always good to see it with your own eyes).
`page_range` is a range iterator of page numbers (`[1, 2, 3, 4]`), so the `{{ page }}` each time shows one of the numbers on the list.
If you'd just like to get the total number of pages, you can use `num_pages`.

The only other Django thing her is:
```html
{% if page == page_books.number %}active{% endif %}
```
With this, you check if the current page iteration (`page`) is the same as the current page you're on (`page_books.number`).
If it is, the Bootstrap class `active` is added.

![pagination.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801658443/heLSttwBT.png)

Add the link to this page at the bottom of the home page (inside the content block), so you can navigate to the list easily:

```html
<!--all_books.html-->

<a href="{% url 'book_list' %}" class="btn btn-primary mt-3">Check all books</a>
```

## Changing the admin

Admin panel can be very useful. But if you have to open the `Book object (3)` for the seventh time to check if that's the book you're looking for is not user-friendly.
This is Django, of course, there's a better way.

Django's admin has many hooks for customization. Here you'll use only a few, but if you need something else, you have the documentation for it [here](https://docs.djangoproject.com/en/3.1/ref/contrib/admin/).

Open `admin.py` and create a `ModelAdmin` with all the customization:

```python
# ...
class BookAdmin(admin.ModelAdmin):
    list_display = ('book', 'read_by')
    ordering = ('-read_by',)
    search_fields = ['book']
    fields = (('book', 'read_by'), 'description')
    
admin.site.register(Book, BookAdmin)
    
# ...
```

So, `BookAdmin` is basically a 'list' of all the changes you want for your admin.

1. `list_display` determines which fields get displayed in the list of objects
1. `ordering` determines the default field to order objects by when the page loads. 
1. `search_fields` determines which fields are searchable
1. `fields` are meant to make simple layout changes. In this case, we force the `book` and the `read_by` fields in the same line. This is the only change we made to the single-file admin view

![changed_admin.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801675994/j4GT0bhbC.png)

![change_admin_single.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1617801685009/dFByZo_7d.png)

Now it will be much easier to find and edit the correct book.

## Conclusion

In this part, you made a huge leap from the Django basics.
You added an image field, you used external packages, added custom CSS, learned how to do pagination, and changed admin to your liking.
All of the things we added have a lot more to show, so I invite you to experiment a little.
Play around with the packages, customize admin a little more and if you're not sure what's going on with the code, try to rename or comment out something.
That way, you understand why things are the way they are in no time.

You can find the whole code from Part 2 [on GitLab](https://gitlab.com/GirlThatLovesToCode/complete-django-tutorial-part-2).

> Continue with the [Part 3 - COMING SOON](), where we'll get the know the Django login, so your members will be able to register to the site.

