Learning Django

Learning Django

2016-08-18. Category & Tags: Python, Django, Web

(updated 2021.1)

Related:
Learn Python

Ref: This is a combined learning note of:

Update: newly tested w/ Ubuntu 1804 + py 3.8.5 + django 3.1.

Install #

python 3 (inc pip), make sure added to PATH,

then pip3 install django==3.1.1 (old: v1.7)

Start a Project/Site #

start #

mkdir myProject
cd myProject
django-admin startproject mySite
tree
>└─djangoSunnySite # container folder
>    └─djangoSunnySite # settings folder

There will be a file ‘db.sqlite3’ in the container folder, because sql-lite is the default DB for python.

set git to ignore ‘secret_key’ #

see: git – Ignore Sensitive/Secret Lines

run server #

cd <container folder>
manage.py runserver

check naming convention (naming rules) #

ref

Start an App (Under a Site) #

start #

manage.py startapp <appName>
the folders should look like this now:

└─djangoSunnySite       # container folder
    ├─app111            # one app folder
    │  └─migrations
    ├─appSunny2         # another app folder
    │  └─migrations
    ├─app_underline     # another app folder
    │  └─migrations
    ├─webapp            # another app folder
    │  └─migrations
    └─djangoSunnySite   # settings folder
        └─__pycache__

// (i use ‘app111’ as the ‘personal’ app in the video)

Warn: we should START an app before registration!!!. Otherwize, manager.py will give errors: ImportError: No module named '<appName>'.
Warn: after creating db tables, table’s name is fixed together with app name, so renaming app means also renaming db’s table name (or copy the table if we should not delete the table).

register the app in settings.py #

config route to the app in the project’s main urls.py #

vim <settings folder>/urls.py

(fig. is showing the official tutorial, code is not)

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

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

Note: ‘webapp’ is the name of app, ‘urls’ is the file name of ‘urls.py’ without extension. If the being called urls.py file does not exist, there will be exception.

add a (function-based) view, in app’s views.py #

cd <app folder>
vim views.py
from django.shortcuts import render # this statement is automaticlly added when creating apps
from django.http import HttpResponse

def index(request):
    return HttpResponse("<h2> Hello! == Hallå! <h2>")

register the (function-based) view in app’s urls.py #

cd <app folder>
vim urls.py
from django.conf.urls import path # previously "import url"
from . import views # . means import from current package

urlpatterns = [
    path(r'^$', views.index, name='index')] # "r" means regular expression, "view" is file name, "index" is the function in the file

run server #


and:

jinja Templates #

// sunny created another app “app111”

add route to the page “mypage.html” #

vim <app folder>/urls.py
# the same as the previous urls.py
from django.urls import path, re_path # note: "re" stands for regular expression

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    re_path(r'^\d+$', views.reg_index, name='reg_index'),
]
vim  <app folder>/views.py
from django.shortcuts import render
# this time, we use render() instead of HttpResponse()
def index(request):
    return render(request, '<app name>/my_page.html')

create a template for header “header.html” #

cd <app folder>
mkdir templates # plural

cd templates
mkdir <app name>

OBS: all apps’ templates’ content are shared, so it is necessary to have another layer to distinguish apps, and usually, it has the same name as the app.

vim   templates/<app name>/header.html
<!DOCTYPE html>
<html>

<head><title> a title </title></head>

<body>
    <div>
        {% block my_content %}
        {% endblock %}
    </div>
</body>

</html>

use the template in “my_page.html” #

vim   templates/<app name>/my_page.html
{% extends "<app name>/header.html" %}  <!-- NOTE: relative, all templates use 'templates' as its root dir -->

{% block my_content %}
this is sunny's template test page.
{% endblock %}

create another smaller snippet-template to be included #

mkdir templates/<app name>/includes/ # also plural
vim   templates/<app name>/includes/small.html
{% block content %}
<p>i am a small snippet, and i have been included multi times.  </p>
{% endblock %}

modify “mypage.html” to use this snippet:

{% extends "<app name>/header.html" %}

