Daily Archives: 13/04/2016

Django – Ngôn ngữ Template

Trong phần này chúng ta sẽ tìm hiểu kỹ hơn về cú pháp của Template trong Django.

Ngôn ngữ Template của Django được thiết kế với mục đích chính là hỗ trợ những người đã từng làm việc với HTML, do đó nếu bạn đã từng học HTML thì sẽ không quá khó khăn để làm quen với Template.

Nếu bạn đã từng làm việc với các ngôn ngữ như Javascript, PHP, JSP… hay các ngôn ngữ có thể trộn chung với code HTML thì bạn cũng nên phân biệt là Template của Django không giống các ngôn ngữ đó. Các ngôn ngữ như Javascript, PHP… là ngôn ngữ lập trình, dùng để thực hiện các công việc mang tính logic, còn HTML chỉ là ngôn ngữ đánh dấu, tức là chỉ dùng để hiển thị giao diện chứ không mang nặng phần tính toán, Template cũng vậy, đây chỉ là ngôn ngữ hỗ trợ hiển thị giao diện.

Hệ thống Template của DJango cung cấp các thẻ có các chức năng tương tự như các câu lệnh trong Python, chẳng hạn như thẻ if dùng để kiểm tra điều kiện, thẻ for dùng trong vòng lặp… các thẻ này cũng không hoạt động giống như trong Python. Khi dịch thì Django chỉ dịch các thẻ Template chứ không đụng chạm gì tới HTML.

Template

Một Template đơn giản chỉ là một file text, có thể là bất cứ định dạng nào như .html, .xml, .csv…v.v

Template chứa các biến sẽ được thay thế bằng giá trị thực khi dịch, và các thẻ dùng để thực hiện các câu lệnh logic.

Đây là một đoạn template cơ bản:

{% extends "base_generic.html" %}

{% block title %}
    {{ section.title }}
{% endblock %}

{% block content %}
    <h1>{{ section.title }}</h1>
    {% for story in story_list %}
    <h2>
        <a href="{{ story.get_absolute_url }}">
            {{ story.headline|upper }}
        </a>
    </h2>
    {{ story.tease|truncatewords:"100" }}
    {% endfor %}
{% endblock %}

Biến

Biến là những thứ giống như {{ variable }}. Khi trình dịch template đọc đến một biến thì biến sẽ được thay thế bằng một giá trị thật (mà chúng ta truyền vào từ hàm render() trong các view). Biến chỉ được đặt tên bằng các kí tự chữ cái và dấu gạch dưới (_).

Chúng ta dùng dấu chấm (.) để truy xuất các thuộc tính của biến.

Trong đoạn code trên, {{ section.title }} sẽ được thay thế bởi thuộc tính title của đối tượng section.

Nếu bạn gõ sai tên biến thì Django sẽ thay bằng chuỗi rỗng chứ không báo lỗi.

Bộ lọc – Filter

Django cung cấp các bộ lọc để hỗ trợ chúng ta hiển thị dữ liệu theo nhiều cách khác nhau.

Ví dụ {{ name|lower }}trong đó lower là một bộ lọc, có tác dụng chuyển toàn bộ chữ cái thành chữ thường. Để dùng các bộ lọc thì chúng ta kèm theo dấu | và tên bộ lọc vào sau tên biến.

Chúng ta cũng có thể dùng nhiều bộ lọc cùng một lúc, các bộ lọc được thực hiện tuần tự từ trái sang phải, ví dụ {{ text|escape|linebreaks }} có tác dụng xuống dòng sau khi in dữ liệu.

Một số bộ lọc cần có cả tham số nữa, ví dụ như {{ bio|truncatewords:30 }} có nghĩa là lấy 30 từ đầu tiên của biến bio.

Nếu tham số của bộ lọc có khoảng trống thì chúng ta phải kẹp chúng trong cặp dấu nháy kép “”. Ví dụ {{ list|join:", " }} sẽ nối các item trong biến list thành một string, ngăn cách nhau bởi dấu phẩy và dấu cách.

Django có khoảng 60 bộ lọc. Bạn có thể tìm hiểu chúng tại đây. Ở đây mình chỉ giới thiệu một số bộ lọc thường dùng:

  • default: nếu biến không có giá trị hoặc giá trị rỗng thì thay thế bằng giá trị default. Ví dụ {{value|default:"nothing"}}
  • length: trả về độ dài của dữ liệu, có thể áp dụng cho string và list. Ví dụ {{value|length}}
  • filesizeformat: đổi kiểu số thành định dạng file, ví dụ {{value|filesizeformat}} sẽ chuyển con số 123456789 thành 117.7 MB.

Thẻ – Tag

Thẻ có cú pháp {% tag %}. Thẻ thì phức tạp hơn biến một tí, có thể dùng để tạo chuỗi, thực hiện các luồng điều khiển hoặc load các thông tin khác vào template.

