Last time, in Django Tutorial (Blog App Creation) ⑥ --Article Details / Edit / Delete Function, we created the details, edit, and delete screens for each article. ..
This time, we will make major adjustments to the template, but if we divide it roughly, we will do the following.
Creating a screen common to all pages
Creating a navigation bar
Modify each template
Delete unnecessary templates and processes
Not limited to Django, there are places on the homepage that are displayed in common even if the screen changes. In Qiita, the green navigation bar displayed at the top is a good example.
↓ This However, it is difficult to write this in every template every time. If I write the code once and finish it, but when I think about the time when the correction was made ...
That's why we use ** common templates ** as a useful feature of Django. Simply put, the common parts are put together in one file, The different part for each screen is that you call and use a different template.
To do this, first create a file directly under the template folder. This time, let's create a file called /template/base.html.
└── templates
├── base.html #add to
└── blog
├── index.html
├── post_confirm_delete.html
├── post_detail.html
├── post_form.html
└── post_list.html
Write common processing in this file, and the parts that differ for each screen I will call a file such as post_detail.html.
The contents will be like this.
base.html
<!doctype html>
<html lang="ja">
<head>
<title>tmasuyama's blog</title>
<!-- 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="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{% url 'blog:post_list' %}">Top</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:post_create' %}">Post</a>
</li>
</ul>
</div>
</nav>
<!--The contents of each template are called in this block-->
{% block content %} #Attention!
{% endblock %} #Attention!
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
</body>
</html>
This tutorial doesn't go into detail about the front end, You can call and use Bootstrap from your CDN to make it look good with Bootstrap, The navigation bar is represented in
What I would like you to pay attention to is the part where you made a note of "** # Attention! **".
<!--The contents of each template are called in this block-->
{% block content %} #Attention!
{% endblock %} #Attention!
Each template will be stored here when you call the template according to the View. To put it the other way around, each template only needs to write the characteristic parts on each page.
In addition, it is necessary to specify the parent template (base.html) in the called template. The basic writing method is as follows.
Each template
{% extends 'base.html' %} #Specifying the parent template
{% block content %} #Start describing the contents
...Description specific to each template...
{% endblock %} #End of description of contents
Now you can clearly separate the roles of the parent template and the child template. Next, I will explain the navigation bar prepared by the parent template.
Using the above base.html, the following navigation bar will be displayed. This time, I'm referring to Bootstrap4's Cheat Sheat. https://hackerthemes.com/bootstrap-cheatsheet/#navbar
If you select ** Top **, you will be taken to the post_list.html screen. If you select ** Post **, you will be taken to the post_form.html screen (new post screen).
The part of base.html for displaying the navigation bar was here.
base.html
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{% url 'blog:post_list' %}">Top</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:post_create' %}">Post</a>
</li>
</ul>
</div>
</nav>
The important part here is ** "href =" {% url'blog: post_list'%} "" **.
Until now, I haven't written a link in template, If you write a reverse URL in the above format, You will now be redirected to the URL that corresponds to .
urls.py
...
path('post_list', views.PostListView.as_view(), name='post_list'),
...
Thanks to giving each URL a name like ** name ='post_list' ** here By specifying the name on the template side, it will be routed automatically.
Now, let's adjust the appearance with Bootstrap while specifying the parent template for each template. I will put the completed form.
post_detail.html
{% extends 'base.html' %}
{% block content %}
<table class="table">
<tr>
<th>title</th>
<td>{{ post.title }}</td>
</tr>
<tr>
<th>Text</th>
<!--If you insert linebreaksbk, it will be displayed properly with a line break tag.-->
<td>{{ post.text | linebreaksbr}}</td>
</tr>
<tr>
<th>date</th>
<td>{{ post.date }}</td>
</tr>
</table>
{% endblock %}
post_form.html
{% extends 'base.html' %}
{% block content %}
<p>{{ post.title }}</p>
<!--Send information to any URL on the server for action-->
<!--If you leave action blank, the URL that is currently open= /blog/post_Returns a value for create, so views.py's PostCreateView will be called again-->
<form action="" method="POST">
<table class="table">
<tr>
<th>title</th>
<td>{{ form.title }}</td>
</tr>
<tr>
<th>Text</th>
<td>{{ form.text }}</td>
</tr>
</table>
<button type="submit" class="btn btn-primary">Send</button>
{% csrf_token %}
</form>
{% endblock %}
post_confirm_delete.html
{% extends 'base.html' %}
{% block content %}
<form action="" method="POST">
<table class="table">
<tr>
<th>title</th>
<td>{{ post.title }}</td>
</tr>
<tr>
<th>Text</th>
<td>{{ post.text }}</td>
</tr>
<tr>
<th>date</th>
<td>{{ post.date }}</td>
</tr>
</table>
<p>Delete this data.</p>
<button type="submit">Send</button>
{% csrf_token %}
</form>
{% endblock %}
post_list.html
{% extends 'base.html' %}
{% block content %}
<table class="table">
<thead>
<tr>
<th>title</th>
<th>date</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for post in post_list %}
<tr>
<!-- 「url 'app name:Reverse URL'Model passed.How to draw "pk"-->
<td><a href="{% url 'blog:post_detail' post.pk %}">{{ post.title }}</a></td>
<td>{{ post.date }}</td>
<td>
<!--Displayed only when logged in as superuser-->
{% if user.is_superuser %}
<!--HTML app name_Model name_If you change it, you can edit it as it is with admin-->
<a href="{% url 'blog:post_update' post.pk %}">Edit</a>
{% endif %}
</td>
<td>
{% if user.is_superuser %}
<a href="{% url 'blog:post_delete' post.pk %}">Delete</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
The last post_list.html has some additional changes.
One is to add details, edit, and delete links for each article displayed in the list. If the link destination is determined based on the primary key, you can specify the primary key in the form of ** variable name.pk **.
Remember that the form specified by the name of the reverse URL is a fixed way of writing.
<a href="{% url 'blog:post_detail' post.pk %}">...
Also, although this tutorial does not implement the user registration function, Limit the article so that no one can edit or delete it.
This time there is only superuser, so Only if you are logged in from the admin screen (127.0.0.1:8000/admin) as superuser Show links to edit and delete articles.
{% if user.is_superuser %} #Display the contents of the if statement only when logged in as superuser
<a href="{% url 'blog:post_update' post.pk %}">Edit</a>
{% endif %}
In the above, it was the time of superuser, but the display when logging in as some other user, It is also possible to display it only when logged in as a specific user.
Now, I've left an index.html page for practice just to display Hello, If you leave more than this, it will take more time to manage, so delete it at this timing.
└── templates
├── base.html
└── blog
├── index.html #Delete this
├── post_confirm_delete.html
├── post_detail.html
├── post_form.html
└── post_list.html
Don't forget to edit urls.py test_urls.py ,, views.py, test_views.py as well.
urls.py
...
urlpatterns = [
path('', views.IndexView.as_view(), name='index'), #Delete here
...
test_urls.py
...
class TestUrls(TestCase):
"""Test redirect when accessing by URL to index page"""
def test_post_index_url(self): #Remove this method altogether
view = resolve('/blog/')
self.assertEqual(view.func.view_class, IndexView)
class TestUrls(TestCase):
"""Test redirect when accessing by URL to index page"""
def test_post_index_url(self): #Remove this method
view = resolve('/blog/')
self.assertEqual(view.func.view_class, IndexView)
...
views.py
...
class IndexView(generic.TemplateView): #Delete this generic view
template_name = 'blog/index.html'
...
test_views.py
...
class IndexTests(TestCase): #Delete this test class
"""IndexView test class"""
def test_get(self):
"""Confirm that it is accessed by the GET method and status code 200 is returned."""
response = self.client.get(reverse('blog:index'))
self.assertEqual(response.status_code, 200)
...
We made the changes all at once, so let's finally run a unit test to see if there are any errors.
(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..............E
======================================================================
ERROR: blog.tests.test_urls (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: blog.tests.test_urls
Traceback (most recent call last):
File "/usr/local/Cellar/[email protected]/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/loader.py", line 436, in _find_test_path
module = self._get_module_from_name(name)
File "/usr/local/Cellar/[email protected]/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/loader.py", line 377, in _get_module_from_name
__import__(name)
File "/Users/masuyama/workspace/MyPython/MyDjango/blog/mysite/blog/tests/test_urls.py", line 3, in <module>
from ..views import IndexView, PostListView
ImportError: cannot import name 'IndexView' from 'blog.views' (/Users/masuyama/workspace/MyPython/MyDjango/blog/mysite/blog/views.py)
----------------------------------------------------------------------
Ran 15 tests in 0.283s
FAILED (errors=1)
Destroying test database for alias 'default'...
An error has been confirmed. Since the error message is displayed using delimiters etc., it is easy to put where the error is occurring.
If you follow the error message, you will find that some of the test_urls.py cannot be imported and that an error has occurred.
ImportError: cannot import name 'IndexView' from 'blog.views'
When I read test_urls.py again, I found that it did leave the IndexView loaded at the beginning.
test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from ..views import IndexView, PostListView #This line
Erase this and do as follows.
test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from ..views import PostListView
Now let's run the unit test again.
(blog) bash-3.2$ python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...............
----------------------------------------------------------------------
Ran 15 tests in 0.223s
OK
Destroying test database for alias 'default'...
This time the test was completed without any errors. Also, the number of tests is 15, which is consistent with the reduction of 2 test methods from the previous 17 tests.
So far, I've only shown the results of passing unit tests, Even when changes occur in multiple files at once like this time I hope you found out that if you prepare a unit test in advance, you can identify the problem area with a single command.
You've successfully completed your local Django app!
Next time, let's make the application we made this time into Docker when it comes to improving the environment.
Recommended Posts