Django App Features

Description

This is a continuation of the constructions in the Django document. Where we left off was the creation of web response created by a call to HttpResponse.

In theory, we could create complete web applications in this manner; however, there is a much better approach. Django supports a separate HTML template language whereby Python data sent to these templates can be embedded within the templates, providing their dynamic content.

Eclipse: Install Editor for SQL

Open Eclipse MarketPlace and enter this into the Find box of Eclipse MarketPlace:
Find:
Only one entry should come up:
DBViewer Plugin ....
Click Install and then follow through, similar to others. Restart Eclipse after completion.

Use templates

The "blog" app now needs to be listed in Django's INSTALLED_APPS list. The reason is that Django will search this list of directories for available template subdirectories. It will also be necessary to have this in place when the models are installed.

Edit appsite/settings.py and make the following changes:

appsite/settings.py
INSTALLED_APPS = (
   ...
   'blog',                   # add this
)
By default, Django looks for the templates subdirectory of the application, which you need to create. In Eclipse, right-click on the blog folder and use
New ⇾ Folder
Make the name to be: templates

Otherwise, from the shell Do this (then refresh in Eclipse):
--- in ~/eclipse-workspace/appsite ----
$ mkdir blog/templates

The template files will have the .html extension, suggesting that they generate HTML; however, the content is not HTML. In many cases there is very little HTML. These template files will be controlled by Django's rendering process which uses special control syntax within the files to be interpreted by Python.

Within blog/templates create these two template files. In Eclipse use
New ⇾ Other ⇾ Web ⇾ HTML File
giving the names
base 
index
Here are the contents:

appsite/blog/templates/base.html
{% load staticfiles %} <!--  for the static function -->
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  <title>Blog</title>
 
  {% block localstyle %}{% endblock %}
</head>
 
<body>
<div class="container">
  <div class="header">
    <h2>Blog {% block subtitle %}{% endblock %}</h2>
  </div>
  <div class="navigation">
    <a href="/blog">Home</a>
    <a href="/blog/listing">Categories</a>
  </div>
  <div class="content">
    {% block content %}{% endblock %}
  </div><!-- content -->
</div><!-- container -->
 
</body>
</html>
select

blog/templates/index.html
{% extends 'base.html' %}
 
{% block content %}
The blog index in a template.
{% endblock %}
select
To make this structure "kick in" modify the index function in the views module:

blog/views.py
def index(request):
    #return HttpResponse("The blog index.")
    return render(request, 'index.html')

View the development server Open a web browser to this URL:

Initially, only the Home navigational link works. You can probably figure out what is going on:

Allow static files

Static files are usually CSS stylesheets, JavaScript scripts, and image files which are used within the website, but not processed by Django. Static files can be accessed anywhere within the file system, but making them directly accessible by Apache serves other purposes as well and so we will use the subdirectory off of the (Ubuntu) Apache root.

One restriction is that the Django development server, running on port 8000, only accesses static files when not in debug mode, meaning that you must keep this setting to use static files:

appsite/settings.py
DEBUG = True
Once you make the change to DEGUG = False, the static files are still accessible, but only outside the Django development server, i.e., through Apache in our case.

Start by creating the target directory. It makes life easier if this directory is owned by you:
$ sudo mkdir /var/www/html/static
$ sudo chown LOGIN /var/www/html/static
$ mkdir /var/www/html/static/appsite        (not sudo)
Edit appsite/settings.py and make modifications at the end of the file:

appsite/settings.py
# modify this
STATIC_URL = '/static/appsite/'   # make sure to have a trailing "/"
 
# add this entry 
STATICFILES_DIRS = (
    '/var/www/html/static/appsite',
)
The STATIC_URL is what is used in the stylesheet creation template when the function static is called. The STATICFILES_DIRS is a list of directories where Django searches for files referenced.

Create the layout stylesheet

Within PyDev, create the directory css in ~/eclipe-workspace/appsite. Within ~/eclipe-workspace/appsite/css create the file layout.css:
New ⇾ Other ⇾ Web ⇾ CSS File
Copy the following content into layout.css:

~/eclipse-workspace/appsite/css/layout.css
@charset "UTF-8";
body { 
  margin: 0; 
  padding: 0 20px; 
  font-family: sans-serif;
  font-size: 14px;
}
.header h2 { 
  text-align: center;
  color: red;
}
.navigation {
  height: 25px;
  line-height: 25px;
  border: solid 1px #009;
  border-width: 1px 0;
  margin: 10px 0;
}
.navigation a {
  padding-right: 15px;
  font-weight: bold;
  color: #c00;
  text-decoration: none;
}
.navigation a:hover {
  color: #00c;
}
table {
  border-collapse: collapse;
}
table h3 {
  margin: 10px 0;
}
td {
  vertical-align: top;
}
td.left {
  padding-right: 25px;
}
.list {
  line-height: 3ex;
}
select
Then, from the shell, link it into the site created above to permit web-accessibility:
--- in ~/eclipse-workspace/appsite ----
$ ln -s $PWD/css /var/www/html/static/appsite  
We've created a symbolic link from a web-accessible directory to our directory in the Eclipse project.

