Cách sử dụng Django admin và các trường hợp chống chỉ định

Cách sử dụng Django admin và các trường hợp chống chỉ định
Screenshot by myself

Django admin là một app rất hữu ích và đầy đủ tính năng, được tích hợp sẵn trong Django framework. Sử dụng Django admin sẽ giúp chúng ta phát triển một trang Web nhanh hơn. Trong bài viết này, tôi sẽ trình bày một số tip để làm việc với Django admin.

Các tip được trình bày dưới đây sẽ minh họa cho một số bài toán trong thực tế, để các tip được trình bày một cách trực quan nhất. Giả sử chúng ta có một trang Web đơn giản, nơi mà người dùng có thể post các bức ảnh pet mà họ yêu thích và người dùng có thể comment với các bức ảnh được up lên. Đây là một ví dụ khá đơn giản và có những tính năng phổ biến.

Tip 1: Django admin không phải chỉ dành cho trang Web viết bằng Django

Trang admin của Django hoạt động rất tốt khi kết hợp với phần còn lại của framework, tuy nhiên nó cũng rất dễ dàng để hoạt động cùng với những cơ sở dữ liệu của trang khác, hoặc làm trang admin của chính trang đó nếu trang đó có giao diện admin không đẹp lắm. Và rất dễ dàng để kiểm tra xem liệu Django admin có phù hợp với nhu cầu của bạn hay không.

Tất cả những gì bạn cần làm là:

  1. Tạo một app mới trong project của Django và cấu hình để nó kết nối đến đúng cơ sở dữ liệu.
  2. Tạo model cho Django theo cấu trúc của cơ sở dữ liệu. Một lệnh rất hữu ích trong trường hợp này là manage.py inspectdb sẽ giúp bạn, nó sẽ kiểm tra cơ sở dữ liệu đang có và tự động generate model.
  3. Tạo module admin.py và cấu hình những gì bạn muốn.

Trở lại với ví dụ của chúng ta về trang up ảnh pet. Nó có thể được viết bằng bất cứ ngôn ngữ nào, giả sử trang admin của nó không được đẹp lắm. Để thay đổi điều này, chúng ta sẽ đưa cấu trúc cơ sở dữ liệu của nó vào model và sử dụng một cấu hình admin đơn giản của Django.

# models.py

class Picture(models.Model):
    DOG = 1
    CAT = 2
    ANIMAL_KIND_CHOICES = (
        (DOG, 'dog'),
        (CAT, 'cat'),
    )

    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, related_name='pictures')
    animal_kind = models.IntegerField(choices=ANIMAL_KIND_CHOICES)
    photo = models.ImageField(upload_to='animals')
    is_promoted = models.BooleanField(default=False)


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


class Comment(models.Model):
    author = models.ForeignKey(Author, related_name='comments')
    picture = models.ForeignKey(Picture, related_name='comments')
    comment = models.TextField()
    editors_note = models.TextField()

# admin.py

class PictureAdmin(admin.ModelAdmin):
    list_display = ('photo', 'animal_kind', 'author', 'is_promoted', )


class AuthorAdmin(admin.ModelAdmin):
    list_display = ('name', 'email', )


class CommentAdmin(admin.ModelAdmin):
    list_display = ('picture', 'author', )
Django admin

Tip #2: Lọc dữ liệu theo ý của bạn

Phần lớn người sử dụng Django admin đều dùng filter với một số trường nhất định. Việc này thực hiện rất đơn giản bằng cách thêm tên trường vào list_filter. Nhưng chúng ta hoàn toàn có thể tùy biến việc lọc dữ liệu này.

Ví dụ với trang Web của chúng ta, bạn muốn lọc ra những bức ảnh có trên 100 comment. Vậy phải làm thế nào? Chúng ta sẽ tạo ra một filter mới và cho nó vào list_filter.

