(91) 350-9520 support@omarine.org M-F: 7 AM - 7 PM; Weekends: 9 AM - 5 PM

Lập trình Web: Python: Django: Chương trình thăm dò ý kiến, phần 3

8. Thiết kế view

Khi một request đi tới, URL được định tuyến tới một view và view phải có nhiệm vụ trả về đáp ứng. Mỗi view được đại diện bởi một hàm Python (hoặc phương thức đối với view trên cơ sở lớp).

Trong chương trình Polls chúng ta có 4 view:

  • index: Trình bày một số câu hỏi gần đây

  • detail: Trình bày một câu hỏi với một form để biểu quyết

  • results: Trình bày kết quả cho một câu hỏi cụ thể

  • vote: Xử lý biểu quyết cho một bình chọn đối với một câu hỏi cụ thể

Một mẫu URL đơn giản là một dạng chung của URL, ví dụ /newsarchive/<year>/<month>/ (chính xác là phần URL sau tên miền). Để ánh xạ URL tới view, Django sử dụng một module urls mà được gọi là “URLconfs”. Bạn tạo tệp polls/urls.py với nội dung sau:

from django.urls import path
from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

Một view “home” được bổ sung vào dự án dành cho trang đầu, bạn soạn thảo tệp mysite/urls.py với nội dung sau:

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