Put the stylesheet into effect

Just below the title tag in base.html, add the following link tag.

appsite/blog/templates/base.html
{% load staticfiles %} <!--  for the static call -->
...
  <title>Blog</title>
 
  <link rel="stylesheet" type="text/css" 
                         href="{% static "css/layout.css" %}" />
...

View the development server Open a web browser to this URL:

The construction of the href attribute in the stylesheet link yields this HTML content:
  <link rel="stylesheet" type="text/css" 
                         href="/static/appsite/css/layout.css" />
Although you could use this latter version directly, Django prefers the former version in case you relocate the static directories.

Install the Database

Django defaults to using an SQLite DBMS, but it supports others including MySQL. Start by creating a database. There are 3 settings needed to specify a MySQL database and you can choose what you like; they only have to match what is put into the settings.py file below. For the sake of definiteness, I'll use these:
appsite  = database name 
siteuser = MySQL user name
sitepass = MySQL user password
Create the following SQL setup file in the appsite project directory. From Eclipse, you just create an empty file and give it full name. The installed dbviewer plugin will recognize the syntax.

appsite/create_appsite.sql
drop database if exists appsite;
create database appsite;
create user if not exists siteuser@localhost identified by 'sitepass';
grant all on appsite.* to siteuser@localhost;
select
Then execute it from the shell:
--- in ~/eclipse-workspace/appsite ----
$ sudo -H mysql < create_appsite.sql
The "if exists" and "if not exists" qualifier usages mean that if you re-execute it, the database will be re-created from scratch.

Edit appsite/settings.py and replace the DATABASES dict (set up for SQLite) by this:

appsite/appsite/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'appsite',
        'USER': 'siteuser',
        'PASSWORD': 'sitepass',
    }
}
select

Create entry model for the blog app

In Django as well as other web frameworks, database tables are referred to as models. This name conveys the idea that they are not SQL table code, and are database independent as well. Django provides its own DB-independent syntax to represent models. Unlike relational database operations, access to the model is object-oriented. This is a key characteristic of an ORM in that it hides, as much as possible, the underlying database and SQL syntax. Documentation can be found here (among other sites):
https://docs.djangoproject.com/en/1.11/topics/db/models/
We will create a single model for our blog entries. In reality there are many related models involved in a creating blog, but this gives us the basic idea. Edit the blog/models.py file. Replace by the following code:

appsite/blog/models.py
from django.db import models
 
class Entry(models.Model):
    title = models.CharField(max_length=60,unique=True)
    category = models.CharField(max_length=20)
    created = models.DateTimeField(auto_now_add=True)
    visible = models.BooleanField(default=True)
    content = models.TextField()
 
    # the output of printing an entry
    def __str__(self):
        return self.title