class ProductiveAuthorsFilter(admin.SimpleListFilter):
    parameter_name = 'is_productive'
    title = 'Productive author'
    YES, NO = 1, 0

    # Number of comments for an author to be considered a productive one
    THRESHOLD = 100

    def lookups(self, request, model_admin):
        return (
            (self.YES, 'yes'),
            (self.NO, 'no'),
        )

    def queryset(self, request, queryset):
        qs = queryset.annotate(Count('comments'))

        # Note the syntax.  This way we avoid touching the queryset if our
        # filter is not used at all.
        if self.value() == self.YES:
            return qs.filter(comments__count__gte=self.THRESHOLD)
        if self.value() == self.NO:
            return qs.filter(comments__count__lt=self.THRESHOLD)

        return queryset


class PictureAdmin(admin.ModelAdmin):
    list_filter = [..., ProductiveAuthorsFilter]

Bây giờ chúng ta đã lọc ra những bức ảnh được cộng đồng quan tâm tích cực một cách dễ dàng.

Django filter

Và chúng ta có thể thực hiện các thao tác với những user này, ví dụ như quảng cáo chẳng hạn. Cách thực hiện cụ thể sẽ được trình bày trong phần tiếp theo.

Tip #3: Cho những thao tác phổ biến vào “actions”

Đây là một tính năng tuyệt với một trang admin. Bạn có nhớ mục “actions” trong thanh công cụ phía trên của mỗi model. Sẽ vô cùng tiện lợi nếu chúng ta có thể chọn ra một số bức ảnh ấn tượng và quảng bá cho nó chỉ bằng một vài click chuột. Django admin cho phép chúng ta làm như vậy rất dễ dàng.

class PictureAdmin(admin.ModelAdmin):
    actions = ['promote', ]

    def promote(self, request, queryset):
        queryset.update(is_promoted=True)
        self.message_user(request, 'The posts are promoted')
    promote.short_description = 'Promote the pictures'

Đây chính là điều chúng ta cần. Không cần phải mở form cho từng đối tượng nữa. Hơn nữa, nó rất dễ dàng mở rộng action của chúng ta, ví dụ sử dụng intermediate form. Django đã trình bày điều này rất đầy đủ và chi tiết.

Django action

Tip #4: Search trong bất cứ trường nào bạn muốn

Filter là một tính năng hay, nhưng nhiều khi chúng ta cần đến tính năng tìm kiếm. Trong phần lớn các ứng dụng, search được sử dụng để tìm kiếm qua các trường của model. Nhưng Django search có thể làm được nhiều hơn thế, khi nó có thể làm việc với các bảng liên kết. Ví dụ, chúng ta muốn tìm kiếm tiêu đề các bức ảnh, tên tác giả và các comment. Chúng ta sẽ làm thế nào?

class PictureAdmin(admin.ModelAdmin):
    search_fields = ('title', 'author__name', 'comments__text', )

Nếu cơ sở dữ liệu của bạn khá lớn, thì bạn hãy thêm index để việc tìm kiếm nhanh chóng hơn.

Django search

Tip #5: Xem trang view của đối tượng

Một nhu cầu rất hiển nhiên là chúng ta cần xem trang view của đối tượng được tạo ra ở trang admin hiển thị như thế nào với user thông thường. Bình thường, bạn cần truy cập vào form của đối tượng đó và click “View on site”. Chúng ta có thể làm việc này dễ dàng hơn:

class PictureAdmin(admin.ModelAdmin):
    list_display = [..., 'object_link']

    def object_link(self, item):
        url = item.get_absolute_url()
        return format_html('<a href="{url}">open</a>', url=url)
    object_link.short_description = 'View on site'

Đoạn code trên sẽ thêm link “View on site” vào các đối tượng ở trang index. Ở ví dụ trên, tôi giả thiết rằng bạn đã cài đặt phương thức get_absolute_url cho model. Bạn có thể đưa đoạn code này vào mixin hoặc một trang admin chung để dùng nó cho tất cả các đối tượng.

Django link

Tip #6: Edit một trường ngay tại chỗ trong chính trang index

Giả sử chúng ta muốn thêm editor_note vào các comment, và đương nhiên, chúng ta không muốn mở form của từng comment để làm việc này. Django cho phép chúng ta làm việc này dễ dàng bằng cách thay đổi ModelAdmin.

class CommentAdmin(admin.ModelAdmin):
    list_display = ('picture', 'author', 'editors_note', )
    list_editable = ('editors_note', )