{% block content %}
<p>this is sunny's test page.  </p>
{% include "<app name>/includes/small.html" %}
{% include "<app name>/includes/small.html" %}
{% include "<app name>/includes/small.html" %}
{% endblock %}

run server #

notice, obs #

files like “header.html” /shared templates ?/ are usually used everywhere and every page for a site, not so many files.
files as snippets are used somewhere, but the amount of files are more than “shared templates”.

reading list #

Simple Logic & Loop Control by sentdex / Harrison
Quick Examples, Super Blocks, Macros

Boostrap #

install #

getbootstrap > getting-started > download
mkdir <app folder>/static/
Unzip bootstrap and put the folders (css, fonts, js …) to static.
OBS 1: The folder name “static” a default value and can be changed by editing the site’s settings.py.
OBS 2: Similar as “templates”, the “static” folder is also shared by all apps. However, this is usually general for the whole site, so we didn’t create another layer of folder.

use #

Edit <app folder>/templates/<app name>/headerBootstrap.html to include bootstrap:

...
    <head>
        <title> a title </title>
        {% load staticfiles %}
        <link rel='stylesheet' href="{% static 'css/bootstrap.min.css' %}"></link>
    </head>
...

After adding some content, the header file is bigger, the full header html content is here or origin as backup here.
Let’s modify <app folder>/views.py to re-route the default home page to this file.

from django.shortcuts import render

def index(request):
    return render(request, '<app name>/mypageBootstrap.html')

run server #

Passing Variable from Python Function-Based View to Jinja #

add a view #

add a new function-based view in views.py:

vim <app folder>/views.py
...
def contact(request):
    return render(request, 'app111/basic.html', {'content':['if u wanna contact me, plz email me.', '[email protected]']})
# The function-based view contact() has a dictionary as the last parameter. The dictionary has 2 strings.

add a template html file for the new view:

touch <app folder>/templates/<app name>/<file name: basic>.html
vim <the above file>
{%extends 'app111/headerBootstrap.html' %}

{%block content%}
    {% for tcString in content %}
    <!-- this "content" is the variable's name passed from views.py's function-based view `contact()`, and it is different from block's name in "block content", and, there is no conflict!!! (sunny: i prefer to use another varialbe name) -->
        <p>{{tcString}}</p>
    {% endfor %}
{% endblock %}

Tip: The spaces at the beginning and end of {% %} don’t matter.
Tip: To print a varialbe in jinja: {{ varName }}, spaces don’t matter either.
Tip: Flow control: “for loop”.

run server #

Class-Based Generic View (ListView & DetailView fr. models.py) #

Will use a new app “blog” to do this.

start a new app (e.g. blog) #