select
Create the model table as well as other Django-specific tables by creating and running migrations:
--- in ~/eclipse-workspace/appsite ----
$ python3 manage.py makemigrations
$ python3 manage.py migrate
These commands should be re-executed whenever any change is made to a model. To see how Django translates the model syntax into SQL appropriate for our MySQL database, execute:
--- in ~/eclipse-workspace/appsite ----
$ python3 manage.py sqlmigrate blog 0001
The TABLE portion of the output is:
CREATE TABLE `blog_entry` (
    `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `title` varchar(60) NOT NULL UNIQUE,
    `category` varchar(20) NOT NULL,
    `created` datetime NOT NULL,
    `visible` bool NOT NULL
    `content` longtext NOT NULL,
)
Observations: You can see a listing of all the tables created in the appsite database by:
$ python3 manage.py dbshell
mysql> show tables;
+----------------------------+
| Tables_in_appsite          |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| blog_entry                 |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
Django supports user access credentials and authentication through its own database tables. We will not dwell on these features in our usage.

Add Blog Entries

We will write a Python module to directly add a few blog entries. Create the following file. In Eclipse, from the project, use
New ⇾ PyDev Module

appsite/make_entries.py
#!/usr/bin/env python3
 
import os
import django
 
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "appsite.settings")
 
django.setup() 
 
from blog.models import Entry
 
data = [
{
'title': 'first entry',
'category': 'django',
'content': '''\
My First Attempt
Using django
''',
},
{
'title': 'second entry',
'category': 'python',
'content': 'Great Language',
},
{
'title': 'third entry',
'category': 'python',
'content': '''\
It is OK.
A bit compulsive on indentation.
''',
},
{
'title': 'fourth entry',
'category': 'django',
'content': "best MVC system I've seen",
},
]
 
print("--- create entries ---")
 
for datum in data:
    try:
        entry = Entry(
            title=datum['title'], 
            category=datum['category'],
            content=datum['content']
        )
        entry.save()
    except Exception as err:
        print(err)
 
print("--- dump entries ---") 
 
entries = Entry.objects.all()
 
print(entries)
 
for entry in entries:
    print("{}: {}, {}, {}".format(
        entry.id, entry.title, entry.category, entry.created
    ))
 
select
Run it to create and display initial blog entries.
$ python3 make_entries.py
If you run it again, you get duplicate entry exceptions based on the uniqueness of the title field.

You can check out the results through mysql client:
$ echo "select * from blog_entry" | sudo -H mysql appsite
or through Django's dbshell feature:
$ python3 manage.py dbshell
mysql> select * from blog_entry;

Starting Over

If you need to start over with the blog_entry table, you can do this:
$ sudo -H mysql < create_appsite.sql 
$ python3 manage.py migrate
$ python3 make_entries.py
Alternatively, you can do this in a more efficient way:
$ python3 manage.py migrate blog zero
$ python3 manage.py migrate blog
$ python3 make_entries.py

Show Entries on the /blog website

Modify the index function in blog/views.py:

appsite/blog/views.py
# add this
from blog.models import Entry
 
# modify this
def index(request):
    entries = Entry.objects.all()
    print(entries)
    context = {'entries': entries}
    return render(request, 'index.html', context)
select
Change index.html:

appsite/blog/templates/index.html
{% extends 'base.html' %}
 
{% block subtitle %}- Home{% endblock %}
 
{% block content %}
{% if entries %}
  {% for entry in entries %}
    <a class='details' href="/blog/{{ entry.id }}">{{ entry.title }}</a>
    <br />
  {% endfor %}
{% else %}
  <p>No entries are available.</p>
{% endif %}
{% endblock %}
select

View the development server Open a web browser to this URL:

A few key observations:

Entry details

We've made it so that the titles of the entries are hyperlinks, supposedly to a page giving the "details" of each entry via the URL:
http://localhost:8000/blog/ENTRY_ID/
Create the following details template file. In Eclipse, from the templates directory, use
New ⇾ Other ⇾ Web ⇾ HTML File

appsite/blog/templates/details.html
{% extends 'base.html' %}
 
{% block subtitle %}- Details{% endblock %}
 
{% block localstyle %}
<style type='text/css'>
.entry td,th {
  padding: 5px;
  vertical-align: top;
}
.entry th {
  text-align: right;
}
.entry pre {
  margin: 0;
  font-family: inherit;
}
</style>
{% endblock %}
 
 
{% block content %}
<table class='entry'>
<tr><th>id:</th><td>{{entry.id}}</td></tr>
<tr><th>title:</th><td>{{entry.title}}</td></tr>
<tr><th>category:</th><td>{{entry.category}}</td></tr>
<tr><th>created:</th><td>{{entry.created}}</td></tr>
<tr><th>content:</th><td><pre>{{entry.content}}</pre></td></tr>
</table>
{% endblock %}
select
Add a details function and support code to blog/views.py.

appsite/blog/views.py
from django.shortcuts import get_object_or_404
 
def details(request, entry_id):
    entry = get_object_or_404(Entry, pk = entry_id)
    return render(request, 'details.html', {'entry': entry})
select
Modify blog/urls.py, adding a new entry into the urlpatterns list which matches what we're looking for:

appsite/blog/urls.py
urlpatterns = [
    url(r'^$', views.index, name='index'),
 
    # add this:
    url(r'^(?P<entry_id>\d+)/$', views.details),
]

View the development server Open a web browser to this URL: Test it by clicking on the entry title hyperlinks to reveal the full details of each of the selected object.

Say we choose the first entry with URL:
http://localhost:8000/blog/1/
The URL introduced matches this request:
url(r'^(?P<entry_id>\d+)/$', views.details)
It's the parenthesized groups in the URL which form the arguments to the defails function. In particular, the "\d+" matches the "1". The Django-special syntax
?P<entry_id>
means to pass the following named argument
entry_id = 1
to the details function:
def details(request, entry_id): ...
We could just as well have used the following URL, which more obviously matches:
url(r'^(\d+)/$', views.details),
The difference in this latter version is that the call to details passes 1 as a positional argument. Django seems to prefer the former style.

Another observations is regarding the code in the details function, in particular the usage of:
entry = get_object_or_404(Entry, pk = entry_id)
The meaning is to get an entry whose id primary key (pk) value is entry_id. If there is no such entry, generate a "404" error response. Try, for example, entering this URL by hand:
http://localhost:8000/blog/11/
The other oddity to note is that the URL will always maintain itself with a terminal "/" no matter whether you try to omit it.

Manipulating Entry Categories

Create the following listing template file. In Eclipse, from the templates directory, use
New ⇾ Other ⇾ Web ⇾ HTML File

appsite/blog/templates/listing.html
{% extends 'base.html' %}
 
{% block subtitle %}- By Category{% endblock %}
 
{% block localstyle %}
<style type='text/css'>
.leftSide {
  padding-right: 20px;
  vertical-align: top;
}
h3 {
  margin: 0 0 5px 0;
}
</style>
{% endblock %}
 
{% block content %}
<table>
<tr>
 
<td class='leftSide'>
<h3>Categories</h3>
  {% for category in categories %}
    <a href="/blog/listing/{{category}}">{{category}}</a>
    <br />
  {% endfor %}
</td> 
 
<td>
{% if entries %}
<h3>Entries with category "{{category}}"</h3>
  {% for entry in entries %}
    <a class='details' href="/blog/{{ entry.id }}">{{ entry.title }}</a>
    <br />
  {% endfor %}
 
{% endif %}
</td>
 
</tr>
</table>
{% endblock %}
select
Append the listing method to blog/views.py.

appsite/blog/views.py
def listing(request, category=None):
    print(category)
 
    # demo code
    print(Entry.objects.values('category'))
    print(Entry.objects.values_list('category'))
    print(Entry.objects.values_list('category').distinct())
 
    categories = Entry.objects.values_list('category',flat=True).distinct()
    print(categories)
 
    entries = None
    if category:
        entries = Entry.objects.filter(category = category)
        print(entries)
 
    context = {
        'categories': categories,
        'entries': entries,
        'category': category
    }
 
    return render(request, 'listing.html', context)
select
Modify blog/urls.py, adding two new entries into the urlpatterns which match what we're looking for:

appsite/blog/urls.py
urlpatterns = [
    url(r'^$', views.index, name='index'),
 
    url(r'^(?P<entry_id>\d+)/$', views.details), # add
 
    # add these:
    url(r'^listing/$', views.listing),
    url(r'^listing/(?P<category>[\w ]+)/?$', views.listing),
]

View the development server Open a web browser to this URL: Now the Categories navigational hyperlink should work, as well as selection of individual categories to reveal entries which have the given category.

The URL generated for the Categories link is:
http://localhost:8000/blog/listing/
which matches the URL:
url(r'^listing/$', views.listing),
If we click the category, say, "django", we generate the URL:
http://localhost:8000/blog/listing/django
now the request matches the URL:
url(r'^listing/(?P<category>[\w ]+)/?$', views.listing),
In this case the "[\w ]+" means any string of "identifier" or space characters. It would call the listing function with the named argument
category=django
A terminal "/" is optional and ignored — "/" does not belong to [\w ].

The code in the listing function wants to deliver a list of all available categories. Since these are duplicates, we're trying to get the Django equivalent of the SQL statement.
SELECT DISTINCT category FROM blog_entry;
It turns out to be what you see, using the values_list and distinct functions:
categories = Entry.objects.values_list('category',flat=True).distinct()
The three print statements prior to obtaining the correct version illustrate some of the Django peculiarities towards realizing the correct way to do it.

Manage entries by the admin feature

You can manage the contents of models through Django's admin feature. First you have to set a superuser password:
--- in ~/eclipse-workspace/appsite ----
$ python3 manage.py createsuperuser
Username (leave blank to use 'LOGIN'): 
Email address: admin@MACHINE.cs
Password: WHATEVER
Password (again): WHATEVER
Superuser created successfully.
Access the admin page through this URL. Give your login and password you used in the setup stage:
http://localhost:8000/admin
You'll see two user access-related models: Groups and Users. In order to get access to the Entry model objects, you need to add the following code to blog/admin.py after the the "Register your models here" comment line:

appsite/blog/admin.py
from django.contrib import admin
 
# Register your models here
 
from blog.models import Entry
admin.site.register(Entry)
After this refresh the browser to see access to the desired model via the name Entrys (no one said they could spell!). Click on Entrys to get access to the objects.

Example: Use entry visibility

We want the visibility flag in the entry to dictate whether this entry should be shown or not. The way to do so is to modify the blog/views.py script which retrieves the entries for display. In particular, replace the first line as indicated:

blog/views.py
...
def index(request):
    #entries = Entry.objects.all()
    entries = Entry.objects.filter(visible=True)
    ...
Next, access the admin site:
http://localhost:8000/admin
Access the second entry. Uncheck the visibility checkbox and click the Save button. Refresh the blog site and observe that the second entry is no longer shown.


© Robert M. Kline