Django Ứng dụng Python

Django bài 3: Model - database cho site tin tức

Đăng bởi - Ngày 22-06-2018

Chào các bạn, ở bài viết trước chúng ta đã cài đặt django với mongodb. Trong bài này chúng ta sẽ phân tích database để tạo model cho website tin tức của chúng ta. Do mongodb không có schema nên không có tool vẽ schema riêng cho mongo, vì vậy mình mượn tool của mysql để tạo schema.

Các bạn cùng xem qua schema sau nha

Schema database

Đây là schema database cơ bản của một website tin tức bao gồm category, category_detail, news_post.

- categories: Đây là danh mục tin tức, mỗi category chứa nhiều category detail

  • id: primary key của category, mỗi khi có category mới thêm vào, id sẽ tự tăng lên
  • name: tên của category
  • slug: là field bắt buộc, được python sử dụng tạo url cho routes
  • sort_order: dùng để sắp xếp thứ tự category
  • status: trạng thái category
  • parent_category_id: foreign_key của chính category table.

- news: Table lưu trữ các bài viết, được phân theo category và category detail

  • id_post: primary key của news_post
  • title: Tiêu đề bài viết
  • slug: là field bắt buộc, được python sử dụng tạo url cho routes
  • short_description: Tóm tắt của bài viết
  • description: nội dung đầy đủ của bài viết
  • created_at: ngày tạo bài viết
  • publish_at: ngày xuất bản
  • feature_img: hình đại diện bài viết
  • status: trạng thái bài viết
  • views_count: đếm lượt xem bài viết, field này sẽ dùng để xem bài viết nào hot được quan tâm
  • categories_id: foreign key của category

Tạo app category

Để tạo app category, ta sử dụng command start app

$ ./manage.py startapp appnews

Sau khi tạo xong app category bạn cần phải thêm app vào INSTALLED_APPS trong settings.py

# other settings
INSTALLED_APPS = [
    #other app
    'appnews'
]

Trong appnews chúng ta vừa tạo thì model news có phần upload feature_img. Để có thể upload image trong django bạn cần cài đặt pillow

$ pip install Pillow

Tiếp theo ta cần tạo migrations và migrate appnews đến database thông qua command sau:

$ ./manage.py makemigrations appnews
Migrations for 'appnews':
  appnews/migrations/0001_initial.py
    - Create model Categories
    - Create model News

$ ./manage.py migrate appnews
Operations to perform:
  Apply all migrations: appnews
Running migrations:
  Applying appnews.0001_initial... OK

Khi bạn startproject cho django thì django không tạo admin user cho bạn. Bạn cần chạy command bên dưới để tạo admin. Mình sẽ tạo admin với user là admin và password là admin12345

$ ./manage.py createsuperuser                                                                                
Username (leave blank to use 'root'): admin
Email address: admin@sitetintuc.com
Password: 
Password (again): 
Superuser created successfully.

Tạo models cho appnews

Chúng ta sẽ bắt đầu tạo models cho appnews. Thật sự thì phần này sẽ hơi rắc rối và nhiều bug nếu đi chi tiết từng bước để fix, nên mình sẽ chỉ đưa lên code hoàn chỉnh sau khi fix xong. Những chỗ các bạn có thể sẽ gặp lại lỗi nếu sử dụng những package của tuts này để viết lại thì mình sẽ nói chi tiết hơn chút.

Đầu tiên bạn hãy mở file models.py lên và import các package cần thiết

from django.utils.translation import ugettext_lazy as _ #1
from django.db import models #2
from django.utils.text import slugify #3
from datetime import date
  1. ugettext và ugettext_lazy: Cả hai đều là function translate của django, dựa vào cách đặt tên bạn cũng có thể biết được cả 2 khác nhau như thế nào. Với ugettext_lazy thì khi nào ta truy cập vào forms hoặc model thì string mới được translate, thường được dùng khi ta cần chuyển đổi ra nhiều ngôn ngữ khi django đã được chạy. Còn với ugettext thì bạn cũng có thể dùng cho views và các function khác mà không gặp vấn đề gì cả, bởi vì khi views được gọi thì ugettext mới thực thi.
  2. Function models của django, ta sẽ override models.Model để tạo model cho appnews
  3. Function slugify dùng để tạo slug cho model, fuction slugify nhận vào 2 tham số là value và allow_unicode: slugify(value, allow_unicode=False). Nếu allow_unicode là True thì slug sẽ giữ nguyên dấu từ name, ngược lại sẽ bỏ dấu và thay khoảng trắng thành dấu "-"