urlpatterns = [
    path('', views.home, name='home'),
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

Hàm path() nhận bốn đối số: hai đối số yêu cầu là routeview, hai đối số tùy chọn là kwargsname:

  • route

    route là một xâu kí tự chứa mẫu URL. Khi xử lý một request, Django bắt đầu tại mẫu đầu tiên trong urlpatterns, rồi đi xuống dưới danh sách, so sánh URL yêu cầu với từng mẫu cho tới khi nó thấy khớp.

    Các mẫu không tìm tham số GET và POST, hoặc tên miền. Ví dụ, trong một request tới https://www.example.com/myapp/, URLconfs sẽ tìm cho myapp/. Trong một request tới https://www.example.com/myapp/?page=3, URLconfs cũng tìm cho myapp/.

  • view

    Khi Django tìm thấy một mẫu khớp, nó gọi hàm view tương ứng với một đối tượng HttpRequest như đối số đầu tiên và các giá trị “bắt được” từ route như những đối số từ khóa. view cũng có thể là một include().

  • kwargs

    Là một từ điển dùng để đưa các đối số từ khóa bổ sung tới view mục tiêu.

  • name

    Đặt tên cho URL để bạn tham chiếu tới nó từ mọi nơi khác trong Django, đặc biệt là từ trong các tệp khuôn mẫu. Điều này rất hữu ích vì nó cho phép bạn làm một thay đổi toàn diện các mẫu URL của dự án mà chỉ cần động tới một file đơn. Đây là tên mẫu URL, hay còn gọi là URL có tên.

Khi bạn trỏ trình duyệt tới một URL chỉ có tên miền, ví dụ https://www.example.com thì như phân tích ở trên, URLconfs (mysite.urls) sẽ tìm xâu rỗng. Do đó Django sẽ gọi views.home. URLconfs này được dùng vì cấu hình sau đây trong mysite/settings.py:

ROOT_URLCONF = 'mysite.urls'

Chúng ta tạo tệp mysite/views.py với nội dung sau:

from django.shortcuts import render 

def home(request):
    return render(request, 'index.html')

render() là một hàm “shortcuts” (gọi các hàm khác bên trong và có nhiều đối số mặc định thay vì phải viết nhiều mã), nó nhận tên khuôn mẫu là 'index.html' và trả về một đối tượng HttpResponse (là một nhiệm vụ sau cùng của một view). Chúng ta sẽ xem xét các tệp khuôn mẫu bên dưới.

Trong một request tới chương trình Polls, ví dụ https://www.example.com/polls/, hàm include() sẽ được gọi. Hàm này cho phép tham chiếu tới URLconfs khác. Django cắt đi phần URL đã khớp, là polls/, và gửi phần còn lại cho URLconfs được bao hàm, là polls.urls, xử lý tiếp. Trong trường hợp này phần URL còn lại là rỗng, do đó nhìn vào tệp polls/urls.py chúng ta thấy rằng mẫu khớp là mẫu đầu tiên. Vì vậy, views.IndexView.as_view() được xử lý. Để ý dòng app_name = 'polls', tên tham chiếu cho URL này sẽ là 'polls:index'. Sử dụng tiền tố như vậy là cần thiết cho một dự án có nhiều chương trình, cần các tên phân biệt.

Bạn tạo tệp polls/views.py với nội dung sau:

from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
from django.utils import timezone
from django.template.response import TemplateResponse

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return TemplateResponse(request, 'polls/results.html', {'question': question})

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        context = {
            'question': question,
            'error_message': "You didn't select a choice.",
        }
        return TemplateResponse(request, 'polls/detail.html', context)
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Trong một request, ví dụ tới https://www.example.com/polls/1/results/, phần URL còn lại sau khi đã cắt để chuyển cho polls.urls là '1/results/'. Xâu này khớp '<int:question_id>/results/' sử dụng cặp ngoặc góc (<>) để “bắt” phần URL, là '1', trong đó int dùng để chuyển kiểu và question_id là một cái tên mà question_id=1 được gửi như đối số từ khóa tới hàm view, là hàm results().

Hai lớp IndexView và DetailView áp dụng cho các view trên cơ sở lớp. Để thấy rõ hơn, chúng ta thay thế lớp IndexView bằng định nghĩa dưới đây:

from django.views import View
from django.http import HttpResponse
from django.template.loader import get_template

class IndexView(View):
    latest_question_list = Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

    def get(self, request):
        template = get_template('polls/index.html')
        context = {'latest_question_list': self.latest_question_list}
        output = template.render(context, request)
        return HttpResponse(output)

Lớp này kế thừa lớp View thay cho ListView. latest_question_list là một danh sách các đối tượng câu hỏi được lọc để lấy chỉ những câu hỏi xuất bản trong quá khứ, sắp thứ tự theo ngày xuất bản nhưng đảo ngược, với tối đa 5 câu hỏi đầu tiên, tức là 5 câu hỏi gần đây nhất. Sắp thứ tự đảo ngược vì có dấu trừ (-) đằng trước pub_date.

Khởi tạo latest_question_list như thế là tương đương với

latest_question_list = sorted(Question.objects.filter(
        pub_date__lte=timezone.now()
    ), key = lambda q: q.pub_date, reverse = True)[:5]

Phương thức get() trả lời cho request mà sử dụng HTTP GET. Hàm get_template() trả về đối tượng khuôn mẫu template từ khuôn mẫu 'polls/index.html'. context là một từ điển ngữ cảnh, nó gửi ngữ cảnh tới khuôn mẫu qua hàm render() của template. Hàm này kết xuất nội dung sau khi đã xử lý ngữ cảnh. Cuối cùng, phương thức get() trả về HttpResponse với nội dung. Đó là những hoạt động rất căn bản trong xử lý khuôn mẫu và đáp ứng request. Dạng lớp IndexView ban đầu sử dụng ListView chỉ vì để thuận tiện, các chức năng phải thực hiện nằm trong lớp cơ sở nên không cần viết lại, song nguyên lý chung cũng như vậy. Tuy nhiên chúng ta vẫn dùng ListView để có thêm tính năng, chẳng hạn để chạy test sau này.

Đối với view sử dụng hàm results(), hàm get_object_or_404() lấy đối tượng câu hỏi với khóa chính nhận từ question_id truyền vào đối số. Hàm này báo lỗi 404 nếu không tìm thấy câu hỏi cho id chỉ ra. TemplateResponse là một đối tượng đáp ứng khuôn mẫu, là một phiên bản thuận tiện của HttpResponse với khả năng giữ khuôn mẫu và ngữ cảnh.

DetailView có một điểm hơi khác là nó đón nhận giá trị khóa chính bắt được từ URL gọi là “pk”, do đó chúng ta đổi từ question_id thành pk trong URLconfs.

Hàm vote() xử lý hành động biểu quyết của khách trên trình duyệt. request.POST là một đối tượng kiểu từ điển mà giữ dữ liệu submit. request.POST['choice'] trả về id của mục bình chọn được biểu quyết. Nếu mục bình chọn không được cung cấp trong dữ liệu POST, lỗi KeyError được phát sinh và xử lý quay trở lại form biểu quyết với một ngữ cảnh thông báo. Nếu không, số biểu quyết của mục bình chọn tăng 1, kết quả được giữ vào cơ sở dữ liệu rồi trả về một HttpResponseRedirect chuyển hướng tới trang kết quả. Hàm HttpResponseRedirect nhận 1 đối số, là URL mà được chuyển hướng tới. Hàm reverse() đổi ngược từ tên mẫu URL sang URL. Trong trường hợp này, sử dụng bổ sung thêm đối số từ khóa args được gán bởi một đối tượng lặp được bao gồm các giá trị xem như “bắt được” từ route (xem hàm path() bên trên), cụ thể là chỉ có một giá trị question.id để hoàn thành URL cho view mục tiêu.

Ví dụ với câu hỏi có question.id là 3, reverse('polls:results', args=[3]) sẽ trả về xâu:

'/polls/3/results/'

Rồi cuộc gọi results(request, question_id=3) được thực hiện để trình bày trang kết quả.

9. Các file tĩnh

Django gọi các file như ảnh, JavaScript, hoặc CSS là các file tĩnh.

Các file tĩnh của chương trình Polls lẽ ra nên để trong thư mục chương trình, nhưng vì chúng ta sẽ chạy server sản phẩm do đó đặt chúng luôn vào thư mục dự án. Bạn tạo thư mục static/mysite, rồi thêm các dòng dưới đây vào tệp cấu hình mysite/settings.py

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

Cấu hình đó cho Django biết nơi đặt các file tĩnh, thư mục các file tĩnh là static.

CSS

Có rất nhiều mẫu CSS để tham khảo. Ở đây chúng ta chỉ áp dụng CSS đơn giản. Bạn tạo tệp static/mysite/style.css với nội dung sau:

/* General */

body {

font-family: Liberation Serif, serif;

font-size: 14px;

color: #333333;

background-color: #F9F9F9;

}

.KeyError {

color: red;

}

p {

width: 80%;

}

h1 {

font-family: Liberation Serif, serif;

font-size: 18px;

font-weight: bold;

color: #800080;

}

h2 {

font-family: Liberation Serif, serif;

font-size: 16px;

font-weight: bold;

color: #000000;

border-bottom: 1px solid #C6EC8C;

}

h3 {

font-family: Liberation Serif, serif;

font-size: 14px;

}

li {

list-style-type: circle;

line-height: 150%;

}

/* Pseudo classes */

a:link {

color: #00cc00;

text-decoration: underline;

}

a:visited {

color: #cc00cc;

text-decoration: underline;

font-weight: bold;

}

a:hover {

color: rgb(0, 96, 255);

padding-bottom: 5px;

font-weight: bold;

text-decoration: underline;

}

a:active {

color: rgb(255, 0, 102);

font-weight: bold;

}

li a:link {

color: green;

text-decoration: none;

font-weight: bold;

}

li a:visited {

color: #800080;

text-decoration: none;

font-weight: bold;

}

li a:hover {

display: block;

color: rgb(0, 96, 255);

padding-bottom: 5px;

font-weight: bold;

border-bottom-width: 1px;

border-bottom-style: solid;

border-bottom-color: #C6EC8C;

}

/* IDs */

#sidebar {

position: absolute;

z-index: 10;

width: 200px;

height: 600px;

margin-left: 10px;

border-right: 1px solid #C6EC8C;

font-weight: normal;

}

