Skip to content

mramshaw/Writing_Django

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Writing Django

PLEASE REFER TO http://github/mramshaw/Writing_Django_2 FOR A CURRENT VERSION OF DJANGO WITH PYTHON.

Known Vulnerabilities

Having used the Flask framework (it describes itself as a microframework) with SQLAlchemy for my OAuth 2.0 repo, I thought it might be fun to check out Python's Django framework.

As far as I can tell, it is a heavyweight MVC framework very much like PHP's Laravel. It has its own ORM (for Flask there is SQLAlchemy but Django's ORM apparently does not play nicely with NoSQL solutions such as DynamoDB or MongoDB).

[For a more flexible option - somewhere between Flask and Django - there is Pyramid. Pyramid apparently can play nicely with DynamoDB/Mongo but doesn't have the fuller set of features of Django. Of course, if you are looking at NoSQL solutions you probably are not interested in an ORM anyway.]

tl;dr As with pip versus npm (where the similiarities seem to far outnumber any differences), there are naming differences between Django and Laravel but the overall workflow and structure seems - perhaps not all that surprisingly - almost exactly the same.

Prerequisites

Python is required, also a package manager (either pip or pip3) appropriate to the Python version.

Verify the installed version of Python as follows:

$ python --version

[Python 2.7.12 in my case. Don't worry about version issues, Django will create needed __future__ import statements auto-magically.]

Install the latest version of Django (plus dependencies) as follows:

$ pip install --user -r requirements.txt

[Replace pip with pip3 as appropriate.]

[The dependency Python Time Zone module pytz will also be installed.]

Verify the installed version of Django as follows:

$ python -m django --version

[Version 1.11.18 in my case.]

Create a Project

Use the django-admin command to do this:

$ django-admin startproject polls

Note that django-admin creates a folder plus infra-structure files, much like maven or sbd.

Lets check the development server works:

$ cd polls
$ python manage.py runserver

The results should be something like:

Performing system checks...

System check identified no issues (0 silenced).

You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

January 10, 2019 - 20:16:30
Django version 1.11.18, using settings 'polls.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
^C$

The development server at http://127.0.0.1:8000/ should look something like:

Development_Server

Projects versus Apps

A project is mostly concerned with a website, whereas an app is mostly concerned with an application (or microservice if you will).

An app can live anywhere, but for the sake of convenience we will create this one in our current project.

Create an App

This needs to be done in the folder where manage.py lives:

$ python manage.py startapp polls-app

And if, like me, you are from a polyglot background, you will now get a reminder that Python prefers underscores to hyphens:

CommandError: 'polls-app' is not a valid app name. Please use only numbers, letters and underscores.
$

So lets get with the program:

$ python manage.py startapp polls_app

This will create a polls_app folder, plus files.

Create a View

Open polls_app/views.py and change it as follows:

$ diff -uw views.py.orig views.py
--- views.py.orig	2018-02-11 14:18:23.894106000 -0800
+++ views.py	2018-02-11 14:25:03.776911375 -0800
@@ -3,4 +3,7 @@
 
 from django.shortcuts import render
 
-# Create your views here.
+from django.http import HttpResponse
+
+def index(request):
+    return HttpResponse("Hello, world. You're at the polls index.")
$

Create a polls_app/urls.py file and change it as follows:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

Next update the polls/urls.py file as follows:

$ diff -uw urls.py.orig urls.py
--- urls.py.orig	2018-02-11 14:37:19.727374508 -0800
+++ urls.py	2018-02-11 14:45:15.909210776 -0800
@@ -13,9 +13,11 @@
     1. Import the include() function: from django.conf.urls import url, include
     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
 """
+from django.conf.urls import include
 from django.conf.urls import url
 from django.contrib import admin
 
 urlpatterns = [
+    url(r'^polls/', include('polls_app.urls')),
     url(r'^admin/', admin.site.urls),
 ]
$

Again, lets check to see if everything works:

$ python manage.py runserver

The polls app at http://127.0.0.1:8000/polls/ should look as follows:

polls

The admin interface at http://127.0.0.1:8000/admin/ should look as follows:

admin

And our development server at http://127.0.0.1:8000/ should now look like:

404

[END OF PART 1]

[START OF PART 2]

Database setup

We could open polls/settings.py and change the database settings but as I prefer to leave the time zone set to UTC this will not be needed. We will also stick with the default sqlite3 database.

So lets run our database migrations:

$ python manage.py migrate

The results should look as follows:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK
$

Model Creation

Now lets create our models - update polls_app/models.py as follows:

$ git diff polls_app/models.py
diff --git a/polls/polls_app/models.py b/polls/polls_app/models.py
index 1dfab76..04bb7f0 100644
--- a/polls/polls_app/models.py
+++ b/polls/polls_app/models.py
@@ -3,4 +3,13 @@ from __future__ import unicode_literals
 
 from django.db import models
 
-# Create your models here.
+
+class Question(models.Model):
+    question_text = models.CharField(max_length=200)
+    pub_date = models.DateTimeField('date published')
+
+
+class Choice(models.Model):
+    question = models.ForeignKey(Question, on_delete=models.CASCADE)
+    choice_text = models.CharField(max_length=200)
+    votes = models.IntegerField(default=0)
$

Model Activation

Now lets activate our models - update polls/settings.py as follows:

$ git diff polls/settings.py
diff --git a/polls/polls/settings.py b/polls/polls/settings.py
index 787a666..5892350 100644
--- a/polls/polls/settings.py
+++ b/polls/polls/settings.py
@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
 # Application definition
 
 INSTALLED_APPS = [
+    'polls_app.apps.PollsAppConfig',
     'django.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
$

And lets create our migrations:

$ python manage.py makemigrations polls_app

The results should look like:

Migrations for 'polls_app':
  polls_app/migrations/0001_initial.py
    - Create model Choice
    - Create model Question
    - Add field question to choice
$

Lets verify what the resulting SQL looks like:

$ python manage.py sqlmigrate polls_app 0001

It should look like (bear in mind this is unformatted SQLite):

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_app_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL);
--
-- Create model Question
--
CREATE TABLE "polls_app_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Add field question to choice
--
ALTER TABLE "polls_app_choice" RENAME TO "polls_app_choice__old";
CREATE TABLE "polls_app_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" integer NOT NULL REFERENCES "polls_app_question" ("id"));
INSERT INTO "polls_app_choice" ("choice_text", "votes", "id", "question_id") SELECT "choice_text", "votes", "id", NULL FROM "polls_app_choice__old";
DROP TABLE "polls_app_choice__old";
CREATE INDEX "polls_app_choice_question_id_38df74ee" ON "polls_app_choice" ("question_id");
COMMIT;
$

This can all be customized of course but it's a pretty good starting point for something that was auto-generated.

Just for fun we will run the following code to see if there are any problems so far:

$ python manage.py check

And happily there are no issues:

System check identified no issues (0 silenced).
$

Now we will migrate again to apply our migrations:

$ python manage.py migrate

And the results should look like:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls_app, sessions
Running migrations:
  Applying polls_app.0001_initial... OK
$

Lets use showmigrations to see what migrations now exist:

$ python manage.py showmigrations

And the results should look like:

admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
polls_app
 [X] 0001_initial
sessions
 [X] 0001_initial
$

The 'X' in the square brackets indicates that the migration has been run (for unapplied migrations this field would be blank).

The migrations of the most concern to us are the polls_app ones (only 0001_initial for now).

Django API

Lets invoke the Django API as follows:

$ python manage.py shell

The results should look as follows (I have IPython installed so this will be verbose):

Python 2.7.12 (default, Dec  4 2017, 14:50:18) 
Type "copyright", "credits" or "license" for more information.

IPython 2.4.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: 

Type in from polls_app.models import Question, Choice followed by Question.objects.all().

This should result in an empty set as follows:

In [1]: from polls_app.models import Question, Choice

In [2]: Question.objects.all()
Out[2]: <QuerySet []>

In [3]:

So lets create a question:

In [3]: from django.utils import timezone

In [4]: q = Question(question_text="What's new?", pub_date=timezone.now())

In [5]: q.save()

In [6]: q.id
Out[6]: 1

In [7]:

And query for questions again:

In [7]: Question.objects.all()
Out[7]: <QuerySet [<Question: Question object>]>

In [8]:

For convenience lets add string methods to our models:

$ git diff polls_app/models.py
diff --git a/polls/polls_app/models.py b/polls/polls_app/models.py
index 04bb7f0..e2118e1 100644
--- a/polls/polls_app/models.py
+++ b/polls/polls_app/models.py
@@ -2,14 +2,21 @@
 from __future__ import unicode_literals
 
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
 
+@python_2_unicode_compatible
 class Question(models.Model):
     question_text = models.CharField(max_length=200)
     pub_date = models.DateTimeField('date published')
+    def __str__(self):
+        return self.question_text
 
 
+@python_2_unicode_compatible
 class Choice(models.Model):
     question = models.ForeignKey(Question, on_delete=models.CASCADE)
     choice_text = models.CharField(max_length=200)
     votes = models.IntegerField(default=0)
+    def __str__(self):
+        return self.choice_text
$

Now if we restart our API shell, we get a more readable result:

$ python manage.py shell
Python 2.7.12 (default, Dec  4 2017, 14:50:18) 
Type "copyright", "credits" or "license" for more information.

IPython 2.4.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: from polls_app.models import Question, Choice

In [2]: Question.objects.all()
Out[2]: <QuerySet [<Question: What's new?>]>

In [3]: 

Lets add some answers:

In [3]: q = Question.objects.get(pk=1)

In [4]: q.choice_set.all()
Out[4]: <QuerySet []>

In [5]: q.choice_set.create(choice_text='Not much', votes=0)
Out[5]: <Choice: Not much>

In [6]: q.choice_set.create(choice_text='The sky', votes=0)
Out[6]: <Choice: The sky>

In [7]: q.choice_set.create(choice_text='Taxes', votes=0)
Out[7]: <Choice: Taxes>

In [8]: q.choice_set.all()
Out[8]: <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Taxes>]>

In [9]: q.choice_set.count()
Out[9]: 3

In [10]: quit
$

Admin Creation

Lets create an Admin user:

$ python manage.py createsuperuser
Username (leave blank to use 'owner'): admin
Email address: admin@example.com
Password: 
Password (again): 
This password is too common.
Password: 
Password (again): 
This password is too common.
Password: 
Password (again): 
This password is too common.
Password: 
Password (again): 
This password is too short. It must contain at least 8 characters.
This password is too common.
Password: 
Password (again): 
Superuser created successfully.
$

[Password verification is reasonably strict - 'password', 'drowssap', 'passw0rd' and '123abc' were all rejected. I eventually used '123abcde'.]

Admin Interface

Lets start the server and see if we can log in:

$ python manage.py runserver

The logged-in interface at http://127.0.0.1:8000/admin/ should look as follows:

Admin_UI

We need to modify polls_app/admin.py to register Question as follows:

$ git diff polls_app/admin.py
diff --git a/polls/polls_app/admin.py b/polls/polls_app/admin.py
index 13be29d..03ddead 100644
--- a/polls/polls_app/admin.py
+++ b/polls/polls_app/admin.py
@@ -3,4 +3,6 @@ from __future__ import unicode_literals
 
 from django.contrib import admin
 
-# Register your models here.
+from .models import Question
+
+admin.site.register(Question)
$

And now if we refresh our browser window we should get:

Admin_UI_with_Questions

And now we can edit our Question:

Question

[END OF PART 2]

Notes on Updating Django

As these instructions were an exhaustive list, the simplest solution was simply to delete the base polls folder and start again from scratch.

We could have saved our database (polls/db.sqlite3) first, and this might have saved some work. But it was simple in this case to re-create it. Obviously, if any Migrations had been written then deleting the polls folder would not have been a viable strategy. SemVer means this was a point release (i.e. 1.11.10 -> 1.11.18). If it had been a major release (say Django 1 to Django 2), then things would probably have been different too (generally, a point release means only bugfixes and minor changes, with any breaking changes requiring a major release).

The nice thing about git is that it can be used very effectively to monitor progress.

In the end there were very minimal differences, with the main difference being the generation comments and the generated SECRET_KEY.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md
	modified:   polls/polls/settings.py
	modified:   polls/polls_app/migrations/0001_initial.py

no changes added to commit (use "git add" and/or "git commit -a")
$

[These notes apply to upgrading from an insecure Django 1.11.10 -> 1.11.18.]

To Do

  • Update for latest LTS Django (1.11.18 as of the time of writing)
  • Create a requirements.txt file for dependencies
  • Add Snyk.io vulnerability scanning and badge
  • Follow parts 3 - 7 of this tutorial

Credits

Part 1:

http://docs.djangoproject.com/en/1.11/intro/tutorial01/

Part 2:

http://docs.djangoproject.com/en/1.11/intro/tutorial02/

Releases

No releases published

Packages

No packages published

Languages