manage.py startapp blog
vim <setting folder>/settings.py
...
INSTALLED_APPS = [
    'blog',
...
vim <setting folder>/urls.py
...
urlpatterns = [
    path('blog/', include('blog.urls')),
...

We will use two types of view (list view & detail view). ref: doc: Generic display views
Thus:

vim <app folder>/urls.py
from django.conf.urls import url, include
from django.views.generic import ListView, DetailView
from blog.models import Post # we need a DB table/model named "Post"


urlpatterns = [
    path('', ListView.as_view(queryset = Post.objects.all().order_by("-date")[:25],
                                template_name = "blog/blog.html"))
# Tip: order by DESC, limit 25.
...
vim blog.html
{% extends "app111/headerBootstrap.html" %} {% block content %} {% for one_post in object_list %}
{{ one_post.date|date:"Y-m-d" }} {{one_post.title}}
    </br>
{% endfor %} {% endblock %}

See here for headerBootstrap.html.

Those two class-based views can be extended/enhanced by subclassing (DEF), see the blog2.views, or also official examples: for ListView and for DetailedView.

See also SingleTableView (used in my site2020proj) instead of ListView.

models.py #

Django uses models to avoid writing sql query strings directly.

vim <app folder>/models.py

models.py == the entire DB
a class == a table
a variable == a column (a column named id will be added by default, as its key)
More info here: django doc: Model field reference

from django.db import models


class Post(models.Model): # this will create a table "blog_post" in small letter format: "<app_name>_<class_name>"
    title = models.CharField(max_length = 140) # optional max_length
    body  = models.TextField()
    date  = models.DateTimeField()

    def __str__(self):
        return self.title

??? what does __str__ mean? default method/func to display current item?

Migration #

  • Migration is used to initialize project’s database.
  • Just like registering an app is the 1st thing to do in settings (after starting an app), migration is the most important thing for model.
  • One part of migration is for the app admin, this is a default app that comes with all projects.
  • As it is NOT able to undo it, so it is good to check the sql strings (before it will be run) using sqlmigrate.
manage.py makemigrations # make migration-s
# `makemigrations` can be followed by: [optional: app name]

# optional: check sql strings (need to firstly manully find files' prefix in: <app folder>/migrations/<digital-prefix>_initial.py)
manage.py sqlmigrate <app name> <digital-prefix e.g. 0001>

# if `sqlmigrate` gives good sql query strings, run:
manage.py migrate

The file “db.sqlite3” will become bigger now.

The Default Admin App #

To use the admin app, we need to create a super user first.

create a super user #

manage.py createsuperuser
manage.py runserver

Go to http://127.0.0.1:8000/admin and log in.

Now, there are “admin.py” files in each <app folder>.
Make sure the models which we want to use is registered in admin.py, otherwise the /admin cannot manipulate the corresponding tables.

vim <app folder>/admin.py
from django.contrib import admin  # added automatically

from blog.models import <table name, eg: Post>

admin.site.register(<table name, eg: Post>)  # Register your models here.

The site will have one more thing:

Display #

See also: a more generic view, see video.

add some test blog posts via admin app #

We can add a new post using this GUI:

Thus, we have something to show:

add view to display a single post #

register a function-based deailed-view: #

vim <app folder>/urls.py
...
urlpatterns = [
    re_path(r'^(?P<pk>\d+)$', DetailView.as_view(model = Post, template_name = 'blog/post.html')),
...

Tip: ‘?P’ is a named (capturing) group. ‘pk’ means primary-key. doc: urls:named-groups

add the deailed-view: #

vim <app folder>/templates/<app name>/post.html
{% extends "app111/headerBootstrap.html" %}

{% block content %}
    <h3>{{post.title}}</h3>
    <h6>{{post.date}}</h6>
    {{post.body|safe|linebreaks}} <!-- the "safe" filter tells jinja that the string is safe (no need to escape html tags), so we can have html tags taking effect (come into operation) instead of escaped and displayed as plain text -->
{% endblock %}

OBS: I cannot change the word “post”, why and where is that word from? model???

Also, modify blog.html to have links to each post.

{% extends "app111/headerBootstrap.html" %}

{% block content %}
    {% for one_post in object_list %}
    <h5>{{ one_post.date|date:"Y-m-d" }}<a href="/blog/{{one_post.id}}">{{one_post.title}}</a></h5> <!-- jinja: apply a filter to format date -->
    {% endfor %}
{% endblock %}

Model Form #

It would be necessary to have seperate forms/urls for normal users instead of uing the admin app.

add a form for creating a post (in the model/db-table) #

post creation commit diff (in app blog2).

Note, base.html is used for the app blog2, a base template is the frame for all pages in the app, thus usually a big file.

add html files: #

Add form html file, which MUST be named as <model name>_form.html i.e. “post_form.html here (commit).
Besides, form-group is suggested to have another separate file (any filename, called within <model name>_form.html ), e.g. form-group-template.html or form_template etc. (commit).

add a view in views.py: #

...
from django.views.generic.edit import CreateView

class PostCreate(CreateView):
     model = Post
     fields = ['title', 'body', 'date']

commit

add route in app urls.py: #

...
urlpatterns = [
    path('post/create/', views.PostCreate.as_view(), name='post-create')
...

success insert-db redirection in models.py via reverse() #

...
from django.urls import reverse  # or import reverse_lazy
# OBS: diff django v1: from django.core.urlresolvers import reverse

    def get_absolute_url(self):
        return reverse('blog2:detail', kwargs={'pk': self.pk}) # form inserted into db, then redirect to this url.

add corresponding namespace in main urls.py: #

OBS: Namespace is used for ReverseMatch.

...
urlpatterns = [
    path('blog2/', include(('blog2.urls', 'blog2'), namespace='blog2')),  # OBS: diff django v1
...

add corresponding name in app urls.py: #

...
urlpatterns = [
    re_path(r'^(?P<pk>\d+)$', views.DetailView.as_view(), name='detail'),
...

update & delete #

code see commit diff
OBS: “GET” deletion requires confirmation (i.e. via post_confirm_delete.html), but “POST” does NOT.

ref: video

Passing Parameters/Variables #

function-based view to jinja #

See above in the tutorial.

browser/client to class-based View using post/get (NOT tried) #

self.request...
request.GET['my_key'] # the key MUST exists, otherwise an exception
request.GET.get('my_key', <some_default_value>) # the key may exists, otherwise HttpResponseBadRequest() by default

request.query_params.get('status', None) # for GET
request.data.get('role', None) # for POST

[stackoverflow: ref1 & ref2]

browser/client to class-based View using named url parts from urls.py #

using to set form default values (in class-based View): #

class AbcView(CreateView):
    ...

    def get_initial(self):
        # school = get_object_or_404(models.School, school_pk=self.kwargs.get('school_pk'))
        return {
            var1 = self.kwargs['var'],
            # or:
            var1 = self.kwargs.get('var', <default_value>),
        }

using to query db (in class-based View): #

Usually in views.py, e.g. the_select_item = MyModel.objects.get(pk=var1).

class-based view to jinja template/js (by updating context) #

# in views.py
class AbcView(generic.ListView):
    var1 = 'asdf'

    def get_context_data(self, **kwargs):
        context = super(BatchListView, self).get_context_data(**kwargs)
        context.update({'var1': self.var1})
        return context
<!-- use var1 in jinja html template -->
    {% if var1 %} <!-- if var1 is defined -->
        ...
    {% else %}
        ...
    {% endif %}

WARN: {% if var1 is defined %} checking will not work.

toast (floating-message) #

In views.py:

from django.contrib import messages

messages.success(request, "ok")
messages.info(request, "aaa")
messages.warning(request, "aaa")
messages.error(request, "danger")
messages.debug(request, "aaa")

Later in front end .html:
bootoast can be used which works with bootstrap 3 & 4.

<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'css/bootoast.css' %}" type="text/css" />
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
<script src="{% static 'js/bootoast.js' %}"></script>

{% if messages %} {% for message in messages %} {% if message.tags == 'success' %}
<script>
  bootoast({
    message: "{{message}}",
    timeout: 3,
    type: "{{ message.tags }}",
  });
</script>
{% elif message.tags == 'info' %}
<script>
  bootoast({
    message: "{{message}}",
    timeout: 3,
    type: "info",
  });
</script>
{% elif message.tags == 'warning' %}
<script>
  bootoast({
    message: "{{message}}",
    timeout: 3,
    type: "warning",
  });
</script>
{% elif message.tags == 'error' %}
<script>
  bootoast({
    message: "{{message}}",
    timeout: 3,
    type: "danger",
  });
</script>
{% endif %} {% endfor %} {% endif %}

File Uplad #

upload to mysql file-field column #

see video

upload to folder with filename in db #

+ function-based view
see sunny’s implementation, (ref: minimal-django-file-upload-example)

+ class-based view
//to be cont.

multi filenames in mysql’s one record/row’s #

//to be cont.

Put Online #

Situation 1: we already have a project online.
We just need to copy the apps and db file to the existing project container folder, then modify settings.py to register the new apps. This is what the tutorial video did.
Situation 2: new project.
//to be cont.

Restful API with Angular - A Shopping List Demo #

(Very Simple Demo with Json)

  • George Kappel 2013 video (backed up as: django-rest-framework and angularjs.mp4)
  • code with db
    See Shopping List Demo for the rest Angular part. Below is the API part.
    George’s video is not giving clear explaination for beginners, see Chris Hawkes 2015 video for clear explaination to understand. However, Chris did not provide code, we can continue to use the app “blog”. (video backed up as: how-to-create-a-RESTful-API-with-Django-in-20-minutes.mp4)

Virtualenv
OBS: This demo is old, and needs Python 2.7.
If we run using py3: NameError: name 'unicode' is not defined.
We can use virtual env to solve this problem.

Migrate Database
The original code does not contain a db file (I added to my github repo), so we need to do migration which requires django 1.7 or higher. It is different to the freezed 1.5, we can do this with another venv. See LearnDjango for more info.

C:\Users\user_name\Envs\py27_rest_demo\Scripts\python.exe manage.py makemigrations
C:\Users\user_name\Envs\py27_rest_demo\Scripts\python.exe manage.py migrate

rest_framework install & settings #

pip install --prefer-binary djangorestframework
vim <project setting folder>/settings.py
...
INSTALLED_APPS = [
    'rest_framework',
...

# add the following control (otherwise, default settings allow: GET, POST, HEAD, OPTIONS)
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly']
}
...

serializers in views.py (app) #

vim views.py
from rest_framework import generics, serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post # model/table name
        field = ('id', 'title', 'body', 'date') # some/all columns in the corresponding table


class PostList(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

Tip: Chris Hawkes has many serializers, so he created a dedicated file “serializers.py” for serializers, then import it in views.py.

urls.py (project & app) #

vim <project setting folder>/urls.py
urlpatterns = [
    url(r'^api/', include('<app name>.urls', namespace='rest_framework')),
...
vim <app folder>/urls.py
urlpatterns = [
    url(r'^posts/$', PostList.as_view()),
    url(r'^post/(?P<pk>\d+)$', PostDetail.as_view()),
...

User Sessions #

Sessions are enabled by default which can be seen in settings.py:

LOGIN_URL = '/my-login_url/'

INSTALLED_APPS = [
...
    'django.contrib.sessions',
...
]

MIDDLEWARE = [
...
    'django.contrib.sessions.middleware.SessionMiddleware',
...
]

Going through ‘urls.py’ and ending in ‘views.py’:

from django.contrib.auth import authenticate, login, logout

def user_login(request):
    if request.method == 'POST':
        form = AuthenticationForm(request, data=request.POST)
        if form.is_valid():  # verify passwd, but not actually login
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            user = authenticate(request, username=username, password=password)
            if user is not None:
                login(request, user)
                messages.success(request, "Login success")
                return redirect("myapp:url-name")
        else:
            messages.error(request, "用户名或密码错误。")
    form = AuthenticationForm()
    return render(request,
                  'myapp/my_template.html',
                  {'form': form}
                  )

def user_logout(request):
    logout(request)
    return redirect("density:User-login")

Still in views.py or other access points such as API DEF file, decorate functions using @login_required and modify classes using LoginRequiredMixin.

@login_required
def some_func_needs_authentication(request):
    pass


class MyView(LoginRequiredMixin, AbcView):
    pass

For front-end, do:

{% if user.is_authenticated %} some basic menu ... {% endif %} {% if user.is_superuser %} some restricted menu ... {% endif %}

Tip: user status should be set to at leat active/working state so that the user can login.

Extra Tips #

  • 14 PyCharm Plugins;
  • pyinstaller -> js MIME as plain/text so not executable, see pyinstaller;
  • set max_age when response.set_cookies() otherwise different browsers actdifferently;
    4 PyCharm Plugins](https://mp.weixin.qq.com/s/95KOSyWXN2A4gwZPrFdhvw);
  • pyinstaller -> js MIME as plain/text so not executable, see pyinstaller;
  • set max_age when response.set_cookies() otherwise different browsers actdifferently;