#content {

position: absolute;

z-index: 15;

margin-left: 235px;

}

10. Tạo thẻ khuôn mẫu tùy biến

Django có các thẻ khuôn mẫu built-in. Chúng ta tạo thêm các thẻ khuôn mẫu tùy biến để bố trí khuôn mẫu theo cách riêng.

Cấu hình mặc định sử dụng backend khuôn mẫu django.template.backends.django.DjangoTemplates. DjangoTemplates cần biết vị trí các khuôn mẫu. Cũng cần các lớp tải khuôn mẫu cho các thẻ khuôn mẫu. Bạn soạn thảo tệp cấu hình mysite/settings.py, thay bộ phận TEMPLATES với nội dung dưới đây:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], 
        'OPTIONS': {
            'loaders': [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
            ],
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.i18n',
            ],
        },
    },
]

Các lớp tải khuôn mẫu biết rằng khuôn mẫu như 'polls/templates/polls/index.html' sẽ được tham chiếu như 'polls/index.html'.

Bạn tạo thư mục polls/templatetags rồi thêm tệp __init__.py rỗng vào đó. Tạo tệp polls/templatetags/blocks_tags.py có nội dung sau:

from django.template import Library
from django.template.loader import get_template

register = Library()

@register.simple_tag
def get_header():
    return get_template('polls/header.html').render()