Chỉ cần vào dòng code như trên là đủ. Bây giờ bạn có thể mở danh sách các comment, lọc theo tiêu chí mà bạn muốn, và có thể viết editor_note một cách dễ dàng.

Django edit

Tip #7: Tùy biến total với dữ liệu bạn muốn

Có một dòng total ở dưới mỗi “changelist”. Giả sử bạn muốn phân biệt số lượng các bức ảnh về chó và mèo. Tính năng này sẽ yêu cầu chúng ta code khá nhiều vì phải override changelist và template (bạn có thể tìm hiểu thêm về override template)

“Changelist” là trang mà chúng ta liệt kê các đối tượng theo một số tiêu chí nào đó. Bạn có thể lọc và thực hiện một số thao tác với những đối tượng này. Thông thường, click vào một đối tượng trong changelist sẽ đưa bạn đến changeform của đối tượng đó.

from django.contrib.admin.views.main import ChangeList

class PicturesChangeList(ChangeList):
    def get_results(self, request):
        super(PicturesChangeList, self).get_results(request)
        totals = self.result_list.aggregate(
            dogs_count=Sum(Case(When(animal_kind=Picture.DOG, then=1),
                           output_field=IntegerField())),
            cats_count=Sum(Case(When(animal_kind=Picture.CAT, then=1),
                           output_field=IntegerField())))
        self.totals = totals


class PictureAdmin(admin.ModelAdmin):
    def get_changelist(self, request):
        return PicturesChangeList

Và dưới đây là template

{% extends "admin/change_list.html" %}
{% block result_list %}
  {{ block.super }}
  <p>
    There are
    <strong>
      {{ cl.totals.dogs_count|default:'none' }} dogs and
      {{ cl.totals.cats_count|default:'none' }} cats
    </strong>
    on this page.
  </p>
{% endblock %}
Django total

Tip #8: Readonly trang admin với một vài người dùng

Ví dụ chúng ta muốn giới hạn readonly cho một vài người dùng để tránh việc họ có thể làm hỏng cả trang Web. Chúng ta có thể làm việc này khá đơn giản với Django admin.

class GrandmaProofAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if request.user.username == 'granny':
            return [f.name for f in self.model._meta.fields]
        else:
            return super(GrandmaProofAdmin, self).get_readonly_fields(request, obj)


class PictureAdmin(GrandmaProofAdmin):
    ...

Bây giờ, bạn có thể kế thừa quyền edit với những user được chỉ định. Lưu ý rằng, cách làm này không đảm bảo sẽ phù hợp với mọi hoàn cảnh. Bạn có thể cần cài đặt nhiều hơn nữa.

Tip #9: Tùy biến thao tác với từng đối tượng

Có những lúc, bạn chỉ muốn thực hiện action nào đó với chỉ một đối tượng mà thôi. Mục “action” trong thanh công cụ có thể làm được, nhưng việc phải chọn ra đối tượng đó, chọn chọn action khá mất thời gian. Bạn có thể thao tác nhanh hơn bằng cách click chỉ 1 nút.

Ví dụ chúng ta muốn gửi email đến các user, biểu thị sự tri ân với đóng góp của họ với trang Web của chúng ta.

class PictureAdmin(admin.ModelAdmin):
    list_display = (..., 'mail_link', )

    def mail_link(self, obj):
        dest = reverse('admin:myapp_pictures_mail_author',
                       kwargs={'pk': obj.pk})
        return format_html('<a href="{url}">{title}</a>',
                           url=dest, title='send mail')
    mail_link.short_description = 'Show some love'
    mail_link.allow_tags = True

    def get_urls(self):
        urls = [
            url('^(?P<pk>\d+)/sendaletter/?$',
                self.admin_site.admin_view(self.mail_view),
                name='myapp_pictures_mail_author'),
        ]
        return urls + super(PictureAdmin, self).get_urls()

    def mail_view(self, request, *args, **kwargs):
        obj = get_object_or_404(Picture, pk=kwargs['pk'])
        send_mail('Feel the granny\'s love', 'Hey, she loves your pet!',
                  'granny@yoursite.com', [obj.author.email])
        self.message_user(request, 'The letter is on its way')
        return redirect(reverse('admin:myapp_picture_changelist'))