Có một số thẻ đi kèm với cả thẻ kết thúc nữa, ví dụ {% tag %} thì sẽ có {% endtag %}.

Cũng giống như các bộ lọc, số lượng thẻ trong Django rất nhiều, bạn có thể xem danh sách các thẻ ở đây. Trong bài này mình cũng chỉ giới thiệu các thẻ thường dùng:

  • for: duyệt qua một đối tượng danh sách. Ví dụ:
{% for athlete in athlete_list %}
<li>{{ athlete.name }}</li>
{% endfor %}
  • if, elifelse: kiểm tra một biến, nếu biến đúng thì thực hiện đoạn code bên trong.
{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}

Trong đoạn code trên, nếu athlete_list không rỗng thì in ra biến athlete_list, ngược lại thì kiểm tra nếu athlete_in_locker_room_list không rỗng thì in ra đoạn text “Athletes should…”, còn nếu không thì in ra đoạn text “No athletes.”

Ngoài kiểm tra các biến thì bạn cũng có thể áp dụng bộ lọc vào biến khi dùng thẻ if:

{% if athlete_list|length > 1 %}
    Team: {% for athlete in athlete_list %} ... {% endfor %}
{% else %}
    Athlete: {{ athlete_list.0.name }}
{% endif %}

Hầu hết các bộ lọc chỉ trả về giá trị là kiểu chuỗi nên thường sẽ không dùng được các biểu thức so sánh với số nguyên như trên, length chỉ là một trong số ít ngoại lệ.

  • blockextends: kế thừa template, tức là dùng các file template khác. Chúng ta sẽ tìm hiểu thêm sau.

Bình luận – Comment

Bình luận được đặt trong cặp dấu {# #}, các đoạn code bên trong cặp dấu này sẽ không được thực thi, ví dụ:

{# greeting #}hello

Django chỉ hỗ trợ bình luận trên một dòng. Nếu muốn bình luận trên nhiều dòng thì bạn dùng thẻ comment.

Thừa kế template

Tính năng mạnh mẽ nhất và cũng là phức tạp nhất của Template trong Django là tính năng thừa kế. Tính năng thừa kế cho phép bạn xây dựng một bộ template tổng quát và các template con, trong đó template tổng quát sẽ chứa các template con.

Ví dụ:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="style.css" />
    <title>{% block title %}My amazing site{% endblock %}</title>
</head>

<body>
    <div id="sidebar">
        {% block sidebar %}
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
        {% endblock %}
    </div>

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

Đoạn code trên là template thiết kế bộ khung của một trang web, cấu trúc của template này bao gồm 2 cột, nằm giữa các thẻ block. Nhiệm vụ của các template con là lấp đầy các khoảng trống của 2 cột đó.

Trong đoạn code trên có 3 thẻ block là title, contentsidebar, nhiệm vụ của thẻ block là báo cho Django biết đây là nơi mà các template con có thể override lại và chèn dữ liệu cần hiển thị vào đó.

Ví dụ về một template con:

{% extends "base.html" %}

{% block title %}My amazing blog{% endblock %}

{% block content %}
    {% for entry in blog_entries %}
        <h2>{{ entry.title }}</h2>
        {{ entry.body }}
    {% endfor %}
{% endblock %}

Để một template con có thể override lại các thẻ block của template khác thì ở đầu template chúng ta khai báo thẻ extends với tên file template. Trong ví dụ trên, trình biên dịch Django sẽ đọc trong template cha và thấy các thẻ block trong template cha cũng có trong template con nên phần block trong template con sẽ được chèn vào trong template cha.

Trong ví dụ trên thì tùy thuộc vào giá trị của blog_entries mà kết quả là template cha có thể sẽ có nội dung như sau:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="style.css" />
    <title>My amazing blog</title>
</head>

<body>
    <div id="sidebar">
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
    </div>
    <div id="content">
        <h2>Entry one</h2>
        This is my first entry.
        <h2>Entry two</h2>
        This is my second entry.
    </div>
</body>
</html>

Trong đoạn code template con chúng ta chỉ định nghĩa 2 block là contenttitle, nếu chúng ta không override block sidebar thì nội dung của sidebar sẽ được dùng là nội dung trong template cha.

Một số lưu ý:

  • Thẻ {% extends %} luôn được đặt trước tất cả các thẻ còn lại.
  • Nên override từng thẻ block trong từng file template chứ không nên “ôm” tất cả vào trong một file.
  • Thẻ block trong template con cũng có thể dùng lại nội dung của template cha, chỉ cần gọi {{block.super}}
  • Thẻ {% endblock %} không cần phải có tên block theo sau nhưng chúng ta cũng nên đưa tên block vào để code dể đọc và dễ quản lý hơn. Ví dụ:
{% block content %}
...
{% endblock content %}
  • Không được có 2 thẻ block có tên giống nhau.

Tự động thoát HTML

Thoát HTML tức là trang web tự động chuyển đổi các kí tự đặc biệt trong HTML sang một dạng mã, dùng để bảo vệ website.

Ví dụ chúng ta có đoạn code template như sau:

Hello, {{ name }}

Thoạt nhìn thì có vẻ đơn giản, chúng ta có thể yêu cầu người dùng nhập vào một textbox rồi lưu vào biến name, sau đó in nội dung trong biến name ra thôi.

Nhưng nếu người dùng không nhập vào biến name một đoạn chuỗi bình thường mà là đoạn chuỗi kì lạ như:

<script>alert('hello')</script>

Lúc này Django sẽ dịch đoạn template sang đoạn code HTML như sau:

Hello, <script>alert('hello')</script>

Khi chạy, trang web sẽ hiển thị một hộp thoại thông báo. Đó chỉ là trường hợp đơn giản, trong thực tế hacker có thể lợi dụng lỗ hổng này để khai thác nhiều thứ hơn nữa. Đây gọi là kỹ thuật tấn công Cross Site Scripting (XSS).

May mắn là mặc định trình dịch Template của Django tự động “thoát” (auto-escape) các kí tự đặc biệt, tức là chuyển đổi những kí tự sau đây thành những kí tự mã khác:

  • Dấu < chuyển thành &lt;
  • Dấu > chuyển thành &gt;
  • Dấu nháy đơn ' chuyển thành &#39;
  • Dấu nháy kép " chuyển thành &quot;
  • Dấu & chuyển thành &amp;

Vì tính năng tự động thoát này mà bạn không cần phải lo đến vấn đề bảo mật XSS nữa.

Nhưng nếu bạn muốn tắt tính năng này thì làm sao?

Trước hết là tại sao bạn lại muốn tắt tính năng này, đó là vì trong một số trường hợp bạn thật sự muốn in các đoạn mã HTML, Javascript… lên trang web, chẳng hạn như bạn dự định xây dựng một blog về lập trình, như blog Phở Code 🙂 thì việc đăng các đoạn code lên trang web là thường xuyên, và do đó bạn cần các kí tự HTML hiển thị nguyên gốc – tức là không được “thoát”.

Để tắt “thoát” trên từng biến: chúng ta dùng bộ lọc safe:

This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}

Đoạn code trên sẽ cho ra HTML như sau:

This will be escaped: &lt;b&gt;
This will not be escaped: <b>

Tắt “thoát” trên template: chúng ta đặt nội dung file template hoặc một phần nào đó của template trong cặp thẻ autoescape:

{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}

Thẻ autoescape nhận tham số on hoặc off tương ứng với bật và tắt.

Bạn cũng có thể lồng các cặp thẻ autoescape vào nhau.

Auto-escaping is on by default. Hello {{ name }}
{% autoescape off %}
    This will not be auto-escaped: {{ data }}.

    Nor this: {{ other_data }}
    {% autoescape on %}
        Auto-escaping applies again: {{ name }}
    {% endautoescape %}
{% endautoescape %}

khi kế thừa template thì nếu template cha tắt “thoát” thì các template con cũng sẽ tự tắt tính năng này, nếu muốn bật tính năng này thì template con phải override lại.

Gọi phương thức

Bạn không chỉ có thể truy xuất dữ liệu từ các thuộc tính trong các biến mà còn có thể gọi phương thức của chúng nữa, tất nhiên là bạn chỉ có thể gọi các phương thức có trả về dữ liệu để hiển thị chứ không thể gọi các phương thức thực hiện tính toán mà không trả về thứ gì được.

Ví dụ, các đối tượng Queryset có phương thức count() trả vê số lượng phần tử của nó, chúng ta có thể gọi phương thức này như sau:

{{ task.comment_set.all.count }}

Bạn cũng có thể gọi các phương thức do bạn tự định nghĩa:

class Task(models.Model):
    def foo(self):
        return "bar"
{{ task.foo }}

Đáng tiếc là bạn không thể truyền tham số vào các lời gọi hàm trong Template vì mục đích chính của template cũng chỉ là hiển thị dữ liệu chứ không phải tính toán, do đó bạn chỉ có thể gọi các phương thức không có tham số.

Django – Phân trang

Trong phần này chúng ta sẽ học cách phân trang. Django cung cấp lớp django.core.paginator hỗ trợ phân trang rất tốt.

Ví dụ

Chúng ta tạo một app có tên là pagination.

C:\Project\mysite>python manage.py startapp pagination

Thêm app vào danh sách INSTALLED_APPS:

INSTALLED_APPS = [
    #...
    'pagination',
    #...
]

Tiếp theo chúng ta tạo model và thêm vào một số dòng dữ liệu mẫu để test.

from django.db import models

# Create your models here.

class Customer(models.Model):
    name = models.CharField(max_length=100)
    country = models.CharField(max_length=20)

Chúng ta định nghĩa model Customer có 2 trường là namecountry.

Cập nhật lại CSDL:

python manage.py makemigrations
python manage.py migrate

Đoạn script sau sẽ tạo một số dòng dữ liệu mẫu trong bảng pagination_customer.

import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings");
django.setup()

from pagination.models import Customer

Customer.objects.create(name='Alfreds Futterkiste', country='Germany')
Customer.objects.create(name='Ana Trujillo Emparedados y helados', country='Mexico')
Customer.objects.create(name='Antonio Moreno Taquería', country='Mexico')
Customer.objects.create(name='Around the Horn', country='UK')
Customer.objects.create(name='Berglunds snabbköp', country='Sweden')
Customer.objects.create(name='Blauer See Delikatessen', country='Germany')
Customer.objects.create(name='Blondel père et fils', country='France')
Customer.objects.create(name='Bólido Comidas preparadas', country='Spain')
Customer.objects.create(name='Bon app', country='France')
Customer.objects.create(name='Bottom-Dollar Marketse', country='Canada')
Customer.objects.create(name='Bs Beverages', country='UK')
Customer.objects.create(name='Cactus Comidas para llevar', country='Argentina')
Customer.objects.create(name='Centro comercial Moctezuma', country='Mexico')
Customer.objects.create(name='Chop-suey Chinese', country='Switzerland')
Customer.objects.create(name='Comércio Mineiro', country='Brazil')
Customer.objects.create(name='Consolidated Holdings', country='UK')
Customer.objects.create(name='Drachenblut Delikatessend', country='Germany')
Customer.objects.create(name='Du monde entier', country='France')
Customer.objects.create(name='Eastern Connection', country='UK')

Tiếp theo chúng ta viết template và hàm view.

from django.shortcuts import render

# Create your views here.
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import Customer

def listing(request): 
    customer_list = Customer.objects.all()
    paginator = Paginator(customer_list, 5)
 
    pageNumber = request.GET.get('page')
    try:
        customers = paginator.page(pageNumber)
    except PageNotAnInteger:
        customers = paginator.page(1)
    except EmptyPage:
        customers = paginator.page(paginator.num_pages)
 
    return render(request, 'list.html', {'customers':customers})

Chúng ta sử dụng lớp Pagination để thực hiện phân trang.

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

Đầu tiên chúng ta import một số lớp cần thiết.

paginator = Paginator(customer_list, 5)

Hàm khởi tạo Paginator() nhận vào 2 tham số, tham số đầu tiên là một đối tượng QuerySet, tham số thứ 2 là số tượng item trên mỗi “trang”. Trong ví dụ trên chúng ta đưa đối tượng customer_list vào với số lượng 5 item mỗi trang.

pageNumber = request.GET.get('page')

URL của chúng ta có thêm tham số page là số thứ tự của trang muốn xem.

try:
    customers = paginator.page(pageNumber)
except PageNotAnInteger:
    customers = paginator.page(1)
except EmptyPage:
    customers = paginator.page(paginator.num_pages)

Nếu tham số page không hợp lệ, chẳng hạn như page=abc thì Paginator sẽ giải phóng lỗi PageNotAnInterger, trong trường hợp này chúng ta trả về trang đầu tiên với phương thức Paginator.page(), hoặc nếu page nằm ngoài pham vi trang cho phép, chẳng hạn như chúng ta chỉ có 4 trang nhưng tham số page=1000 thì Paginator sẽ giải phóng lỗi EmptyPage, ở đây chúng ta xử lý bằng cách trả về trang cuối cùng bằng thuộc tính num_pages.

Template:

<table>
    <tr>
        <th>Customer name</th>
        <th>Country</th>
    </tr>
    {% for customer in customers %}
    <tr>
        <td>{{ customer.name }}</td>
        <td>{{ customer.country }}</td>
    </tr>
    {% endfor %}
</table>
<div class="pagination">
    <span class="step-links">
        {% if customers.has_previous %}
            <a href="?page={{ customers.previous_page_number }}">Previous</a>
        {% endif %}
    </span>

    <span class="current">
        Page {{ customers.number }} of {{ customers.paginator.num_pages }}.
    </span>
    
    <span>
        {% if customers.has_next %}
            <a href="?page={{ customers.next_page_number }}">Next</a>
        {% endif %}
    </span>
</div>

Cuối cùng chúng ta tạo URL và chạy server để xem kết quả.

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.listing),
]
from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^pagination/', include('pagination.urls')),
]

Capture