@register.simple_tag
def get_sidebar():
    return get_template('polls/sidebar.html').render()

@register.simple_tag
def get_footer():
    return get_template('polls/footer.html').render()

Module này đăng kí các thẻ get_header, get_sidebar, get_footer. Khi module được tải vào một khuôn mẫu, những ví trí xuất hiện các thẻ này sẽ kết xuất các khuôn mẫu đã khai báo tương ứng.

11. Các khuôn mẫu

Bạn tạo thư mục polls/templates/polls, rồi lần lượt thêm vào đó các tệp khuôn mẫu có các nội dung tương ứng như dưới đây:

polls/templates/polls/index.html

{% load blocks_tags %}

{% get_header %}
{% get_sidebar %}

<div id="content">
    {% if latest_question_list %}
        <ul>
        {% for question in latest_question_list %}
            <li>
                <a href="{% url 'polls:detail' question.id %}">
                    {{ question.question_text }}
                </a>
            </li>
        {% endfor %}
        </ul>
    {% else %}
        <p>No polls are available.</p>
    {% endif %}
</div>
{% get_footer %}

{% load blocks_tags %} thực hiện tải module thẻ khuôn mẫu blocks_tags. Các thẻ get_header, get_sidebar, get_footer được sử dụng như đã nêu trên. Khuôn mẫu thực hiện lặp qua biến latest_question_list nhận được từ ngữ cảnh để trình bày các câu hỏi với một link. Thẻ {% url %} trả về URL của mẫu URL có tên 'polls:detail' với question.id tham gia vào đối số, để tạo link tới trang chi tiết của câu hỏi có id chỉ ra.

polls/templates/polls/detail.html

{% load blocks_tags %}

{% get_header %}
{% get_sidebar %}