Rất đơn giản, bây giờ, một link sẽ hiển thị bên cạnh các trường của đối tượng, cho phép chúng ta có thể send mail nhanh chóng bằng cách click.

Django sendmail

Bonus Tip: Giảm thiểu query bằng cách thêm một dòng duy nhất vào admin

Một tip khá phổ biến với Django admin (và cả các thành phần khác) là select_related. Nó sẽ giúp chúng ta load các đối tượng liên quan bằng cách thêm tên của chúng vào list_select_related và khi hiển thị thì không cần query thêm nữa. Nhưng nếu chúng ta muốn load tất cả các đối tượng liên quan thì sao? Rất đơn giản, chúng ta có thể set giá trị list_select_relatedTrue thì Django sẽ load tất cả các đối tượng được liên kết với đối tượng hiện tại.

class PictureAdmin(admin.ModelAdmin):
    list_select_related = True

Khi nào thì không nên dùng Django admin

Trên đây là những tip và chúng ta có thể dùng để làm việc với Django admin. Tuy nhiên, Django admin cũng có những hạn chế nhất định.

Dưới dây là những hạn chế của Django admin, nếu bạn cảm thấy nó đang cản trở hoạt động của trang Web của bạn, thì bạn không nên dùng nó.

Cập nhật template

Một điều khá bất cập là mặc dù đã được test để đảm bảo tương thích, nhưng template và thiết kế của những phiên bản mới vẫn có trường hợp không tương thích với phiên bản cũ. Nhất là khi bạn đã tùy biến và override nhiều thành phần rồi, việc update lên phiên bản mới có thể làm vỡ layout hiện tại. Django 1.9 với thiết kế phẳng cho trang admin, tuy nhiên hãy cân nhắc kỹ trước khi update vì nếu không thiết này không tương thích, bạn sẽ phải sửa khá nhiều.

JavaScript API không ổn định

Cũng giống như những API khác của Django, JavaScript API cũng không được đảm bảo là ổn định. Hơn nữa, nó cũng không cho phép bạn tùy biến. Ví dụ, bạn không thể thay đổi độ rộng của các popup mà không override gần như toàn bộ file.

Permission mặc định không phù hợp

Nếu bạn muốn xem danh sách các đối tượng, Django admin cung cấp cho bạn changelist. Bạn có thể tùy biến nó, rất ấn tượng. Nhưng nó có một vấn đề: để xem được changelist, bạn cần phải có quyền “change”. Nhưng với một trang Web thông thường, việc chỉ xem danh sách đối tượng thì quyền “view” là đủ.

Vì vậy, một developer phải đối mặt với 2 lựa chọn: một là bỏ tính năng changelist, hai là override nó với 90% code dùng lại. Lựa chọn nào cũng không phải ý tưởng hay.

Không thể sử dụng class-based view

Hiện tại, bạn chưa thể sử dụng class-based view cho trang admin. Nếu trang Web của bạn có sử dụng class-based view để chia sẻ những tính năng giống nhau thì việc gắn kết nó vào trang admin cũng không hề dễ dàng. Bạn có thể phải thay đổi middleware.

Cài đặt nguyên khối

Module django.contrib.admin.options có hơn 1900 dòng code (vào thời điểm bài viết này là 1977 dòng). Tất nhiên là những dòng code này rất dễ hiểu, nhưng việc duy trì một file gần 2000 dòng code không phải là ý tưởng hay. Nếu bạn quan tâm thì có thể tìm hiểu thêm package django.db.models có thể có những module còn lớn hơn nữa.

Fieldset chỉ hiệu quả với các trường hợp đơn giản

Với fieldset của admin, nó sẽ hiệu quả nếu bạn làm việc với những model được liên kết đơn giản. Nếu bạn muốn sử dụng các tab, fieldset đóng mở, v.v… thì có thể bạn sẽ cần code khá nhiều để tùy biến module này.

Kết luận

Django admin là một app rất đầy đủ, tiện lợi với người phát triển Web. Bài viết này trình bày cả những lợi ích và hạn chế của nó. Nên tùy vào tình huống thực tế mà bạn có thể quyết định xem có nên sử dụng nó hay không.

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.