Django bài 3: Model - database cho site tin tức
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
Đâ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
- 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.
- Function models của django, ta sẽ override models.Model để tạo model cho appnews
- 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']
- Ở 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
- 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
- Để 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
- 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
- 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
- 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
- Package admin dùng để register admin section đến django
- 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
- 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
- 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
- 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
- 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
- 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ử
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
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
Và khi ta edit categories, sort_order phải hiện ra
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ụ:
- 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.
- 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
)
- 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
- 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
- Rename lại tên column trong list display
- Sắp sếp thứ tự category theo tên
- Ta sẽ hiển thị một cột filter bài viết theo categories trong danh sách 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
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)
- Đầu tiên ta sẽ set date_hierarchy = None để tránh lỗi xảy ra
- 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
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.
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
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
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
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
Lê Mạnh
Anh ơi,chưa có bài mới ạ?
Anh Vũ Từ
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
Shin
trận chung kết cá độ bán web à anh :v, chưa thấy anh ra bài 4 nhỉ hóng quá
Anh Vũ Từ
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 :(
Cương
Cảm ơn anh rất nhiều. bài viết rất hay. mong ngóng tiếp bài mới.!!!
Công ty dịch thuật
Công ty dịch thuật chuyên dịch các tài liệu chuyên ngành tại thành phố Hồ Chí Minh, quận 1. Địa chỉ website: https://congtydichthuat.vn/.
Với uy tín và trình độ dịch chính xác cao, chắc chắn sẽ làm hài lòng các khách hàng khó tính nhất.
Công ty còn có cả các dịch vụ khác như dịch thuật công chứng: https://congtydichthuat.vn/dich-thuat-cong-chung/
dịch vụ dịch thuật chuyên ngành, công ty dịch thuật hcm, dịch thuật công chứng quận 1
Jacky
Anh ơi vẫn chưa có bài mới ạ. Bài viết của anh rất chi tiết.
Tu nguyen
Cám ơn tác giả, bài viết rất chi tiết!