Tiếp theo ta sẽ tạo model categories trong appnews/models.py

class Categories(models.Model):
    name = models.CharField(max_length=254, verbose_name=_('name'))
    slug = models.SlugField(max_length=254, unique=True, blank=True, editable=True)
    parent_category = models.ForeignKey("self", verbose_name=_('Parent category'),
                                        null=True, blank=True,
                                        on_delete=models.CASCADE) #1
    sort_order = models.PositiveSmallIntegerField(default=10, verbose_name=_('Sort order'))
    status = models.BooleanField(default=0, verbose_name=_('Status'))

    def __str__(self):
        return self.name

    @staticmethod
    def extra_filters(obj): #2
        if not obj.parent_category:
            return {'parent_category__isnull': True}
        return {'parent_category': obj.parent_category}

    def save(self, *args, **kwargs): #3
        self.slug = slugify(self.name) #4
        if self.parent_category_id is not None: #5
            Categories.objects.filter(parent_category=self.id).update(parent_category=None)

        if not self.id: #6
            try:
                filters = self.__class__.extra_filters(self)
                self.sort_order = self.__class__.objects.filter(
                    **filters
                ).order_by("-sort_order")[0].sort_order + 10
            except IndexError:
                self.sort_order = 0

        super(Categories, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _('Category')
        verbose_name_plural = _('Categories')
        ordering = ['sort_order']
  1. Ở bảng schema sql table trên ta đã có ý định tạ một ForeignKey trong chính model categories, ở dòng code này bạn sẽ thấy "self", có nghĩa là khai báo sử dụng chính model categorires để là ForeignKey. Ngoài ra còn một cách viết khác là
    parent_category = models.ForeignKey("Categories", verbose_name=_('Parent category'),
                                            null=True, blank=True,
                                            on_delete=models.CASCADE)​

    Tuy nhiên cách viết này không được khuyến khích lắm

  2. Function static extra_filter này được sử dụng để kiểm tra xem object này có parent_category hay không. Kết quả trả về như một điều kiện để lọc dữ liệu. function @staticmethod không có param self, nó chỉ nhận trực tiếp data vào như một function độc lập không nằm trong class. Xem thêm tại đây
  3. Để override function save của models.Model, ta cần khai báo function save để custom data trước khi save data vào database
  4. Tại dòng này, ta sẽ dùng slugify và chỉ định sẽ dùng field nào để set dữ liệu cho slug field. Khi save bạn sẽ để ý một số từ encode với utf8 bị slugify cắt bỏ mất, mình sẽ hướng dẫn bạn cách fix sau
  5. Logic tại dòng này mình thêm vào với ý tưởng là: nếu categories A đã là parent của một hoặc nhiều category khác, nhưng ta lại set category A là con của category Z thì sẽ reset lại tất category con của category A thành Null
  6. Logic tiếp theo sẽ dùng tự động cộng thêm 10 vào sort_order. Ta sẽ lấy sort_order lớn nhất được filter bởi extra_filter để cộng lên. Trường hợp nếu category add vào không có data, ta sẽ bắt lỗi IndexError để set sort_order bằng 0

Và tiếp theo là model news. Bạn hãy copy model này vào appnews/models.py

class News(models.Model):
    categories = models.ForeignKey(Categories, null=True, verbose_name=_('Category'),
                                   on_delete=models.CASCADE)
    title = models.CharField(max_length=254, verbose_name=_('name'))
    slug = models.SlugField(max_length=254, unique=True, blank=True, editable=True)
    short_description = models.TextField(verbose_name=_('Short description'))
    description = models.TextField(verbose_name=_('Description'))
    created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True)
    publish_at = models.DateTimeField(verbose_name=_('Publish at'),)
    upload_to = 'img/news/{0}/{1}'.format(date.today().year, date.today().month)
    feature_img = models.ImageField(upload_to=upload_to, blank=True, null=True, max_length=254,
                                      verbose_name=_('Feature Image'))
    status = models.BooleanField(default=0, verbose_name=_('Status'))
    views_count = models.IntegerField(default=0, verbose_name=_('Views count'))

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super(News, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _('News')
        verbose_name_plural = _('News')
        ordering = ['-publish_at']
        get_latest_by = 'created_at'

Trong model này thì không có gì đặc biệt. Chỉ có upload_to là đường dẫn hình bạn sẽ upload. File appnews/models.py hoàn chỉnh như sau

from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.utils.text import slugify
from datetime import date

# Create your models here.
class Categories(models.Model):
    name = models.CharField(max_length=254, verbose_name=_('name'))
    slug = models.SlugField(max_length=254, unique=True, blank=True, editable=True)
    parent_category = models.ForeignKey("self", verbose_name=_('Parent category'),
                                        null=True, blank=True,
                                        on_delete=models.CASCADE)
    sort_order = models.PositiveSmallIntegerField(default=10, verbose_name=_('Sort order'))
    status = models.BooleanField(default=0, verbose_name=_('Status'))

    def __str__(self):
        return self.name

    @staticmethod
    def extra_filters(obj):
        if not obj.parent_category:
            return {'parent_category__isnull': True}
        return {'parent_category': obj.parent_category}

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        if self.parent_category_id is not None:
            Categories.objects.filter(parent_category=self.id).update(parent_category=None)

        if not self.id:
            try:
                filters = self.__class__.extra_filters(self)
                self.sort_order = self.__class__.objects.filter(
                    **filters
                ).order_by("-sort_order")[0].sort_order + 10
            except IndexError:
                self.sort_order = 0

        super(Categories, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _('Category')
        verbose_name_plural = _('Categories')
        ordering = ['sort_order']


class News(models.Model):
    categories = models.ForeignKey(Categories, null=True, verbose_name=_('Category'),
                                   on_delete=models.CASCADE)
    title = models.CharField(max_length=254, verbose_name=_('name'))
    slug = models.SlugField(max_length=254, unique=True, blank=True, editable=True)
    short_description = models.TextField(verbose_name=_('Short description'))
    description = models.TextField(verbose_name=_('Description'))
    created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True)
    publish_at = models.DateTimeField(verbose_name=_('Publish at'),)
    upload_to = 'img/news/{0}/{1}'.format(date.today().year, date.today().month)
    feature_img = models.ImageField(upload_to=upload_to, blank=True, null=True, max_length=254,
                                      verbose_name=_('Feature Image'))
    status = models.BooleanField(default=0, verbose_name=_('Status'))
    views_count = models.IntegerField(default=0, verbose_name=_('Views count'))

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super(News, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _('News')
        verbose_name_plural = _('News')
        ordering = ['-publish_at']
        get_latest_by = 'created_at'

Đừng quên chạy comand tạo migration và migrate nha các bạn

$ ./manage.py makemigrations appnews && ./manage.py migrate appnews

Thiết lập admin.py - Fix lỗi djongo

Phần này mới có nhiều thứ để fix. Một phần bài mình ra chậm cũng do lỗi từ đây mà ra. Các lỗi này đa phần là do package djongo không support, hoặc là bugs. Mình cũng chưa submit lỗi này cho bên đó.

Bạn mở file appnews/admin.py lên và import các thư viện cần thiết vào

from django.contrib import admin #1
from django.db.models import Q #2
from django.utils.translation import ugettext_lazy as _
from .models import Categories, News #3
  1. Package admin dùng để register admin section đến django
  2. Q là một model cho chúng ta tạo câu query AND hoặc OR, mình thường dùng để tạo AND NOT hoặc OR NOT
  3. Import các model mà bạn muốn đăng ký vào section admin

Section đầu tiên ta sẽ tạo là CategoriesAdmin. Bạn copy đoạn sau vào appnews/admin.py

@admin.register(Categories) #1
class CategoriesAdmin(admin.ModelAdmin): #2
    list_display = ('name', 'parent_category', 'status', 'sort_order') #3
    search_fields = ['name', ] #4
  1. Có nhiều cách để register model với admin trong django. Cá nhân mình thì thường dùng cách khai báo này hơn
  2. Bạn có thể đặt tên class là gì cũng được, nhưng để dễ quản lý, mình quy định tên class sẽ là tên Model + Admin. Trường hợp trên là CategoriesAdmin
  3. list_display cho phép chúng ta hiển thị field nào sẽ hiện lên trong trang danh sách categories
  4. search_fields cho phép ta thiết lập django sẽ search vào những field nào trong model categories

Bây giờ bạn hãy vào trang admin tại địa chỉ http://127.0.0.1:8000/admin với user là admin và password là admin12345 để xem thử

Categories section

Bạn sẽ thấy section hiện ra tại đây. Tiếp theo bạn hãy tạo một category mới

New category

Tại form này bạn sẽ thấy sort order hiển thị mặc định là 10. Điều này hoàn toàn đúng, không sai. Nhưng bạn còn nhớ ta có viết logic tự động cộng giá trị sort_order khi tạo category mới chứ? Chúng ta sẽ không cần sort order hiển thị trên form tạo categories mới mà chỉ cho hiển thị trong chỉnh sửa categories. Để làm được điều đó ta sẽ phải override function trong CategoriesAdmin.

@admin.register(Categories)
class CategoriesAdmin(admin.ModelAdmin):
    list_display = ('name', 'parent_category', 'status', 'sort_order')
    search_fields = ['name', ]
    none_type = type(None)

    def get_form(self, request, obj=None, **kwargs):
        request.obj = obj

        if isinstance(obj, self.none_type) is True:
            self.exclude = ("sort_order", )
        else:
            self.exclude = None

        return super(CategoriesAdmin, self).get_form(request, obj, **kwargs)

Ta sẽ override function get_form và kiểm tra nếu object của form không có data là NoneType, ta sẽ remove sort_order và ngược lại. Save lại và reload page để xem kết quả nào

new categories without sort order

Và khi ta edit categories, sort_order phải hiện ra

Edit categories

Ok vậy là ta đã xử lý xong sort order. Vấn đề tiếp theo là ta cần chỉ show ra category parent ở parent_category thôi và nếu ta mở category parent nào, thì category đó sẽ không hiển thị trong parent_category.

Để thực hiên logic này, ta cần phải override tiếp function foreignkey, chúng ta sẽ thiết lập lại dữ liệu đưa vào trong foreignkey để bắt nó thực hiện logic của chúng ta. Bạn thêm tiếp function bên dưới vào CategoriesAdmin

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'parent_category':
            if not isinstance(request.obj, self.none_type):
                kwargs['queryset'] = Categories.objects.filter(~Q(
                    slug__exact=request.obj.slug), parent_category__isnull=True,
                                                               slug__exact=request.obj.slug)
            elif request.method == 'GET':
                kwargs['queryset'] = Categories.objects.filter(parent_category__isnull=True)

Function này sẽ kiểm tra tên field có đúng là parent_category không và thực hiện 2 nhiệm vụ:

  1. Nếu là form edit thì query_set sẽ filter không hiển thị chính category parent đó, và chỉ lọc ra những category nào có parent_category là null, cuối cùng slug__exact=request.obj.slug sẽ fix lỗi trả về quá nhiều kết quả khi bấm submit form.
  2. Trường hợp nếu là form mới, chúng ta sẽ chỉ lọc ra category nào có parent_category là null thôi

File appnews/admin.py lúc này sẽ như sau:

from django.contrib import admin
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from .models import Categories, News
# Register your models here.

@admin.register(Categories)
class CategoriesAdmin(admin.ModelAdmin):
    list_display = ('name', 'parent_category', 'status', 'sort_order')
    search_fields = ['name', ]
    none_type = type(None)

    def get_form(self, request, obj=None, **kwargs):
        request.obj = obj

        if isinstance(obj, self.none_type) is True:
            self.exclude = ("sort_order", )
        else:
            self.exclude = None

        return super(CategoriesAdmin, self).get_form(request, obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'parent_category':
            if not isinstance(request.obj, self.none_type):
                kwargs['queryset'] = Categories.objects.filter(~Q(
                    slug__exact=request.obj.slug), parent_category__isnull=True,
                                                               slug__exact=request.obj.slug)
            elif request.method == 'GET':
                kwargs['queryset'] = Categories.objects.filter(parent_category__isnull=True)

        return super(CategoriesAdmin,
                     self).formfield_for_foreignkey(db_field, request, **kwargs)

Tiếp theo sẽ là section NewsCategory, bạn thêm đoạn code bên dưới vào appnews/admin.py và vào trang http://127.0.0.1:8000/admin/appnews/news/ để xem kết quả

@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
    list_display = ('title', 'get_category', 'status')
    date_hierarchy = 'created_at' #1
    search_fields = ['title', 'categories__name']

    def get_category(self, obj): #2
        return obj.categories.name

    get_category.short_description = _('Categories') #3
    get_category.admin_order_field = 'category__name' #4

    list_filter = (
        ('categories', admin.RelatedFieldListFilter), #5
    )
  1. date_hierarchy là một variable dùng để tạo filter cho các đối tượng DATE hoặc DATETIME. Xem thêm chi tiết tại đây
  2. Vì mình muốn biết bài viết nào sẽ hiển thị kèm theo danh mục, ta phải tạo một custom column để hiển thị tên category vào list_display
  3. Rename lại tên column trong list display
  4. Sắp sếp thứ tự category theo tên
  5. Ta sẽ hiển thị một cột filter bài viết theo categories trong danh sách news
    Category filter in news

Nhưng tới đây, thì sẽ có vẫn đề xảy ra. Đây có lẽ là bugs của django, hoặc chỉ riêng với package djongo. Vì mình có làm project với mysql nhưng không gặp, và trên google cũng chưa trường hợp nào bị (Chắc do số mình hên :d). Bạn hãy refresh lại page news để thấy lỗi

date hierarchy index error

Lỗi này xuất hiện khi ta set date_hierarchy = 'created_at' nhưng lại không có dữ liệu. Để fix được ta cần override function get_list_display

    date_hierarchy = None #1

    def get_list_display(self, request):
        if News.objects.all().count() > 0: #2
            self.date_hierarchy = 'created_at'
        else:
            self.date_hierarchy = None

        return super(NewsAdmin, self).get_list_display(request)
  1. Đầu tiên ta sẽ set date_hierarchy = None để tránh lỗi xảy ra
  2. Kế tiếp ta viết 1 đoạn code nhỏ check thử nếu table News có data thì ta sẽ set date_hierarchy = 'created_at' và ngược lại

File appnews/admin.py của bạn lúc này sẽ như sau

from django.contrib import admin
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from .models import Categories, News
# Register your models here.

@admin.register(Categories)
class CategoriesAdmin(admin.ModelAdmin):
    list_display = ('name', 'parent_category', 'status', 'sort_order')
    search_fields = ['name', ]
    none_type = type(None)

    def get_form(self, request, obj=None, **kwargs):
        request.obj = obj

        if isinstance(obj, self.none_type) is True:
            self.exclude = ("sort_order", )
        else:
            self.exclude = None

        return super(CategoriesAdmin, self).get_form(request, obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'parent_category':
            if not isinstance(request.obj, self.none_type):
                kwargs['queryset'] = Categories.objects.filter(~Q(
                    slug__exact=request.obj.slug), parent_category__isnull=True,
                                                               slug__exact=request.obj.slug)
            elif request.method == 'GET':
                kwargs['queryset'] = Categories.objects.filter(parent_category__isnull=True)

        return super(CategoriesAdmin,
                     self).formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
    list_display = ('title', 'get_category', 'status')
    date_hierarchy = 'created_at'
    search_fields = ['title', 'categories__name']

    def get_category(self, obj):
        return obj.categories.name

    get_category.short_description = _('Categories')
    get_category.admin_order_field = 'category__name'

    list_filter = (
        ('categories', admin.RelatedFieldListFilter),
    )

    def get_list_display(self, request):
        if News.objects.all().count() > 0:
            self.date_hierarchy = 'created_at'
        else:
            self.date_hierarchy = None

        return super(NewsAdmin, self).get_list_display(request)

Bây giờ bạn hãy refresh lại page xem sao

News list

Hì, nãy giờ cực khổ vầy chỉ để show ra cái này thôi đấy. Tiếp theo bạn hãy tạo một bài viết mới nha.

Djongo error

Oát đờ *** vừa tận hưởng niềm vui chưa được bao lâu, giờ lại lỗi tiếp :( Đây cũng là một lỗi trên google không có, và mình cũng chưa submit đến team dev djongo nữa. Chủ yếu là vẫn lỗi với date_hierarchy, khi ta set date_hierarchy = 'created_at' thì trong subclasses của BaseDatabaseOperations cần phải có datetrunc_sql(), cụ thể là trong tuts của chúng ta thì anh chàng djongo thiếu các method này.

Bây giờ bạn hãy vào thư mục cài đặt site-packages chứa djongo, tìm và mở file operations.py. Trường hợp máy mình thì là ở đường dẫn sau

/var/projects/django/sitestintuc/pyenv/lib/python3.6/site-packages/djongo/operations.py

Bạn kéo đến cuối file vào thêm vào 3 functions sau

    def date_trunc_sql(self, lookup_type, field_name):
        return field_name

    def date_extract_sql(self, lookup_type, field_name):
        return field_name

    def datetime_extract_sql(self, lookup_type, field_name, tzname):
        return field_name

file lib/python3.6/site-packages/djongo/operations.py sẽ như sau

import pytz
from django.conf import settings
from django.db.backends.base.operations import BaseDatabaseOperations
from django.utils import six, timezone
import datetime, calendar


class DatabaseOperations(BaseDatabaseOperations):

    def quote_name(self, name):
        if name.startswith('"') and name.endswith('"'):
            return name
        return '"{}"'.format(name)

    def adapt_datefield_value(self, value):
        if value is None:
            return None

        if isinstance(value, datetime.datetime) and timezone.is_aware(value):
            raise ValueError("Djongo backend does not support timezone-aware dates.")

        return datetime.datetime.utcfromtimestamp(calendar.timegm(value.timetuple()))

    def adapt_datetimefield_value(self, value):
        if value is None:
            return None

        if isinstance(value, datetime.datetime) and timezone.is_aware(value):
            if settings.USE_TZ:
                value = timezone.make_naive(value, self.connection.timezone)
            else:
                raise ValueError("Djongo backend does not support timezone-aware datetimes when USE_TZ is False.")
        return value

    def adapt_timefield_value(self, value):
        if value is None:
            return None

        if isinstance(value, six.string_types):
            return datetime.datetime.strptime(value, '%H:%M:%S')

        if timezone.is_aware(value):
            raise ValueError("Djongo backend does not support timezone-aware times.")

        return datetime.datetime(1900, 1, 1, value.hour, value.minute,
                                 value.second, value.microsecond)

    def convert_datefield_value(self, value, expression, connection):
        if isinstance(value, datetime.datetime):
            if settings.USE_TZ:
                value = timezone.make_aware(value, self.connection.timezone)
            value = value.date()
        return value

    def convert_timefield_value(self, value, expression, connection):
        if isinstance(value, datetime.datetime):
            if settings.USE_TZ:
                value = timezone.make_aware(value, self.connection.timezone)
            value = value.time()
        return value

    def convert_datetimefield_value(self, value, expression, connection):
        if isinstance(value, datetime.datetime):
            if settings.USE_TZ:
                value = timezone.make_aware(value, self.connection.timezone)
        return value

    def get_db_converters(self, expression):
        converters = super(DatabaseOperations, self).get_db_converters(expression)
        internal_type = expression.output_field.get_internal_type()
        if internal_type == 'DateField':
            converters.append(self.convert_datefield_value)
        elif internal_type == 'TimeField':
            converters.append(self.convert_timefield_value)
        elif internal_type == 'DateTimeField':
            converters.append(self.convert_datetimefield_value)
        return converters

    def sql_flush(self, style, tables, sequences, allow_cascade=False):
        # TODO: Need to implement this fully
        return [f'ALTER TABLE "{table}" FLUSH'
                for table in tables]

    def max_name_length(self):
        return 50

    def no_limit_value(self):
        return None

    def bulk_insert_sql(self, fields, placeholder_rows):
        return ' '.join(
            'VALUES (%s)' % ', '.join(row)
            for row in placeholder_rows
        )

    def date_trunc_sql(self, lookup_type, field_name):
        return field_name

    def date_extract_sql(self, lookup_type, field_name):
        return field_name

    def datetime_extract_sql(self, lookup_type, field_name, tzname):
        return field_name

Xong xuôi thì bạn hãy restart server django và thử submit news lại

djongo fixed news

Hì hì, lần này là ẻm chạy mượt mà rồi đó, nhưng mà vẫn còn một vấn đề nữa đó là cột filter bên phải show cả Sự kiện và Châu á, trong khi chúng ta chỉ có một tin cho categories Sự kiện. Để fix vấn đề này bạn hãy tạo một file utils.py trong appnews như sau

from django.contrib.admin import RelatedFieldListFilter
from django.utils.translation import ugettext_lazy as _


class CategoryFilter(RelatedFieldListFilter):
    news_model = None

    def __init__(self, field, request, params, model, model_admin, field_path):
        self.lookup_kwarg = '%s__%s__exact' % (field_path, field.target_field.name)
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
        super(CategoryFilter, self).__init__(field, request, params,
                                                model, model_admin,
                                                field_path)
        self.news_model = model

    def expected_parameters(self):
        return [self.lookup_kwarg]

    def choices(self, changelist):
        yield {
            'selected': self.lookup_val is None and not self.lookup_val_isnull,
            'query_string': changelist.get_query_string(
                {},
                [self.lookup_kwarg, self.lookup_kwarg_isnull]
            ),
            'display': _('All'),
        }

        for pk_val, val in self.lookup_choices:
            if self.news_model.objects.filter(categories=pk_val).count() > 0:
                yield {
                    'selected': self.lookup_val == str(pk_val),
                    'query_string': changelist.get_query_string({
                        self.lookup_kwarg: pk_val,
                    }, [self.lookup_kwarg_isnull]),
                    'display': val,
                }

Chúng ta sẽ override RelatedFieldListFilter ở đây, và xem thử category nào có bài viết thì mới show ra trong list filter

Bây giờ ta sẽ thay admin.RelatedFieldListFilter bằng CategoryFilter mà ta vừa tạo. Bạn hãy mở file appnews/admin.py lên và thêm như sau

#other imports
from .utils import CategoryFilter


@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
   
    ## other code
    list_filter = (
        ('categories', CategoryFilter),
    )

Cuối cùng file appnews/admin.py sẽ như sau:

from django.contrib import admin
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from .models import Categories, News
from .utils import CategoryFilter
# Register your models here.

@admin.register(Categories)
class CategoriesAdmin(admin.ModelAdmin):
    list_display = ('name', 'parent_category', 'status', 'sort_order')
    search_fields = ['name', ]
    none_type = type(None)

    def get_form(self, request, obj=None, **kwargs):
        request.obj = obj

        if isinstance(obj, self.none_type) is True:
            self.exclude = ("sort_order", )
        else:
            self.exclude = None

        return super(CategoriesAdmin, self).get_form(request, obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        if db_field.name == 'parent_category':
            if not isinstance(request.obj, self.none_type):
                kwargs['queryset'] = Categories.objects.filter(~Q(
                    slug__exact=request.obj.slug), parent_category__isnull=True,
                                                               slug__exact=request.obj.slug)
            elif request.method == 'GET':
                kwargs['queryset'] = Categories.objects.filter(parent_category__isnull=True)

        return super(CategoriesAdmin,
                     self).formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
    list_display = ('title', 'get_category', 'status')
    date_hierarchy = 'created_at'
    search_fields = ['title', 'categories__name']

    def get_category(self, obj):
        return obj.categories.name

    get_category.short_description = _('Categories')
    get_category.admin_order_field = 'category__name'

    list_filter = (
        ('categories', CategoryFilter),
    )

    def get_list_display(self, request):
        if News.objects.all().count() > 0:
            self.date_hierarchy = 'created_at'
        else:
            self.date_hierarchy = None

        return super(NewsAdmin, self).get_list_display(request)

Refresh lại page xem kết quả nào :3

Fix filter category

Category Châu Á bây giờ đã biến mất đúng theo ý của chúng ta :3

Thay đổi giao diện mặc định của django

Còn một điều sau cùng trong bài hôm nay nữa, đó là giao diện admin mặc định của django xấu quá chịu không nổi :)) mình sẽ tiến hành thay đổi giao diện admin django. Bạn nào không muốn làm theo cũng không sao.

Có rất nhiều developer phát triển các package template cho django, bạn có thể tham khảo tại link này https://djangopackages.org/grids/g/admin-styling/. Mình sẽ chọn django-jet vì lý do... không gì cả :d nếu bạn thích thì có thể chọn package khác. Nhưng sẽ có bài mình hướng dẫn custom dashboard hoặc chỉnh sửa giao diện trong admin :p

Để cài django-jet bạn hãy dùng command sau

$ pip install django-jet

và copy các file mình đưa ra ở đây cho lẹ :d bạn nào muốn xem cách cài chi tiết có thể xem tại đây: https://github.com/geex-arts/django-jet

newsproject/settings.py

"""
Django settings for newsproject project.

Generated by 'django-admin startproject' using Django 1.11.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""

import os

#########
# PATHS #
#########

# Full filesystem path to the project.
PROJECT_APP_PATH = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = BASE_DIR = os.path.dirname(PROJECT_APP_PATH)

STATIC_URL = '/skin/'
STATICFILES_DIRS = (os.path.join(PROJECT_ROOT, "static"),)
STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/"))

MEDIA_URL = "/media/"

MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'static', *MEDIA_URL.strip("/").split("/"))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '4bgs5cx!k#hdwwd_5owg31#_!93&u1+2^rfgyr!sper3digml2'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['127.0.0.1', ]

SITE_ID = 1

# Application definition

INSTALLED_APPS = [
    'theme',
    'jet.dashboard',
    'jet',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'appnews'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'newsproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(PROJECT_ROOT, "templates")
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'newsproject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'djongo',
        'NAME': 'django_newsproject',
        # 'USER': 'admin',
        # 'PASSWORD': 'abc#123',
        # 'HOST': '127.0.0.1',
        # 'PORT': 27017,
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

newsproject/urls.py

"""newsproject URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.conf.urls import url, include
    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin
from theme.views import homepage
from newsproject import settings
from django.conf.urls.static import static
import debug_toolbar

urlpatterns = [
    url(r'^jet/', include('jet.urls', 'jet')),  # Django JET URLS
    url(r'^jet/dashboard/', include('jet.dashboard.urls', 'jet-dashboard')),  # Django JET dashboard URLS
    url(r'^admin/', admin.site.urls),
    url(r'^$', homepage)
]

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

sau cùng chạy lần lượt các lệnh sau

$ ./manage.py migrate jet
# Chạy collectstatic nếu bạn set DEBUG = False trong settings.py
$ ./manage.py collectstatic

Django dashboard

Django category

Django news

Bài 3 trong series django đến đây là kết thúc. Mọi ý kiến góp ý của các bạn mình rất hoan nghênh. Các bạn thắc mắc có thể hỏi mình bằng cách comment tại bài viết này hoặc trên group Python Community Viet Nam. Chúc các bạn tự học python thành công.

P/s: Các bạn muốn trao đổi thảo luận nhanh có thể join group telegram để tiện trao đổi nha: https://t.me/joinchat/CZonIg9l9VX3WbbVChHRnQ

Các thẻ
Bài viết liên quan
5 nhận xét
  1. Trả lời

    Lê Mạnh

    21 Tháng 7, 2018

    Anh ơi,chưa có bài mới ạ?

    • Trả lời

      Anh Vũ Từ

      23 Tháng 7, 2018

      sorry em, hiện tại anh đang kẹt nhiều project quá chưa có thời gian để viết tiếp, anh sẽ cố gắng sắp xếp thời gian để viết tiếp

  2. Trả lời

    Shin

    23 Tháng 7, 2018

    trận chung kết cá độ bán web à anh :v, chưa thấy anh ra bài 4 nhỉ hóng quá

    • Trả lời

      Anh Vũ Từ

      23 Tháng 7, 2018

      Làm gì có cá độ web em =]] thông cảm nha, nhiều project quá mà thời gian anh có hạn, không xoay hết được :(

  3. Trả lời

    Cương

    21 Tháng 10, 2018

    Cảm ơn anh rất nhiều. bài viết rất hay. mong ngóng tiếp bài mới.!!!

Nhận xét mới

bắt buộc

yu.kusanagi
Từ Anh Vũ
Hồ Chí Minh, Việt Nam

Xin chào, tôi tên Từ Anh Vũ và là 1 free lancer developer và ngôn ngữ code yêu thích của tôi là Python và PHP. Công việc chủ yếu là viết các module cho magento, magento2, wordpress, django, flask và các framework khác
Nếu bạn muốn trao đổi với tôi hoặc muốn thuê tôi làm việc cho dự án của bạn, hãy liên hệ với tôi

ĐĂNG KÝ NHẬN BÀI MỚI

Tweets gần đây
Tác giả
Feeds
RSS / Atom
ADVERTISING

Đăng ký nhận bài viết mới tại hocpython.com?

Hãy đăng ký nhận bài viết mới tại hocpython.com để:

  • Không bỏ lỡ các bài tutorials mới tại hocpython.com!
  • Cập nhật các công nghệ mới trong python!

Chỉ cần điền email và họ tên của bạn và nhấn Đăng ký nhận tin!