<div id="content">
    <h1>{{ question.question_text }}</h1>

    {% if error_message %}<h3 class="KeyError">{{ error_message }}</h3>{% endif %}

    <form action="{% url 'polls:vote' question.id %}" method="post">
    {% csrf_token %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
    {% endfor %}
    <input type="submit" value="Vote" />
    </form>
</div>

{% get_footer %}

Khi khách bình chọn thực hiện biểu quyết mà chưa chọn mục bình chọn nào, hàm vote() (xem bên trên) sẽ gửi đi một ngữ cảnh mà từ đó biến error_message được dùng trong khuôn mẫu này. Bên dưới là một form nhập thông thường với các thẻ khuôn mẫu được sử dụng.

{% csrf_token %} là một thẻ an ninh, dùng để bảo vệ CSRF (Cross Site Request Forgery). Thẻ này loại trừ hành động mà không thực hiện trên form nhập từ bên trong site của bạn như thiết kế mà gửi một request giả mạo từ một site độc hại bên ngoài. Các request qua các phương thức “không an toàn” như POST, PUT, DELETE đều cần bảo vệ CSRF. (xem https://tools.ietf.org/html/rfc7231.html#section-4.2.1).

forloop.counter là một biến đếm vòng lặp, dùng để gán nhãn đúng cho từng radio trong vòng lặp.

polls/templates/polls/results.html

{% load blocks_tags %}

{% get_header %}
{% get_sidebar %}

<div id="content">
    <h1>{{ question.question_text }}</h1>

    <ul>
    {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
    {% endfor %}
    </ul>

    <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
</div>

{% get_footer %}

Khuôn mẫu này rất đơn giản. Chỉ có một vấn đề đáng nói ở đây là pluralize. Nó là một lọc khuôn mẫu, trả về hậu tố số nhiều, mặc định là 's' nếu giá trị trước lọc không phải là 1. Ví dụ nếu có 2 vote thì vote{{ choice.votes|pluralize }} tương đương với vote{{ 2|pluralize }}, và đầu ra là votes.

polls/templates/polls/header.html

{% load static %}

<!DOCTYPE html>
<html>
<head>
    <title>Polls Application</title>
    <link rel="stylesheet" type="text/css" href="{% static 'mysite/style.css' %}" />
</head>

<body>
    <h2> Sample Python: Django Project </h2>

Thẻ khuôn mẫu {% static %} tạo ra URL tuyệt đối cho các file tĩnh.

polls/templates/polls/sidebar.html

<div id="sidebar">
    <p><a href= "{% url 'home' %}">Home</a></p>
    <p><a href= "/docs/">Documentation</a></p>
    <p><a href= "{% url 'polls:index' %}">Polls</a></p>
    <p><a href= "/admin/">Admin</a></p>
</div>

polls/templates/polls/footer.html

    </body>
</html> 

Ngoài ra, chúng ta tạo một khuôn mẫu cho trang đầu. Tạo thư mục templates, sau đó tạo file templates/index.html có nội dung sau:

{% load blocks_tags %}

{% get_header %}
{% get_sidebar %}

<div id="content">
    <h1>Welcome!</h1>
    <p>
    This is a sample project on Django using the Python programming language. 
    We learn a lot: Modeling and database manipulation, designing the view and 
    URL mapping, template and custom template tags, using secure form, 
    customizing the admin site, using static files, and more.
    </p>
</div>

{% get_footer %}

12. Chạy server

Hiện tại, chúng ta chạy server phát triển của Django trên cổng 8000:

python manage.py runserver

Bạn mở trình duyệt, vào địa chỉ http://localhost:8000, trang đầu xuất hiện như sau:

Nhấn chuột vào Polls trên sidebar, lúc này chương trình chỉ có 1 câu hỏi do chúng ta đã tạo ra khi chơi API:

Nhấn chuột vào “What’s up?”, chi tiết câu hỏi sẽ được trình bày:

Nếu bạn không chọn một mục mà “Vote” ngay, chương trình sẽ báo lỗi:

Tất cả đều là nguồn mở, bạn thoải mái sửa mã. Bây giờ chúng ta biểu quyết cho bình chọn “Not much”:

Advertisements

Gửi phản hồi

Website này sử dụng Akismet để hạn chế spam. Tìm hiểu bình luận của bạn được duyệt như thế nào.

%d bloggers like this: