Category Archives: Python

Django – Kiểm tra dữ liệu gửi lên form

Trong bài này chúng ta sẽ tìm hiểu cách kiểm tra sự đúng đắn của dữ liệu.

Kiểm tra dữ liệu ở đây là kiểm tra xem dữ liệu được gửi lên có đúng với yêu cầu hay không, cũng có thể xem đây là câu lệnh catch trong quá trình kiểm tra lỗi exception vậy, chẳng hạn như chúng ta yêu cầu người dùng nhập vào email nhưng người dùng nhập sai cấu trúc, hay upload ảnh avatar mà lại gửi lên một file PDF, nguy hiểm hơn nữa là up file ảnh giả – tức file có đuôi .jpg, .png… nhưng lại không chứa dữ liệu ảnh mà chứa mã độc… vì thế chúng ta nên kiểm tra dữ liệu được gửi lên trước khi lưu chúng vào CSDL, và Django cũng cung cấp một số phương thức/hàm hỗ trợ chúng ta làm việc này.

Django có một số phương thức kiểm tra dữ liệu có sẵn trong các lớp FieldForm, các phương thức này khi kiểm tra nếu dữ liệu đúng thì trả lại dữ liệu đó, nếu sai thì sẽ trả về một lỗi exception là ValidationError.

Chúng ta đã từng dùng phương thức is_valid() của lớp django.forms.form, phương thức này khi được gọi sẽ tự động gọi các phương thức kiểm tra và dữ liệu nào không có lỗi thì sẽ được lưu trong thuộc tính form.cleaned_data dưới dạng một đối tượng Dictionary, bạn vẫn có thể truy xuất dữ liệu gốc trong thuộc tính request.POST.

Các phương thức kiểm tra khi được gọi sẽ thực hiện kiểm tra dữ liệu theo cách của Django nhưng chúng ta cũng nên override lại các phương thức kiểm tra để chúng làm theo yêu cầu của chúng ta. Chúng ta sẽ tìm hiểu về các phương thức này ở dưới.

Validator

Các lớp Field của Django hỗ trợ một công cụ tiện ích có tên là validatorCác validator sẽ kiểm tra dữ liệu của lớp đó theo một quy luật nào đó và trả về lỗi ValidationError nếu có. Chúng ta có thể định nghĩa validator bằng cách truyền vào ngay hàm khởi tạo Field hoặc định nghĩa bên trong các lớp Field riêng với thuộc tính default_validators. Ví dụ:

from django.forms import CharField
from django.core import validators

class SlugField(CharField):
    default_validators = [validators.validate_slug]

Chúng ta định nghĩa lớp SlugField kế thừa từ CharField, thuộc tính default_validators sẽ quy định kiểu kiểm tra dữ liệu của lớp này.

>>> slugExample = forms.SlugField()
>>> slugExample2 = forms.CharField(validators=[validators.validate_slug])

Cả 2 cách tạo đối tượng trên sẽ sử dụng chung validator.

validators.validate_slug

Validators là một module của Django, module này chứa các lớp như EmailValidator, RegexValidatorURLValidator, Min/MaxValueValidator cho phép chúng ta định nghĩa các luật kiểm tra khác nhau. Ngoài các lớp đó Djang còn chứa sẵn một số đối tượng validator tĩnh quy định sẵn các luật thông dụng, ở ví dụ trên chúng ta dùng validate_slug, đối tượng này quy định chuỗi chỉ được phép chứa kí tự chữ cái, kí tự số, dấu gạch ngang và dấu nối _. Ngoài validate_slug ra còn có các đối tượng khác như validate_email, validate_unicode_slug… bạn có thể xem danh sách tại đây.

Các phương thức kiểm tra dữ liệu

Django cung cấp các hàm/phương thức kiểm tra dữ liệu sau đây:

Phương thức kiểm tra trên Field:

  • Phương thức to_python() có sẵn trong mỗi lớp Field, phương thức này sẽ chuyển đổi kiểu dữ liệu field tương ứng sang kiểu dữ liệu của Python, nếu chuyển được thì không có gì, nếu không sẽ có lỗi ValidationError. Ví dụ như khi chuyển một FloatField thì chúng ta sẽ được một đối tượng float hoặc một lỗi exception.
  • Phương thức validate() có sẵn trong mỗi lớp Field, thường được dùng khi không có sẵn các validator.
  • Phương thức run_validators() có trong mỗi lớp Field tự động kiểm tra tất cả các validator của Field và cái nào có lỗi thì gom lại vào một đối tượng ValidationError. Thường chúng ta không dùng phương thức này.
  • Phương thức clean() có trong các lớp kế thừa từ lớp Field sẽ gọi các phương thức to_python(), validate()run_validators() trên field theo thứ tự và nếu gặp lỗi tại đâu thì dừng quá trình kiểm tra và giải phóng exception tại đó. Nếu dữ liệu hợp lệ thì sẽ được truyền vào thuộc tính cleaned_data của form.

Phương thức kiểm tra trên Form:

  • Phương thức clean_<tên thuộc tính field>() được Django tạo ra trong các lớp kế thừa từ lớp Form, trong đó <tên thuộc tính field> là tên do chúng ta đặt cho từng thuộc tính field khi định nghĩa form. Phương thức này kiểm tra dữ liệu theo yêu cầu chứ không quan tâm đến kiểu dữ liệu. Chẳng hạn như form của chúng ta có một thuộc tính tên là serialnumber có kiểu CharField và thuộc tính này phải là duy nhất – tức là không có 2 bản ghi nào có serialnumber giống nhau, thì việc kiểm tra sẽ được thực hiện trong phương thức clean_serialnumber() và chúng ta phải override lại phương thức này.

  • Phương thức clean() có trong các lớp kế thừa từ lớp Form kiểm tra dữ liệu trên từng field, chúng ta sẽ override lại khi cần dùng. Phương thức này được gọi sau khi các phương thức kiểm tra trên Field đã thực thi.

Khi phương thức is_valid() được gọi, từng thuộc tính field mà chúng ta khai báo theo thứ tự từ trên xuống dưới khi định nghĩa Form sẽ gọi từng phương thức kiểm tra đã nêu trên, phương thức Field.clean() sẽ được gọi đầu tiên, sau đó là Form.clean_<tên thuộc tính field>(), cuối cùng là phương thức Form.clean(), phương thức Form.clean() sẽ luôn được gọi bất kế các phương thức trước đó có trả về lỗi hay không.

Ví dụ

Chúng ta sẽ override một số phương thức đã nói trên.

from django import forms
from django.core.validators import validate_email

class MultiEmailField(forms.Field):
    def to_python(self, value):        
        if not value:
            return []
        return value.split(',')

    def validate(self, value):   
        super(MultiEmailField, self).validate(value)

Chúng ta định nghĩa lớp MultiEmailField kế thừa từ lớp Field. Ở đây chúng ta override lại 2 phương thức to_python()validate().

if not value:
    return []
return value.split(',')

Trong phương thức to_python(), chúng ta chỉ đơn giản là kiểm tra xem dữ liệu có rỗng hay không, nếu không thì trả về một list các string tách ra từ string gốc dùng dấu phẩy.

super(MultiEmailField, self).validate(value)

Trong phương thức validate(), chúng ta gọi lại phương thức validate() của lớp cha là lớp Field. 

Bây giờ chúng ta định nghĩa lớp ContactForm và override một số phương thức kiểm tra của Form.

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    recipients = MultiEmailField()
    cc_myself = forms.BooleanField(required=False)

    def clean_recipients(self):
        data = self.cleaned_data['recipients']
        if "fred@example.com" not in data:
            raise forms.ValidationError("You have forgotten about Fred!")

        return data

    def clean(self):
        cleaned_data = super(ContactForm, self).clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject:
            if "help" not in subject:
                raise forms.ValidationError(
                    "Did not send for 'help' in the subject despite "
                    "CC'ing yourself."
                )

Chúng ta override 2 phương thức là clean_recipients()clean().

recipients = MultiEmailField()

Chúng ta sử dụng lớp MultiEmailField như các lớp field bình thường. Khi phương thức is_valid() được gọi thì các phương thức to_python()validate() của từng field sẽ được gọi, bất kể có override hay không.

def clean_recipients(self):
    data = self.cleaned_data['recipients']     
    if "fred@example.com" not in data:
        raise forms.ValidationError("You have forgotten about Fred!")
    return data   

Sau khi các phương thức to_python()validate() đã được gọi thì Django sẽ lưu các dữ liệu đã được kiểm tra là hợp lệ trong thuộc tính cleaned_data. Chúng ta có thể lấy ra để kiểm tra tiếp.  Trong đoạn code trên chúng ta kiểm tra nếu fred@example.com không có trong list thì giải phóng lỗi ValidationError.

def clean(self):
    cleaned_data = super(ContactForm, self).clean()
    ...
    if cc_myself and subject:
        if "help" not in subject:
            raise forms.ValidationError(
                "Did not send for 'help' in the subject despite "
                "CC'ing yourself."
            )

Phương thức clean() khá đơn giản, chúng ta chỉ kiểm tra xem nếu cc_myselfsubject không rỗng thì trong subject phải có chứa chuỗi “help”, nếu không thì báo lỗi. Ngoài ra trước đó chúng ta cũng gọi lại phương thức clean() của lớp cha.

Django – Tạo form từ model

Giả sử bạn xây dựng một ứng dụng có sử dụng cơ sở dữ liệu – tức là bạn phải có định nghĩa các lớp model, thường thì bạn sẽ phải tạo các form cho người dùng nhập dữ liệu và các form này thường cũng có các field gần như là giống hoàn toàn với các field có trong model, ví dụ như trong một ứng dụng Blog bạn có một model tên là BlogComment có các field lưu trữ thông tin các comment do người dùng nhập vào, vậy thì bạn cũng phải tạo form để người dùng nhập comment vào bài viết.

Nhưng việc định nghĩa một lớp Model và một lớp Form có các field gần như giống nhau hoàn toàn là rất vô nghĩa, mà lại còn viết từ code HTML hoặc cùng lắm là kế thừa từ lớp Form của Django đi nữa thì code vẫn rất mệt mỏi, vì thế nên Django có thể tự định nghĩa luôn các lớp form giùm bạn từ các lớp model mà bạn đã định nghĩa trước, tức là trong model có các field nào thì Django cũng sẽ tự tạo lớp form có các field tương ứng như thế, bạn không cần phải định nghĩa lại nữa.

Kiểu dữ liệu

Mỗi lớp Field của Model sẽ có một lớp Field của Form tương ứng. Dưới đây là bảng tên các lớp Field tương ứng của ModelForm.

Lưu ý là các lớp ModelField hỗ trợ 3 kiểu khóa ngoại là 1-n, n-n1-1 nhưng bên FormField chỉ có 2 kiểu duy nhất được hỗ trợ là ModelChoiceField tương ứng với ForeignKeyModelMultipleChoiceField tương ứng với ManyToManyField. Cả 2 lớp trên đều nhận một tham số khi khởi tạo là một đối tượng QuerySet.

Mỗi lớp Form Field có chung một số thuộc tính như sau:

  • required: nếu thuộc tính blank của ModelTrue thì required = False và ngược lại.
  • label: thuộc tính này được gán tự động bằng thuộc tính verbose_name của Model và kí tự đầu được viết hoa.
  • help_text: thuộc tính này được gán tự động bằng thuộc tính help_text của model.

Ví dụ

Chúng ta định nghĩa các lớp như sau.

from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES)
    birth_date = models.DateField(blank=True, null=True)
   
class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']

Chúng ta định nghĩa 2 lớp ModelAuthorBook, sau đó định nghĩa 2 lớp Form có các Field được tạo tự động từ 2 lớp model là AuthorFormBookForm. Các lớp này được kế thừa từ lớp django.forms.ModelForm thay thì lớp django.forms.Form như trước.

class Meta:
    model = Author
    fields = ['name', 'title', 'birth_date']

Chúng ta định nghĩa model được dùng trong lớp nội Meta, thuộc tính model sẽ tạo các field tương ứng từ lớp Model tương ứng, thuộc tính fields sẽ chọn các trường nào được dùng, nếu chúng ta không khai báo thuộc tính fields thì mặc định Django sẽ dùng tất cả các thuộc tính có trong lớp Model, ngoại trừ thuộc tính id nếu bạn không khai báo thuộc tính khóa chính. Ngoài ra lớp Meta còn có thuộc tính exclude, thuộc tính này trái ngược với thuộc tính fields, tức là thuộc tính này sẽ quy định các trường nào không được phép sử dụng.

Định nghĩa từ lớp ModelForm tương đương với định nghĩa từ lớp Form trong các bài trước như sau:

from django import forms

class AuthorForm(forms.Form):
    name = forms.CharField(max_length=100)
    title = forms.CharField(max_length=3,
    widget=forms.Select(choices=TITLE_CHOICES))
    birth_date = forms.DateField(required=False)

class BookForm(forms.Form):
    name = forms.CharField(max_length=100)
    authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all())

Lưu dữ liệu

Lớp ModelForm có một phương thức tên là save(), phương thức này tạo mới hoặc lưu một đối tượng Model vào cơ sở dữ liệu giống như phương thức save() bên các lớp Model vậy.

Khi khởi tạo các đối tượng ModelForm thì chúng ta có thể truyền các đối tượng model có sẵn vào hàm khởi tạo, nếu chúng ta chỉ truyền vào không thôi thì Django sẽ tạo mới một đối tượng trên CSDL, nếu chúng ta truyền vào và ghi rõ truyền với tham số là instance thì Django sẽ cập nhật dữ liệu trong đối tượng đó. Ví dụ:

>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm
>>> f = ArticleForm(request.POST)
>>> new_article = f.save()

Đoạn code trên tạo đối tượng mới từ đối tượng POST được gửi lên và lưu vào CSDL.

>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()

Đoạn code trên tạo một đối tượng ArticleForm, truyền dữ liệu từ POST vào rồi truyền cập nhật trên đối tượng a chứ không tạo mới.

Lưu ý là nếu chúng ta không kiểm tra sự đúng đắn của dữ liệu thì khi gọi phương thức save(), Django cũng sẽ tự động làm việc đó và sẽ giải phóng lỗi exception ValueError nếu dữ liệu có lỗi, trong bài trước chúng ta đã dùng một phương thức để kiểm tra dữ liệu là is_valid(), chúng ta sẽ tìm hiểu thêm về cách kiểm tra dữ liệu trong các bài sau.

Tùy chỉnh lớp Field

Như trong bảng tên các lớp ModelFieldFormField tương ứng đã chỉ rõ ở trên, nếu bạn khai báo loại field nào trong lớp model thì Django sẽ tạo lớp field tương ứng bên form. Chẳng hạn như khi bạn khai báo trường DateTimeField bên model thì Django sẽ tạo một trường DateTimeField tương ứng bên form.

Mặc dù tạo form bằng cách code từ đầu với HTML hoặc dùng lớp django.forms.Form sẽ cho bạn quyền điều khiển nhiều hơn nhưng không có nghĩa là ModelForm không cho phép bạn tùy chỉnh các lớp field có sẵn.

Chẳng hạn như để tùy chỉnh kiểu hiển thị thì chúng ta dùng thuộc tính widgets có trong lớp nội Meta. Ví dụ:

from django.forms import ModelForm, Textarea
from myapp.models import Author

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        widgets = {
            'name': Textarea(attrs={'cols': 80, 'rows': 20}),
        }

Mặc định lớp CharField sẽ hiển thị thẻ <input type="text">, nhưng đoạn code trên sẽ chỉ định CharField hiển thị <input type="textarea">.

Tương tự bạn có thể tùy chỉnh lại các thuộc tính như label, help_text trong lớp nội Meta nếu muốn:

from django.utils.translation import ugettext_lazy as _

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        labels = {
            'name': _('Writer'),
        }
        help_texts = {
            'name': _('Some useful help text.'),
        }

Bạn cũng có thể quy định các thuộc tính phải dùng lớp field do bạn định nghĩa thông qua thuộc tính field_classes trong lớp Meta:

from django.forms import ModelForm
from myapp.models import Article

class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
        field_classes = {
            'pub_date': MyDateTimeField,
        }

class MyDateTimeField():
    #...
    pass

Thừa kế Form

ModelForm cũng chỉ là một lớp Python, do đó bạn có thể cho thừa kế và mở rộng các trường hoặc các phương thức của chúng. Ví dụ:

class EnhancedArticleForm(ArticleForm):
    def clean_pub_date(self):
        #... 
        pass

Lớp EnhancedArticleForm kế thừa từ lớp ArticleForm nên sẽ có tất cả các thuộc tính và phương thức của lớp ArticleForm, ngoài ra lớp EnhancedArticleForm còn có phương thức clean_pub_date() của riêng nó nữa.

Tương tự với các lớp Model, các lớp form cũng có thể thừa kế cả lớp nội Meta:

class RestrictedArticleForm(EnhancedArticleForm):
    class Meta(ArticleForm.Meta):
        exclude = ('body',)

Lớp RestrictedArticleForm kế thừa từ lớp EnhancedArticleForm ngoại trừ lớp này không sử dụng thuộc tính body.

Lưu ý là khi sử dụng đa thừa kế thì lớp Meta của lớp con chỉ thừa kế từ lớp Meta của lớp cha đầu tiên trong danh sách thôi.

Hàm factory

Nếu bạn quá “lười” để ngồi định nghĩa lại một lớp kế thừa từ ModelForm, sau đó ngồi khai báo các thuộc tính fields, exclude hay lớp Meta thì Django cũng cung cấp một hàm cho phép bạn tạo một lớp ModelForm một cách “cấp tốc” là hàm modelform_factory(), ví dụ:

from django.forms import modelform_factory
from myapp.models import Book

BookForm = modelform_factory(Book, fields=("author", "title"))

Hàm này đặc biệt hữu dụng khi lớp Form mà bạn muốn định nghĩa không có gì khác nhiều so với lớp cha.

Bạn cũng có thể đưa các thuộc tính chỉnh sửa vào ngay trong hàm modelform_factory():

from django.forms import Textarea
Form = modelform_factory(Book, form=BookForm, widgets={"title": Textarea()}, fields=['name'])

Django – Upload file

Trong phần này chúng ta sẽ tạo form thực hiện chức năng upload file lên thư mục gốc của server.

Các hàm view mà chúng ta đã viết đều nhận một tham số đầu vào là một đối tượng HttpRequest, đối tượng này lưu trữ những thông tin về dữ liệu được gửi từ người dùng lên server, ví dụ như khi bạn nhập localhost:8000/ thì trình duyệt sẽ tạo một đối tượng lưu trữ những thông tin chẳng hạn như phương thức gửi lên, địa chỉ ip, url, ngày giờ gửi… rồi nén tất cả những thông tin đó lại và gửi đến server của chúng ta qua giao thức HTTP. Khi dữ liệu được gửi đến, server Django sẽ tạo một đối tượng thuộc lớp HttpRequest và đưa những thông tin được gửi đến vào đối tượng này rồi truyền vào làm tham số cho hàm view tương ứng, trong hàm view đó chúng ta có thể lấy các thông tin từ đối tượng HttpRequest này để xử lý.

Bên trong lớp HttpRequest có một thuộc tính tên là FILES, thuộc tính này lưu trữ những thông tin về file được gửi lên để chúng ta xử lý. Ngoài ra nếu muốn bạn có thể tìm hiểu các thuộc tính khác ở đây.

Bạn cũng nên lưu ý là vì một số vấn đề bảo mật nên bạn nên cấu hình server chỉ cho phép nhận các file có dung lượng giới hạn tại mức nào đó thôi, ví dụ như chỉ cho phép nhận từ 2 đến 5 MB đối với như file ảnh… Mình sẽ đề cập đến vấn đề này trong bài khác.

Tạo form upload file

Chúng ta tạo một app mới với tên là file_uploader:

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

Chúng ta khai báo app cho project.

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

Kế tiếp chúng ta tạo file forms.py bên trong thư mục file_uploader:

from django import forms

class UploadFileForm(forms.Form):
    title = forms.CharField(max_length=50)
    file = forms.FileField()

Để hiển thị thẻ element chọn file thì chúng ta dùng lớp FileField().

Tiếp theo chúng ta tạo thư mục templates và file template:


<form action="" method="POST" enctype="multipart/form-data"> 
    {% csrf_token %} 
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit" value="Submit" />
</form>

Để gửi file lên thì trong thẻ <form> chúng ta phải khai báo thuộc tính enctype="multipart/form-data".

Sau khi đã có template và form để hiển thị thì chúng ta tiến hành tạo view để kết nối chúng với nhau.

from django.db import models

# Create your models here.
from django.http import HttpResponse
from .forms import UploadFileForm

def fileUploaderView(request):
    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            upload(request.FILES['file'])
            return HttpResponse("<h2>File uploaded successful!</h2>")
        else:
            return HttpResponse("<h2>File uploaded not successful!</h2>")

    form = UploadFileForm()
    return render(request, 'fileUploaderTemplate.html', {'form':form})
 
def upload(f): 
    file = open(f.name, 'wb+') 
    for chunk in f.chunks():
        file.write(chunk)

Chúng ta viết hàm view fileUploaderView() và hàm xử lý việc ghi file là upload().

if form.is_valid():

Phương thức is_valid() có trong lớp django.forms.Form sẽ kiểm tra tính hợp lệ của dữ liệu được gửi lên. Mục đích chính của lớp Form trong Django chính là hỗ trợ chúng ta kiểm tra sự đúng đắn của dữ liệu được gửi lên. Chẳng hạn như khi bạn xây dựng chức năng up ảnh avatar thì bạn chỉ cho phép user gửi các file ảnh lên thôi, nhưng kẻ tấn công có thể gửi các file ảnh “giả” lên mà trình duyệt không biết, do đó khi dữ liệu được gửi tới server, chúng ta nên kiểm tra trước các dữ liệu này trước khi lưu vào hệ thống.

Ở đây phương thức is_valid() chỉ có chức năng đơn giản là kiểm tra xem ô text title có dữ liệu hay không vì mặc định các field của Form trong Django bắt buộc phải có dữ liệu mới được nhận.

upload(request.FILES['file'])
return HttpResponse("<h2>File uploaded successful!</h2>")

Nếu dữ liệu là hợp lệ thì chúng ta tiến hành lưu file vào thư mục gốc của server và trả lời thông báo gửi thành công về cho người dùng.

def upload(f): 
    file = open(f.name, 'wb+') 
    for chunk in f.chunks():
        file.write(chunk)

Hàm upload() sẽ thực hiện copy file vào thư mục gốc của server (thư mục có file manage.py). 

Cuối cùng chúng ta định nghĩa các url. Chúng ta sẽ tạo url trỏ tới hàm view trong app.

from django.conf.urls import url

from . import views

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

Tiếp theo là url từ địa chỉ server tới app:

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^file-upload/', include('file_uploader.urls'))
]

Bây giờ chúng ta có thể chạy server và trỏ tới url localhost:8000/file-upload để thực hiện upload file.

Untitled

File được upload thành công.

Untitled

Thiết lập file

Khi Django server nhận file được gửi đến, trước khi lưu vào đĩa cứng thì dữ liệu của file được Django lưu vào đâu đó rồi mới thực sự được lưu trên đĩa.

Mặc định nếu kích thước file nhỏ hơn 2.5 MB thì Django sẽ lưu toàn bộ dữ liệu của file trong RAM, khi lưu vào đĩa thì Django chỉ cần đọc lại nội dung đó trong RAM rồi lưu vào đĩa thôi, do đó tốc độ lưu file rất nhanh.

Tuy nhiên nếu file tải lên quá lớn, Django sẽ lưu toàn bộ file này vào thư mục temp của hệ thống, trên Windows 7 thì thư mục này nằm tại địa chỉ C:\Users\<username>\AppData\Local\Temp rồi sau đó mới copy từ thư mục đó sang thư mục của serverMà tốc độ đọc ghi với đĩa cứng lại chậm hơn RAM nên giả sử chúng ta có 2 file có dung lượng 2.4 và 2.5 MB thì thời gian up file có dung lượng 2.4 MB không khác gì file có dung lượng 2.5 MB, nhưng thời gian upload file có dung lượng 2.5 MB với file có dung lượng 2.6 MB lại khác nhau rất nhiều.

Tuy nhiên bạn có thể thiết lập lại một số thông tin cấu hình về file nhưng mình không đi sâu ở đây, nếu muốn bạn có thể xem thêm ở đây.

Django – Form có sẵn của Django

Trong bài Form và các View có sẵn chúng ta đã tạo form bằng các thẻ HTML, nếu việc code các form bằng tay như thế mệt mỏi quá thì Django cung cấp cho chúng ta lớp Form để chúng ta có thể tạo form ngay từ code Python và có thể nhận request và trả response về cho người dùng.

Trong HTML thì form là một tập các thẻ element nằm giữa cặp thẻ <form>...</form> cho phép người dùng thực hiện một công việc nào đó, sau đó gửi dữ liệu lên server rồi trả về câu trả lời. Hầu hết HTML chỉ hỗ trợ các thẻ element cơ bản như text, checkbox… muốn hiển thị các element cao cấp hơn như DateTimePicker (chọn ngày tháng), Slider (thanh trượt)… bạn phải dùng đến các ngôn ngữ hỗ trợ thêm khác như Javascript, CSS…

Hoặc bạn có thể dùng lớp Form có sẵn của Django, Django cung cấp lớp Form hỗ trợ tạo form một cách nhanh chóng và dễ dàng. Trong phần này chúng ta sẽ tạo form đăng ký user đơn giản sử dụng lớp Form do Django cung cấp.

Tạo app

Đầu tiên chúng ta tạo một app mới với tên là user_auth:

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

Tiếp theo chúng ta khai báo app này trong list INSTALLED_APP:

...
INSTALLED_APPS = [
    'user_auth',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
...

Tạo Form

Trong thư mục user-auth chúng ta tạo một file mới với tên là forms.py để lưu các lớp form.

from django import forms

class RegisterForm(forms.Form):
    username = forms.CharField(label='Username', max_length=100)
    password = forms.CharField(widget=forms.PasswordInput)
    email = forms.EmailField(label='Email')

Các lớp dùng để tạo form được kế thừa từ lớp django.forms.Form.

username = forms.CharField(label='Username', max_length=100)
password = forms.CharField(widget=forms.PasswordInput)
email = forms.EmailField(label='Email')

Bên trong lớp này chúng ta cũng khai báo các trường là các đối tượng Field, nhưng các đối tượng Field này không giống như Field khi tạo model, Field ở đây là để tạo form HTML còn field bên model là để tạo bảng CSDL (hơi rối :()

Mỗi loại lớp Field nhận vào vài tham số khi khởi tạo nhưng không có tham số nào là bắt buộc phải có, chẳng hạn như label của CharField sẽ được dùng cho thuộc tính name trong thẻ <label>...

Ở trên chúng ta có 2 CharField dùng để nhập text và một EmailField dùng để nhập email. Đây là danh sách các lớp và tham số của form Field của Django.

password = forms.CharField(widget=forms.PasswordInput)

Vì HTML có nhiều thẻ element có công dụng chung nhưng lại hiển thị dữ liệu khác nhau, chẳng hạn như một thẻ <input> có thể dùng để nhập tên, số điện thoại, password… do đó Django cung cấp cho mỗi đối tượng Field một đối tượng Widget để chúng ta có thể chỉ định loại text nào hiển thị cái gì.

Ở trên chúng ta dùng CharField để hiển thị password bằng cách truyền tham số widget là forms.PasswordInput. Đây là danh sách các lớp Widget.

Tạo Template

Chúng ta tạo một thư mục với tên là templates, trong thư mục này chúng ta lại tạo một thư mục khác là user_auth rồi tạo một file HTML với tên là register.html:


<form action="" method="post">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Submit" />
</form>

Khi chúng ta viết các lớp form, Django sẽ nhìn các trường mà chúng ta khai báo rồi sinh ra các thẻ <input> tương ứng, ngoại trừ cặp thẻ <form>...</form> và thẻ <input type="submit"/>

<form action="" method="post">

Thuộc tính action sẽ chuyển dữ liệu tới đường dẫn tương ứng, ở đây mình để trống tức là gửi lại cho chính trang đó. Phương thức POST sẽ mã hóa dữ liệu được gửi đi.

{{ form }}

Chúng ta có thể tham chiếu đến phần tử form trong list context mà chúng ta sẽ khai báo trong các hàm view bên dưới, và Django sẽ tự động tạo các thẻ <label><input> cho chúng ta.

Tạo View

Chúng ta viết hàm view register() như sau.

from django.shortcuts import render

# Create your views here.

from django.http import HttpResponse
from .forms import RegisterForm

def register(request):

    if request.method == 'POST':
        response = HttpResponse()
        response.write("<h1>Thanks for registering</h1></br>")
        response.write("Your username: " + request.POST['username'] + "</br>")
        response.write("Your email: " + request.POST['email'] + "</br>")
        return response 
 
    registerForm = RegisterForm() 
    return render(request, 'user_auth/register.html', {'form':registerForm})

Đoạn code trên rất đơn giản, chúng ta chỉ import lớp RegisterForm từ module forms.py mà chúng ta viết ở trên. Sau đó tạo một đối tượng RegisterForm rồi đưa vào làm phần tử của list context (tham số thứ 3 trong hàm render()).

if request.method == 'POST':
    response = HttpResponse()
    response.write("<h1>Thanks for registering</h1></br>")
    response.write("Your username: " + request.POST['username'] + "</br>")
    response.write("Your email: " + request.POST['email'] + "</br>")
    return response 

Sau khi người dùng đã nhập các thông tin cần thiết vào các ô text và bấm gửi thì dữ liệu sẽ gửi trả lại hàm register() do chúng ta đã thiết lập nó trong thuộc tính action ở file template. Nên ở đây chúng ta kiểm tra xem nếu phương thức gửi tới là phương thức POST thì trả về câu trả lời với thông tin mà người dùng đã nhập

Tiếp theo chúng ta tạo url tới hàm view này, đầu tiên chúng ta khai báo một module urls.py của riêng app user_auth đã:

from django.conf.urls import url

from . import views

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

Sau đó là khai báo url của project tới app user_auth.

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls), 
    url(r'^user-auth/', include('user_auth.urls')),
]

Bây giờ bạn có thể chạy server và trỏ đường dẫn tới localhost:8000/user-auth/register/ để xem giao diện mặc định do Form của Django tạo ra.

Untitled

Như bạn thấy trong hình trên giao diện do Django tạo ra mặc định nhìn rất “chuối” vì nhiệm vụ của Django chỉ là tự sinh code cho 2 thẻ element là <label><input> cho chúng ta rảnh tay hơn mà thôi, bản thân Django cũng cung cấp một số thiết lập để chúng ta tùy chỉnh giao diện nhưng rất hạn chế. Phần lớn chúng ta sẽ phải tự chỉnh bằng tay bằng cách sửa đối code trong file template, thêm các file CSS, JS… để giao diện nhìn đẹp hơn.

Có một số tùy chọn hiển thị mà Django cung cấp cho chúng ta là:

  • form.as_table: bọc các thẻ <label><input> trong thẻ cặp thẻ <tr></tr>
  • form.as_p: bọc các thẻ <label><input> trong cặp thẻ <p></p>
  • form.as_ul: bọc các thẻ <label><input> trong cặp thẻ <li>

Tuy nhiên bạn phải khai báo cặp thẻ <table></table><ul></ul> tương ứng trước khi dùng. Ví dụ:


<form action="" method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit" value="Submit" />
</form>

Untitled

Giao diện nhìn có đỡ hơn một tí 🙂

Hoặc nếu muốn bạn có thể truy xuất từng phần tử của biến form trong file template để hiển thị theo ý muốn:


<form action="" method="post">
    {% csrf_token %}
        <table>
            <tr>
                <th>
                    <label for="{{ form.username.id_for_label }}">User:</label>
                </th>
                <td>
                    {{ form.username }}
                <td>
            </tr>
            <tr>
                <th>
                    <label for="{{ form.password.id_for_label }}">Password:</label>
                </th>
               <td>
                    {{ form.password }}
               </td>
            </tr>
            <tr>
                <th>
                    <label for="{{ form.email.id_for_label }}">Email:</label>
                </th>
                <td>
                    {{ form.email }}
                </td>
            </tr>
            <tr>
                <td></td>
                <td>
                    <input type="submit" value="Submit">
                </td>
            </tr>
    </table>
</form>

Bạn chỉ cần lấy theo cú pháp form.<tên thuộc tính> là Django sẽ tự động sinh code cho các thẻ tương ứng. Ví dụ như form.username...

Ngoài ra bạn còn có thể lấy các thuộc tính field nữa. Trong ví dụ trên là form.username.id_for_label sẽ lấy về id được tạo ra tự động. Đây là danh sách các thuộc tính field có trong Django.

Untitled

Đây là giao diện được thiết kế lại dùng thẻ <table> rất đơn giản.

Untitled

Django – Model nâng cao – Phần 3

Sau khi chúng ta đã tạo các lớp model, Django sẽ tự động tạo các phương thức mới trong model đó cho phép bạn thao tác với CSDL, bạn có thể tạo, sửa, xóa, cập nhật dữ liệu.

Trong bài này chúng ta sẽ tạo và sử dụng các lớp model sau đây để thực hành với các phương thức do Django cung cấp.

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self): 
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField()

    def __str__(self): 
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    def __str__(self): 
        return self.headline

Ở đây chúng ta có 3 model là Blog, AuthorEntry, các model này đại diện cho các lớp model cơ bản của một website blog.

Sau khi bạn đã tạo các lớp model trên thì bạn chạy 2 lệnh python manage.py makemigrationspython manage.py migrate để Django cập nhật các bảng mới trong CSDL. Sau đó bạn chạy trình shell bằng lệnh python manage.py shell để bắt đầu thao tác với CSDL (nếu bạn muốn chạy trong file .py riêng thì phải thiết lập biến môi trường DJANGO_SETTINGS_MODULE đến mysite.settings trước).

Tạo đối tượng

Mỗi bản ghi hay mỗi dòng trong từng bảng CSDL sẽ tương ứng với một đối tượng cụ thể. Để tạo một bản ghi trong CSDL thì chúng ta chỉ cần tạo một đối tượng tương ứng với trong Python rồi gọi phương thức save() là xong. Vd:

>>> from blog.models import Blog
>>> b = Blog(name='J.R.R Blog', tagline='All the latest J.R.R news.')
>>> b.save()

Đoạn code trên sẽ thực hiện câu lệnh INSERT, bạn phải gọi phương thức save() thì dữ liệu mới được cập nhật lên CSDL.

Ngoài ra nếu bạn muốn vừa tạo đối tượng vừa cập nhật thẳng lên CSDL luôn thì dùng phương thức create() trong đối tượng objects, đối tượng này là một đối tượng tĩnh do Django tạo ra cho chúng ta để đơn giản hóa việc thao tác với CSDL. Vd:

>>> Blog.objects.create(name='J.R.R Blog', tagline='All the latest J.R.R news.')

Cập nhật đối tượng

Bạn có thể thay đổi giá trị của các đối tượng trong Python rồi chỉ cần gọi phương thức save() là Django sẽ cập nhật mới trong CSDL.

>>> b.name = 'New name'
>>> b.save()

Cập nhật các thuộc tính khóa ngoại

Chúng ta tạo đối tượng Author mới như sau:

>>> from blog.models import Entry, Author
>>> a = Author(name='J.R.R. Tolkien', email='jrr@middleeast.com')
>>> a.save()

Bây giờ chúng ta sẽ tạo đối tượng Entry, đối tượng này chứa 2 thuộc tính khóa ngoại, một thuộc tính many-to-oneblogmany-to-manyauthors.

>>> from django.utils import timezone
>>> e = Entry(blog=b,
... headline='The lord of the rings', 
... body_text='Chapter 0', 
... pub_date=timezone.now(), 
... mod_date=timezone.now(), 
... n_comments=0, 
... n_pingbacks=0, 
... rating=0)
>>> e.save()

Việc gán giá trị cho các thuộc tính khóa ngoại many-to-one (ForeignKey) rất đơn giản, bạn chỉ cần gán thuộc tính khóa ngoại với đối tượng cần trỏ đến là xong, trong đoạn code trên chúng ta gán khóa ngoại blog = b, trong đó b là đối tượng Blog mà chúng ta đã tạo ra ở đầu bài.

>>> e.blog = Blog.create(...)
>>> e.save()

Khi cần thay đổi chúng ta cũng chỉ cần dùng phép gán như thường là được.

Đối với các thuộc tính khóa ngoại many-to-many thì chúng ta không được phép gán trực tiếp như đối với ForeignKey mà phải dùng phương thức riêng của thuộc tính khóa ngoại đó, bởi vì khóa ngoại ForeignKey chỉ lưu trữ các đối tượng đơn lẻ trong khi khóa ngoại ManyToManyField thì lại lưu trữ một danh sách các đối tượng khác nhau:

>>> e.authors.add(a)
>>> e.save()

Chúng ta lưu đối tượng a vào trong khóa ngoại authors, trong đó a là đối tượng Author mà chúng ta đã tạo ở trên.

Truy xuất dữ liệu

Khi chúng ta tạo một đối tượng thuộc lớp model do chúng ta tự định nghĩa, chẳng hạn như b=Blog(...), thì mặc định đối tượng b sẽ được Django cung cấp cho các phương thức để thao tác với CSDL trên chính bản ghi của b, chúng ta đã học một số phương thức này trong các ví dụ trên.

Ngoài cách thao tác với dữ liệu thông qua từng bản ghi như trên thì Django còn cung cấp cho chúng ta một đối tượng thuộc lớp django.db.models.manager.Manager để chúng ta thao tác với chính bảng được tạo ra đó, đối tượng này mặc định được đặt tên là objects.

>>> Blog.objects
>>> <django.db.models.manager.Manager object at 0x....>

Lấy toàn bộ dữ liệu

Khi chúng ta lấy dữ liệu từ CSDL về thì objects sẽ trả về một đối tượng QuerySet.

>>> Blog.objects.all()
>>> [<Blog: New name>]

Để lấy toàn bộ bản ghi hiện có trong bảng thì chúng ta dùng phương thức all().

>>> type(Blog.objects.all())
<class 'django.db.models.query.QuerySet'>

Như mình đã nói, danh sách được trả về là một đối tượng QuerySet.

Lọc dữ liệu

Có 2 phương thức hỗ trợ lọc dữ liệu là filter()exclude(). Tham số của 2 phương thức là một biểu thức tìm kiếm, trong đó phương thức filter() sẽ trả về dữ liệu khớp với biểu thức tìm kiếm, còn exclude() sẽ trả về dữ liệu không khớp. Chúng ta sẽ tìm hiểu thêm về các biểu thức tìm kiếm ở cuối bài.

Ví dụ:

>>> Entry.objects.filter(pub_date__year==2016)

Đoạn code trên sẽ lọc những bản ghi Entrypub_date (ngày đăng) vào năm 2016.

Bạn cũng có thể lọc theo nhiều điều kiện liên tiếp như sau:

>>> Entry.objects.filter(headline__startswith='What')
... .exclude(pub_date__gte=datetime.date.today())
... .filter(pub_date__gte=datetime(2016, 1, 30))

Đoạn code trên lọc các bản ghi có chuỗi bắt đầu là “What” và có pub_date nằm trong phạm vi từ ngày 30/1/2016 đến thời điểm hiện tại.

Lấy một dòng dữ liệu

Django cung cấp phương thức get() giúp bạn lấy một bản ghi duy nhất, tham số của phương thức này cũng là một biểu thức tìm kiếm như 2 phương thức filter()exclude(). Bạn cũng chỉ dùng get() khi biết dữ liệu trả về chỉ có 1 bản ghi thôi, nếu có nhiều bản ghi khớp với biểu thức tìm kiếm thì get() sẽ báo lỗi exception MultipleObjectsReturned.

>>> one_entry = Entry.objects.get(pk=1)

Một sự khác nhau nữa giữa get()filter()/exclude() là nếu get() không tìm thấy bản ghi nào thì sẽ trả về một lỗi exception là DoesNotExist còn 2 phương thức kia sẽ trả về một đối tượng QuerySet rỗng.

Ngoài 3 phương thức lọc dữ liệu trên là get(), filter()exclude() thì bạn có thể tìm hiểu thêm các phương thức khác tại đây.

Lọc số lượng bản ghi

Bạn có thể lọc số lượng bản ghi cần lấy theo cú pháp của Python. Ví dụ:

>>> Entry.objects.all()[:5]     # lấy 5 phần tử đầu tiên
>>> Entry.objects.all()[5:10]   # lấy các phần tử từ vị trí 5 đến 10

Python không hỗ trợ lọc theo chỉ số âm.

>>> Entry.objects.all()[-1]

Đoạn code trên sẽ báo lỗi.

Biểu thức tìm kiếm

Các biểu thức tìm kiếm sẽ thực hiện câu truy vấn SQL WHERE trong CSDL. Biểu thức tìm kiếm là tham số cho các phương thức lọc dữ liệu filter(), exclude()get().

Cú pháp lọc có dạng <tên thuộc tính>__<kiểu tìm kiếm>=<giá trị> (lưu ý ở đây có 2 dấu gạch dưới).

Ví dụ

>>> Entry.objects.filter(pub_date__lte='2016-01-01')

Đoạn code trên tương đương với câu lệnh SQL sau:

SELECT * FROM blog_entry WHERE pub_date <= '2016-01-01';

Trong biểu thức tìm kiếm thì <tên thuộc tính> bao giờ cũng là tên do chúng ta đặt khi định nghĩa model, ngoại trừ thuộc tính khóa ngoại phải thêm vào _id ở cuối tên. Ví dụ:

>>> Entry.objects.filter(blog_id=4)

Django cung cấp rất nhiều cú pháp <kiểu tìm kiếm> khác nhau, bạn có thể xem thêm ở đây. Ở đây mình giới thiệu một số.

  • exact: dữ liệu tìm được phải giống chính xác với giá trị cần tìm. Vd Entry.objects.get(headline__exact='Cat bites dog') chỉ trả về những bản ghi có headline“Cat bites dog“.
  • iexact: dữ liệu tìm được chỉ cần giống kí tự với giá trị cần tìm, không phân biệt chữ HOA-thường. Vd Blog.objects.get(name__iexact='beatles blog'), những bản ghi có headline“Beatles Blog”, “beatles blog” hoặc “BeAtlES blOG” đều được nhận.
  • contains: dữ liệu chỉ cần chứa chuỗi giá trị là được. Vd Entry.objects.get(headlines__contains='Lennon'). Ngoài ra còn có icontains cũng có chức năng như contains nhưng không phân biệt chữ HOA-thường.
  • startswith, endswith:  dữ liệu có chuỗi bắt đầu hoặc kết thúc giống với giá trị.

Tìm kiếm đa bảng

Các bảng trong CSDL có quan hệ với nhau nhờ vào các khóa ngoại, khi truy vấn dữ liệu, SQL cho phép bạn truy vấn các bản ghi có liên quan với nhau bằng cách nối các bảng lại bằng câu lệnh JOIN, Django cũng cho phép bạn làm điều đó, bạn chỉ cần đưa vào biểu thức tìm kiếm cú pháp <tên khóa ngoại>__<tên thuộc tính của bảng khác>=<giá trị>. Ví dụ:

>>> Entry.objects.filter(blog__name='Beatles Blog')

Câu lệnh trên sẽ tìm các bản ghi Entry có quan hệ với bảng Blog với name là “Beatles Blog”.

Django – Model nâng cao – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu về các tính chất của model trong Django.

Metadata

Các lớp trong Django có thể chứa các metadata (siêu dữ liệu), đây là các thông tin lưu trữ về các thiết lập của model, các thông tin này không được dùng để tạo bảng. Ví dụ:

from django.db import models

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        db_table = "oxen"

Các siêu dữ liệu sẽ được lưu trong một lớp nội có tên là Meta. Lớp này lưu trữ các thông tin cấu hình model, chẳng hạn như ordering tức là sắp xếp dữ liệu được trả về mặc định theo cột nào, db_table là tên bảng được tạo trong CSDL… bạn có thể xem danh sách tất cả các thuộc tính metadata ở đây.

Thừa kế model

Model trong Django cũng có thể thừa kế lẫn nhau. Chúng ta có thể viết các lớp model cơ sở (hay model cha) rồi sau đó viết các model khác thừa kế từ lớp model cơ sở này. Các lớp model cơ sở phải được thừa kế từ lớp django.db.models.Model, rồi từ đó các lớp con sẽ kế thừa từ lớp cha này. Model cha có thể có bảng riêng hoặc không có.

Có 3 cách để một model được thừa kế trong Django:

  • Thừa kế từ lớp trừu tượng.
  • Thừa kế đa bảng.
  • Dùng lớp Proxy.

Thừa kế từ lớp trừu tượng

Chúng ta sử dụng lớp trừu tượng khi muốn model cha chỉ có nhiệm vụ là lưu trữ các thông tin dùng chung trong các model con chứ không phải nhằm mục đích lưu các thông tin thật trong CSDL. Để một model là “trừu tượng” thì chúng ta gán thuộc tính abstract = True của lớp nội Meta.

Ví dụ:

from django.db import models

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)

Model Student thừa kế từ model trừu tượng là CommonInfo, Student sẽ bao gồm 3 trường là name, agehome_group.

Khi chúng ta dùng lệnh manage.py migrate, chỉ có model Student mới được tạo bảng cùng với 3 trường của nó.

Khi một model con thừa kế model cha mà không khai báo lớp nội Meta thì sẽ tự động thừa kế lớp nội đó từ model cha. Nhưng nếu muốn các model vẫn có thể có lớp nội riêng hoặc kế thừa và thêm các thuộc tính mới từ model cha.

Ví dụ:

from django.db import models

class CommonInfo(models.Model):
    # ...
    class Meta:
        abstract = True
        ordering = ['name']

class Student(CommonInfo):
    # ...
    class Meta(CommonInfo.Meta):
        db_table = 'student_info'

Ở đây lớp con sẽ kế thừa cả lớp nội Meta từ lớp cha. Nhưng có gì đó kì kì, nếu như vậy thì lớp con cũng sẽ trở thành lớp trừu tượng luôn sao vì nó sẽ kế thừa cả thuộc tính abstract? Không! Trước khi kế thừa từ lớp nội Meta thì Django sẽ tự động thiết lập biến abstract thành False ở lớp cha rồi sau đó sẽ cho kế thừa đến các lớp con, sau khi kế thừa xong thì Django mới thiết lập thuộc tính abstract lại như cũ ở lớp cha. Tuy nhiên nếu muốn bạn vẫn có thể thiết lập abstract = True ở các lớp con để tiếp tục kế thừa sâu hơn nữa.

Thừa kế đa bảng

Thừa kế đa bảng chỉ khác kiểu thừa kế lớp trừu tượng ở chỗ là lớp cha ở đây là lớp “thật” chứ không phải lớp trừu tượng. Tức là các model cha bây giờ sẽ có cả các bảng riêng của chúng trong CSDL. Ví dụ:

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

Chúng ta có 2 model là PlaceRestaurant kế thừa từ Place. Khác với kiểu kế thừa từ lớp trừu tượng, ở lớp trừu tượng thì các trường của lớp cha sẽ nằm cùng bảng với các lớp con vì lớp cha không được tạo bảng riêng, với thừa kế đa bảng thì các lớp con cũng sẽ kế thừa các trường của lớp cha nhưng các trường của lớp cha sẽ nằm trong bảng riêng của lớp cha.

>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")

Chúng ta vẫn có thể truy xuất các thông tin từ cả 2 lớp con và lớp cha như thường.

Lớp Proxy – lớp ủy quyền

Đôi khi bạn không muốn các lớp con có thêm các thuộc tính mới mà chỉ muốn có các phương thức mới, với 2 kiểu kế thừa ở trên bạn vẫn có thể làm được điều này nhưng Django sẽ tạo ra các bảng CSDL mới mặc dù không có trường nào cả, làm như thế sẽ lãng phí bộ nhớ và cũng khá kì cục.

Do đó Django cho phép chúng ta tạo ra các lớp Proxy (lớp ủy quyền), các lớp này kế thừa từ lớp cha nhưng không được tạo bảng mới trong CSDL, những lớp con này sẽ chỉ dùng để lưu các phương thức mới chứ không lưu các thuộc tính mới. Bạn có thể thực hiện thêm, sửa, xóa dữ liệu được kế thừa từ lớp cha, những dữ liệu này vẫn lưu ở bảng cha.

Ví dụ:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

class MyPerson(Person):
    class Meta:
        proxy = True

    def do_something(self):
        # ...
        pass

Chúng ta tạo lớp Person và lớp proxy MyPerson kế thừa từ lớp Person. Chúng ta tạo một lớp Proxy bằng cách khai báo lớp nội Meta và đưa vào thuộc tính proxy = True trong lớp Meta này.

>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>

Bất cứ thao tác cập nhật dữ liệu nào từ lớp MyPerson cũng sẽ là thao tác trực tiếp với dữ liệu từ lớp Person.

class OrderedPerson(Person):
    class Meta:
        ordering = ["last_name"]
        proxy = True

Bạn vẫn có thể định nghĩa lại các thông tin khác trong lớp Meta như sử dụng thuộc tính ordering chẳng hạn… vì bản thân các thuộc tính trong lớp Meta cũng không được dùng để tạo bảng mà nó chỉ là các thông tin cấu hình model thôi.

Đa thừa kế

Vì Django được phát triển từ Python nên các model trong Django có thể thừa kế từ nhiều model cha. Lớp Meta của model con chỉ có thể thừa kế từ lớp Meta của lớp cha đầu tiên.

Thường thì chúng ta cũng ít khi thừa kế từ nhiều bảng vì càng thừa kế nhiều thì sẽ càng làm cho ứng dụng trở nên phức tạp, khó quản lý.

Ngoài ra khi sử dụng đa thừa kế mà các model cha có cùng chung tên thuộc tính khóa chính thì Django sẽ báo lỗi. Do đó khi sử dụng đa thừa kế, chúng ta nên khai báo các trường làm khóa chính một cách rõ ràng. Ví dụ:

class Article(models.Model):
    article_id = models.AutoField(primary_key=True)
    ...

class Book(models.Model):
    book_id = models.AutoField(primary_key=True)
    ...

class BookReview(Book, Article):
    pass

Trong đoạn code trên model BookReview thừa kế từ 2 model cha là ArticleBook, cả 2 lớp cha đều có tên thuộc tính khóa chính khác nhau.

Hoặc nếu không muốn phải khai báo các khóa chính thì chúng ta có thể khai báo các model cha kế thừa lại từ một model khác để dùng chung khóa chính:

class Piece(models.Model):
    pass

class Article(Piece):
    ...
 
class Book(Piece):
    ...

class BookReview(Book, Article):
    pass

Django – Model nâng cao – Phần 1

Trong các bài trước chúng ta đã tìm hiểu sơ qua về model trong Django, trong phần này chúng ta sẽ tìm hiểu kĩ hơn.

Model trong Django là một cách định nghĩa về cách lưu trữ thông tin, một model bao gồm các trường và các hành động về dữ liệu mà bạn sẽ lưu trữ, định nghĩa này không khác gì định nghĩa lớp trong lập trình hướng đối tượng cả, nói cách khác Model chính là cách gọi khác của Class trong Django. Mỗi model sẽ đại diện cho một bảng trong cơ sở dữ liệu.

Model trong Django có các tính chất sau:

  • Mỗi model phải được kế thừa từ lớp django.db.models.Model.
  • Mỗi thuộc tính của model đại diện cho một trường trong cơ sở dữ liệu.
  • Từ đó Django sẽ tự động tạo các hàm thao tác với cơ sở dữ liệu cho bạn.

Ví dụ

Chúng ta định nghĩa model (class) Person có 2 thuộc tính là first_name và last_name:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

first_name và last_name là một trường của model – một thuộc tính của một lớp – một cột trong một bảng CSDL.

Tương ứng với model Person trên thì Django sẽ tạo ra một bảng trong CSDL như sau:

CREATE TABLE myapp_person (
    "id" serial NOT NULL PRIMARY KEY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

Lưu ý:

  • Tên của bảng được cấu thành từ <tên app>_<tên model> nhưng chúng ta có thể thiết lập trước để Django đặt theo tên do chúng ta quy định.
  • Trường id được Django tạo tự động và cũng có thể thiết lập trước.
  • Đoạn code SQL trên là theo cú pháp của CSDL PostgreSQL, tùy vào CSDL nào được dùng mà Django sẽ dùng cú pháp tương ứng.

Sử dụng model

Sau khi đã định nghĩa xong các model thì chúng ta phải thiết lập để Django sử dụng các model đó, bằng cách chỉnh sửa biến INSTALLED_APPS trong file settings.py.

Ví dụ nếu bạn định nghĩa model trong module myapp.models (được tạo ra bằng câu lệnh manage.py startapp) thì trong biến INSTALLED_APPS, bạn khai báo đường dẫn dẫn tới lớp cấu hình của ứng dụng, hoặc đường dẫn (theo cú pháp import của Python) dẫn tới package của ứng dụng như sau:

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

Sau khi đã khai báo trong file settings.py, bạn phải chạy 2 lệnh là manage.py makemigrations để bảo Django là bạn đã thay đổi cái gì đó trong cấu trúc CSDL, sau đó là lệnh manage.py migrate để thực hiện cập nhật lại CSDL.

Các trường

Thành phần quan trọng và cũng là thành phần bắt buộc phải có trong một model là danh sách các trường hay các thuộc tính của một lớp. Chú ý khi đặt tên bạn nhớ tránh đặt tên trùng với các từ khóa có sẵn trong Django.

Ví dụ:

from django.db import models

class Musician(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    instrument = models.CharField(max_length=100)

class Album(models.Model):
    artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    release_date = models.DateField()
    num_stars = models.IntegerField()

Mỗi trường phải là một đối tượng kế thừa từ lớp Field, đây là một lớp ảo trong Django. Các lớp Field sẽ cho Django biết kiểu dữ liệu của các cột trong bảng (chẳng hạn như INTEGER, VARCHAR, TEXT) và loại thẻ HTML được dụng để làm form nhập (vd <input type="text">, <select>...).

Django có rất nhiều các lớp kiểu dữ liệu Field có sẵn, bạn có thể xem thêm ở đây. Bạn cũng có thể viết một lớp Field cho riêng mình, nhưng mình sẽ đề cập đến trong bài khác.

Tham số trong Field

Mỗi lớp Field nhận một vài tham số khi khởi tạo. Ví dụ lớp CharField nhận tham số max_length để biết số lượng kí tự sẽ được dùng trong cột VARCHAR trong CSDL, đây là tham số bắt buộc phải có.

Ngoài các tham số bắt buộc thì còn có một số tham số tùy chọn nữa, ở đây mình chỉ giới thiệu một số:

  • null: có giá trị True hoặc False. Tham số này cho Django biết sẽ dùng giá trị NULL thay cho các bản ghi có giá trị rỗng, tức là trong CSDL mà kiểu chuỗi mà có giá trị "" thì sẽ thay bằng NULL.
  • blank: có giá trị True hoặc False. Tham số này cho Django biết cột tương ứng trong bảng CSDL được phép NULL hay NOT NULL.
  • default: thiết lập giá trị mặc định cho trường.
  • help_text: đây một giá trị chuỗi dùng để hiển thị một đoạn text ngắn mô tả về trường đó, vd như khi di chuột vào cột trên form HTML thì hiện ra đoạn text.
  • primary_key: True hoặc False, chỉ định trường đó có phải là khóa chính hay không. Như bạn đã biết, nếu bạn không chỉ định trường nào làm khóa chính thì Django sẽ tự động tạo một trường AutoField để làm khóa chính luôn. Có một điều nữa là khi bạn thay đổi giá trị khóa chính của một model trong Python thì Django không báo lỗi mà thay vào đó là tạo một đối tượng model mới với khóa chính mới, ví dụ:
from django.db import models

class Fruit(models.Model):
    name = models.CharField(max_length=100, primary_key=True)

Chúng ta tạo model Fruit với khóa chính là trường name.

>>> fruit = Fruit.objects.create(name='Apple')
>>> fruit.name = 'Pear'
>>> fruit.save()
>>> Fruit.objects.values_list('name', flat=True)
['Apple', 'Pear']

Sau khi tạo một đối tượng với nameApple, chúng ta thay đổi Apple thành Pear thì Django không đổi mà thay vào đó là tạo một đối tượng mới với namePear.

Khóa chính tự tạo

Khóa chính do Django tự tạo ra có dạng sau:

id = models.AutoField(primary_key=True)

Đây là một giá trị integer tự động tăng. Mỗi model chỉ được phép có một trường làm khóa chính.

Quan hệ

Sức mạnh của các cơ sở dữ liệu quan hệ nằm ở tính năng mối quan hệ giữa các bảng. Django hỗ trợ 3 loại quan hệ phổ biến của CSDL quan hệ là: many-to-one (một-nhiều), many-to-many (nhiều-nhiều), one-to-one (một-một).

Quan hệ Many-to-one

Chúng ta đã làm việc với kiểu quan hệ này trong các bài trước bằng cách dùng lớp django.db.models.ForeignKey. Tham số bắt buộc phải có của ForeignKey là model cha của nó.

Ví dụ chúng ta có một model là Car có model cha là Manufacturer (tiếng Anh nghĩa là nhà sản xuất), quan hệ này có nghĩa là một Manufacturer sản xuất nhiều Car nhưng một Car chỉ có một Manufacturer. Tương ứng chúng ta có đoạn code khai báo trong Django như sau:

from django.db import models

class Manufacturer(models.Models):
    #...
    pass

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    #...

Bạn còn có thể tạo mối quan hệ đệ quy, tức là một bảng tự quan hệ với chính nó.

Quan hệ Many-to-many

Để sử dụng mối quan hệ này thì chúng ta sử dụng lớp ManyToManyField với tham số bắt buộc là tên model mà nó sẽ trỏ đến.

Ví dụ chúng ta có 2 model là Pizza và Topping (nước sốt), một chiếc bánh pizza có thể có nhiều loại sốt và một loại sốt có thể được “xịt” lên nhiều bánh pizza.

from django.db import models

class Topping(models.Model):
    #...
    pass

class Pizza(models.Model):
    #...
    toppings = models.ManyToManyField(Topping)

Cũng giống như với ForeignKey, bạn cũng có thể dùng quan hệ đệ quy với ManyToManyField.

Bạn cũng chỉ nên cho một model trỏ đến model kia chứ không nên để 2 model trỏ đến nhau. Lý do tại sao chúng ta lại cho Pizza trỏ đến Topping chứ không phải ngược lại là vì tùy theo tình huống mà bạn chọn cho phù hợp, ví dụ như khi đi ăn pizza chúng ta thường mua một chiếc bánh rồi “xịt” nhiều loại nước sốt lên chứ không ai mua nhiều chiếc bánh rồi “xịt” mỗi bánh một loại sốt cả 🙂

Thuộc tính của một mối quan hệ many-to-many

Một mối quan hệ không đơn giản chỉ là quan hệ giữa bảng này với bảng kia, mà chúng còn có các thông tin riêng nữa.

Ví dụ chúng ta có model PersonGroup có quan hệ many-to-many, ý nghĩa là một người có thể tham gia nhiều group và một group có thể có nhiều người, ý nghĩa của quan hệ này không chỉ có bao nhiêu đó mà còn có các thông tin khác như người đó tham gia vào nhóm vào ngày nào, lý do tham gia chẳng hạn…

Để thêm các trường vào một mối quan hệ thì chúng ta sẽ dùng đến một tham số tùy chọn trong hàm khởi tạo ManyToManyFieldthrough.

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

Chúng ta phải định nghĩa một model khác lưu trữ các thông tin của một mối quan hệ, tham số through sẽ nhận tên của model này.

Trong Django thì các model kiểu này (Membership) được gọi mà model trung gian (Intermediate model), trong đó bạn phải khai báo rõ ràng các model tham gia vào mối quan hệ này, ở đây là 2 đối tượng ForeignKey: persongroup.

Sau khi đã có lớp trung gian, chúng ta có thể tạo các mối quan hệ many-to-many.

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
[<Person: Ringo Starr>]
>>> ringo.group_set.all()
[<Group: The Beatles>]
>>> m2 = Membership.objects.create(person=paul, group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason="Wanted to form a band.")
>>> beatles.members.all()
[<Person: Ringo Starr>, <Person: Paul McCartney>]

Chúng ta tạo 1 Groupbeatles, trong đó bao gồm 2 Personringopaul, mối quan hệ giữa ringobeatles được lưu trong m1, mối quan hệ giữa paulbeatles được lưu trong m2.

Đối với mối quan hệ many-to-many mà có sử dụng model trung gian thì chúng ta không thể sử dụng các phương thức như add(), create() hoặc dùng toán tử gán =.

# Error
>>> beatles.members.add(john)
# Error
>>> beatles.members.create(name="George Harrison")
# Error
>>> beatles.members = [john, paul, ringo, george]

Bởi vì bạn không thể tự nhiên mà tạo một mối quan hệ cho 2 đối tượng được, bạn phải khai báo rõ ràng các thông tin của mối quan hệ đó ra (bằng cách dùng hàm khởi tạo Membership()).

Sau khi đã tạo lập các mối quan hệ, chúng ta có thể truy vấn chúng như thường.

>>> Group.objects.filter(members__name__startswith='Paul')
[<Group: The Beatles>]
>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'

Mối quan hệ one-to-one

Để định nghĩa một mối quan hệ one-to-one thì chúng ta sử dụng lớp OneToOneField, lớp này cũng nhận tham số là tên của model mà nó sẽ trỏ tới.

Cũng giống như 2 loại mối quan hệ trên, chúng ta cũng có thể dùng quan hệ đệ quy với one-to-one.

Mối quan hệ này cũng ít được dùng nhiều nên mình không đi sâu ở đây.

Tham chiếu model thông qua file

Bạn có thể định nghĩa các lớp model trong các file khác nhau và khi cần tham chiếu đến chúng thì chỉ cần import vô là được.

from django.db import models
from geography.models import ZipCode

class Restaurant(models.Model):
    # ...
    zip_code = models.ForeignKey(
        ZipCode,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

Django – Tùy chỉnh trang Admin

Trong phần này chúng ta sẽ tùy chỉnh lại hệ thống Admin đã được trình bày trong bài Hệ thống Admin.

Tùy chỉnh form

Khi chúng ta đăng ký các lớp model QuestionChoice bằng hàm admin.site.register(), Django sẽ dựa vào các thuộc tính mà chúng ta khai báo trong các lớp đó để hiển thị form trên trang admin. Chúng ta có thể quy định Django chỉ chọn một số thuộc tính được phép hiện ra để chỉnh sửa, thay vì hiện ra hết, bởi vì khi làm một ứng dụng có sử dụng CSDL, có những thuộc tính mà bạn muốn máy tính tự động sinh ra chứ không phải do con người nhập vào, chẳng hạn như ID, ngày giờ…

Chúng ta sẽ thay đổi bằng cách sửa lại code trong file polls/admin.py.

from django.contrib import admin

# Register your models here.
from .models import Question, Choice

class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)

Việc này làm rất đơn giản, chúng ta chỉ cần viết một lớp kế thừa từ lớp admin.ModelAdmin, ở đây mình đặt tên là QuestionAdmin, sau đó khai báo list có tên là fields có các item là tên các thuộc tính trong model mà chúng ta muốn hiện ra trong trang Admin, sau đó truyền lớp này vào hàm admin.site.register().

Capture

Có thể bạn không nhận ra sự thay đổi vì chúng ta trước sau cũng cho hiện ra có 2 thuộc tính, nhưng thực ra khi bạn để Django tự động cho hiện ra hết thì nó sẽ hiển thị các thuộc tính theo thứ tự mà chúng ta khai báo trong file models.py, còn ở đây Django hiển thị theo thứ tự mà chúng ta khai báo trong biến fields.

Ngoài ra Django còn cho phép gom nhóm các thuộc tính lại với nhau để hiển thị.

from django.contrib import admin

# Register your models here.
from .models import Question, Choice

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)

Chúng ta khai báo các biến fields bên trong biến fieldsets để gom nhóm các thuộc tính lại với nhau. Mỗi nhóm đều có thể có tiêu đề hoặc không có.

Capture

Tùy chỉnh model chứa khóa ngoại

Tùy chỉnh trang chỉnh sửa

Hiện tại thì trang chỉnh sửa lớp Question có giao diện như hình dưới.

Capture

Mặc định thì Django chỉ để hiển thị đoạn text lấy từ phương thức str() mà chúng ta đã override trong phương thức __str__() cho từng đối tượng, chúng ta có thể tùy chỉnh Django hiển thị những thuộc tính khác.

class QuestionAdmin(admin.ModelAdmin):
    ...
    list_display = ('question_text', 'pub_date')

Để làm việc này thì chúng ta chỉ cần khai báo danh sách các thuộc tính cần hiển thị ra trong thuộc tính list_display.

Capture

Bạn thậm chí còn có thể sắp xếp các đối tượng Question bằng cách click chuột vào thanh tiêu đề.

Ngoài ra bạn có thể thêm các ô lọc theo thuộc tính.

class QuestionAdmin(admin.ModelAdmin):
    ...
    list_filter = ['pub_date']

Chúng ta khai báo tên các thuộc tính cần lọc trong thuộc tính list_filter.

Capture

Vì thuộc tính pub_date là kiểu DateTime nên Django biết cách đưa các tùy chọn vào phần lọc chẳng hạn như Any date, Today… còn đối với các thuộc tính kiểu text hay kiểu số nguyên, số thực thì dường như là không có hiệu quả mấy.

Tiếp theo, chúng ta có thể thêm ô tìm kiếm vào trang admin:

class QuestionAdmin(admin.ModelAdmin):
    ...
    search_fields = ['question_text']

Biến search_fields sẽ hiển thị ô tìm kiếm và chỉ tìm theo thuộc tính mà chúng ta đã khai báo trong đó, bạn cũng có thể thêm các thuộc tính khác vào nhưng nên thêm ít thôi vì Django sử dụng câu truy vấn LIKE với CSDL nên thêm nhiều thuộc tính vào sẽ làm quá trình tìm kiếm chậm đi.

 

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.