Daily Archives: 28/03/2016

Django – File tĩnh

Trong phần này chúng ta sẽ tìm hiểu cách sử dụng các file tĩnh.

Ngoài nội dung HTML được sinh ra bởi server thì một ứng dụng web còn cần đến các file bổ sung khác, chẳng hạn như các file hình ảnh, Javascript, CSS… Trong Django thì các file này được gọi là file tĩnh.

Module django.contrib.staticfiles được liệt kê trong biến INSTALLED_APP (trong file mysite/settings.py) sẽ quản lý các file tĩnh này.

Tùy biến CSS

Đầu tiên chúng ta tạo một thư mục có tên là static trong thư mục polls. Django sẽ tự động tìm các file tĩnh trong thư mục này giống như tìm các file template trong thư mục polls/templates vậy.

Trong thư mục static này chúng ta lại tạo một thư mục khác với tên là polls và tạo một file CSS có tên là style.css.

li a {
    color: green
}

Trong file này chúng ta chỉ đơn giản là thiết lập màu chữ cho các thẻ li và a.

Tiếp theo chúng ta cần sửa lại file index.html một tí.

{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" /> 

Dòng {% load staticfiles %} sẽ tự động gán đường dẫn đến thư mục mysite/polls/static/ vào một biến có tên static do Django tự đặt, từ đó bạn chỉ cần lấy biến static là có thể lấy được đường dẫn tuyệt đối đến thư mục này.

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" /> 

Sau đó chúng ta chỉ cần lấy biến static trong cặp thẻ {% %} ra và gắn thêm đường dẫn đến các file css, js… của bạn.

Django – Form và các View có sẵn

Trong phần này chúng ta sẽ viết form cho các hàm view, sau đó tìm hiểu về hệ thống view có sẵn của Django.

Tạo form

Chúng ta sẽ viết lại template detail.


<h1>{{ question.question_text }}</h1>

{% if error_messsage %}


<strong>{{ error_messsage }}</strong>

{% 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>
    {% endfor %}
    <input type="submit" value="Vote" />
</form>

Chúng ta tạo một form cho phép người dùng vote các câu trả lời.

{% csrf_token %}

Bạn chỉ cần biết câu lệnh trên sẽ giúp website của chúng ta chống lại kiểu tấn công CSRF là đủ, nếu muốn bạn có thể tìm hiểu thêm trên mạng, ở đây mình không đi sâu.


<form action="{% url 'polls:vote' question.id %}" method="post">
...
</form>

Chúng ta tạo thẻ form và đưa url dẫn đến view vote() với tham số question.id là ID của đối tượng Choice trong CSDL, phương thức gửi lên là phương thức POST. Nếu bạn chưa biết sự khác nhau giữa GETPOST thì cứ nhớ là nếu gửi dữ liệu lên server mà có thay đổi CSDL thì phải dùng phương thức POST.

{% 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>
{% endfor %}

Chúng ta hiển thị các radio button, mỗi radio button hiển thị một câu trả lời (Choice) cho từng Question. Các radio button sẽ được gán thuộc tính namechoiceThuộc tính forloop.counter là số thứ tự của vòng lặp giống như khi bạn viết for(int i...) trong C++ vậy.

Tiếp theo chúng ta sẽ viết lại hàm view vote() xử lý từng thao tác vote.

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.core.urlresolvers import reverse

from .models import Question, Choice

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):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
    return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Hàm view vote() sẽ xử lý việc vote các câu trả lời.

question = get_object_or_404(Question, pk=question_id)

Khác với phần trước, chúng ta phải dùng khối lệnh try...except để giải phóng một lỗi 404, ở đây chúng ta có thể dùng hàm get_object_or_404(), hàm này sẽ chạy phần try và sẽ tự giải phóng một lỗi 404 luôn cho chúng ta nếu có.

try:
    selected_choice = question.choice_set_get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
    return render(request, 'polls/detail.html', {
        'question': question,
        'error_message': "You didn't select a choice.",
    })

Khi một hàm view được gọi, chúng ta có thể lấy các dữ liệu được gửi kèm theo qua đối tượng request, tùy vào phương thức được gọi và tên biến được đặt mà chúng ta lấy ra cho phù hợp, nếu không sẽ có lỗi ngoại lệ xảy ra, chẳng hạn như KeyError là lỗi sai tên biến, có thể tên biến là choice mà bạn gửi lên theo url là choiec hay chce…, lỗi DoesNotExist là lỗi đối tượng không tồn tại…

else:
    selected_choice.votes += 1
    selected_choice.save()
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Nếu không có lỗi xảy ra thì chúng ta tăng thuộc tính votes trong đối tượng Choice lên 1 và lưu vào CSDL. Sau đó trả về một đối tượng HttpResponseRedirect chứ không dùng đối tượng HttpResponse như thường để tránh các trường hợp người dùng nhấn nút back trên trình duyệt (bạn vẫn có thể dùng HttpResponse nếu muốn), HttpResponseRedirect nhận một đối tượng url thông qua hàm reverse(), hàm này sẽ trả về một đường dẫn có dạng như /polls/1/results/.

Tiếp theo chúng ta sẽ viết lại hàm view result().

from django.shortcuts import get_object_or_404, render
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id) 
    return render(request, 'polls/results.html', {'question': question})

View results() sẽ hiển thị số lượng vote của mỗi câu trả lời Choice.

Hàm này khá đơn giản, chúng ta chỉ lấy về đối tượng Question và gửi đến template results.html ở dưới để hiển thị.

<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>

Đoạn code HTML trên cũng rất đơn giản, chúng ta chỉ lấy từng đối tượng Choice và in tên cùng với số lượng votes của chúng ra, ngoài ra chúng ta còn thêm một đường dẫn về trang detail. Tham số pluralize sẽ thêm kí tự ‘s‘ để biểu thị số nhiều trong tiếng Anh.

Các lớp View của Django

Các hàm view mà chúng ta đã viết như index(), detail()result() rất đơn giản… và rất thừa.

Thường thì các trang web được viết ra đều làm một công việc gần như là tương tự nhau, đó là lấy dữ liệu từ CSDL dựa vào lệnh request được gửi lên thông qua URL, sau đó tải template và gắn dữ liệu đó vào template rồi trả về cho người dùng. Vì tính chất lặp đi lặp lại như thế nên Django cung cấp cho chúng ta hệ thống View có sẵn.

Các View này là các lớp của Django, mục đích chính của các lớp này là giúp bạn đỡ phải viết code Python nhiều, mặc dù thực ra thì cũng có nhiều trường hợp mà việc tự viết các View sẽ tốt hơn là dùng View có sẵn, cũng giống như việc học lập trình vậy, bạn phải biết viết các thuật toán trước khi sử dụng thư viện có sẵn.

Chúng ta sẽ sửa lại ứng dụng polls để sử dụng các lớp View này.

Đầu tiên chúng ta phải sửa lại các đường dẫn url một tí.

from django.conf.urls import url

from . import views

app_name = "polls"
urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
    url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Tên 3 hàm view được thay đổi và tên biến tham số gửi lên cũng được thay đổi từ question_id thành pk.

Tiếp theo chúng ta sẽ viết lại các hàm view mới.

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views import generic

from .models import Choice, Question

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

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]

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

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

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):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Chúng ta sẽ sử dụng các lớp View có sẵn của Django ngoại trừ hàm vote() là giữ nguyên.

from django.views import generic

Đầu tiên chúng ta import module generic.

class IndexView(generic.ListView):
    ...

class DetailView(generic.DetailView):
    ...

Chúng ta sẽ sử dụng 2 loại view là ListViewDetailView, ListView lưu trữ danh sách các đối tượng, DetailView lưu thông tin về một đối tượng cụ thể.

model = Question 

Mỗi lớp View cần biết về mô hình dữ liệu mà nó sẽ lưu trữ thông qua thuộc tính model. Khi đã biết loại model mà mình sẽ dùng, các view này tự động “nghĩ” rằng khóa chính trong CSDL có tên là pk, do đó trong các đối tượng url chúng ta gửi lên tham số có tên là pk.

template_name = 'polls/detail.html'

Mặc định các lớp View này cũng “nghĩ” rằng template sẽ được dùng có dạng <app_name>/<model_name>_detail.html (hoặc <app_name>/<model_name>_list.html), nếu không có file nào như vậy tồn tại thì Django sẽ tự động tạo các file này và sử dụng, chúng ta có thể “bảo” Django sử dụng template do chúng ta tự viết bằng cách gán vào thuộc tính template_name, như thế Django sẽ không tạo template cho nó nữa.

class IndexView(generic.ListView):
    ...
    context_object_name = 'latest_question_list'

Trong hàm detail() cũ, chúng ta tự khai báo các biến questionlatest_question_list để dùng trong template, mặc định các biến này đã có sẵn trong các lớp DetailView. Tuy nhiên đối với hàm ListView thì biến mặc định lại có tên là question_list, nên để Django dùng tên do chúng ta tự đặt thì chúng ta phải gắn tên đó vào thuộc tính context_object_name hoặc bạn phải dùng tên do Django đặt trong các file template.