Tự động tạo Slug Django trong Adminpanel

Phương pháp 1:

Bước 1. Tệp app/models.py

class Brand(models.Model):
    title = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)  # Slug thương hiệu
    ...
    def __str__(self):
        return self.title

Bước 2. Tệp app/admin.py

class BrandAdmin(admin.ModelAdmin):
    ...
    prepopulated_fields = {'slug': ('title',)}   # Chuyển tiêu đề sang slug
admin.site.register(Brand, BrandAdmin)

OK, slug được tạo tự động khi bạn gõ tiêu đề.

slug-django

Bước 3.

Cập nhật: Đã có cách thực hiện bước này tối ưu hơn ở bài viết số #7 bên dưới.

Để hiển thị được tiếng việt không dấu như trên, Sửa tệp urlify.js, Xem tệp gốc ở đây

Trong trường hợp của tôi: \Python\Python39\Lib\site-packages\django\contrib\admin\static\admin\js

Thêm mã bên dưới vào trước const ALL_DOWNCODE_MAPS, đồng thời vô hiệu SERBIAN_MAP.
Tệp của bạn sẽ trông giống như sau:

// Chuyển tiếng việt có dấu sang không dấu
...
const VIETNAM_MAP = {
        'á': 'a', 'à': 'a', 'ạ': 'a', 'ả': 'a', 'ã': 'a', 'ă': 'a', 'ắ': 'a', 'ằ': 'a', 'ặ': 'a',
        'ẳ': 'a', 'ẵ': 'a', 'â': 'a', 'ấ': 'a', 'ầ': 'a', 'ậ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'Á': 'a',
        'À': 'a', 'Ạ': 'a', 'Ả': 'a', 'Ã': 'a', 'Ă': 'a', 'Ắ': 'a', 'Ằ': 'a', 'Ặ': 'a', 'Ẳ': 'a',
        'Ẵ': 'a', 'Â': 'a', 'Ấ': 'a', 'Ầ': 'a', 'Ậ': 'a', 'Ẩ': 'a', 'Ẫ': 'a',
        'đ': 'd', 'Đ': 'd',
        'è': 'e', 'é': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ẹ': 'e',
        'ê': 'e', 'ề': 'e', 'ế': 'e', 'ể': 'e', 'ễ': 'e', 'ệ': 'e',
        'È': 'e', 'É': 'e', 'Ẻ': 'e', 'Ẽ': 'e', 'Ẹ': 'e',
        'Ê': 'e', 'Ề': 'e', 'Ế': 'e', 'Ể': 'e', 'Ễ': 'e', 'Ệ': 'e',
        'ì': 'i', 'í': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i',
        'Ì': 'i', 'Í': 'i', 'Ỉ': 'i', 'Ĩ': 'i', 'Ị': 'i',
        'ò': 'o', 'ó': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o',
        'ô': 'o', 'ồ': 'o', 'ố': 'o', 'ổ': 'o', 'ỗ': 'o', 'ộ': 'o',
        'ơ': 'o', 'ờ': 'o', 'ớ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
        'Ò': 'o', 'Ó': 'o', 'Ỏ': 'o', 'Õ': 'o', 'Ọ': 'o',
        'Ô': 'o', 'Ồ': 'o', 'Ố': 'o', 'Ổ': 'o', 'Ỗ': 'o', 'Ộ': 'o',
        'Ơ': 'o', 'Ờ': 'o', 'Ớ': 'o', 'Ở': 'o', 'Ỡ': 'o', 'Ợ': 'o',
        'ù': 'u', 'ú': 'u', 'ủ': 'u', 'ũ': 'u', 'ụ': 'u',
        'ư': 'u', 'ừ': 'u', 'ứ': 'u', 'ử': 'u', 'ữ': 'u', 'ự': 'u',
        'Ù': 'u', 'Ú': 'u', 'Ủ': 'u', 'Ũ': 'u', 'Ụ': 'u',
        'Ư': 'u', 'Ừ': 'u', 'Ứ': 'u', 'Ử': 'u', 'Ữ': 'u', 'Ự': 'u',
        'ỳ': 'y', 'ý': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y',
        'Y': 'y', 'Ỳ': 'y', 'Ý': 'y', 'Ỷ': 'y', 'Ỹ': 'y', 'Ỵ': 'y'
    };
    const ALL_DOWNCODE_MAPS = [
        VIETNAM_MAP,   // <-- Thêm vào
        ...
        // SERBIAN_MAP,  // Vô hiệu hoặc xóa dòng này
        ...

Phương pháp 2: Sử dụng phần mở rộng AutoSlugField

Cài đặt:

pip install django-autoslug
pip install python-slugify

Sử dụng:

#models.py
from autoslug import AutoSlugField
from slugify import slugify as default_slugify

class Brand(models.Model):
    title = models.CharField(max_length=100)

    def custom_slugify(value):
        return default_slugify(value)

    slug = AutoSlugField(populate_from='title', max_length=100, slugify=custom_slugify)
    ...
    def __str__(self):
        return self.title

Tham khảo thêm: django-autoslug · PyPI

:warning: Bạn có thể sử sụng slugfiy mặc định, tuy nhiên với tiêu đề có chứa chữ đ chẳng hạn, khi tạo slug nó sẽ xóa hẳn chữ đ mà ko chuyển thành chữ d. Vì vậy ta sử dụng gói python-slugify thay thế. Nó tạo slug từ Tiếng Việt tốt hơn.

mình làm theo cách 1 thì không chạy, slug vẫn vậy
c2 thì báo lỗi xung đột với package khác
còn cách nào không bạn

Thử lại cách 1 với bước 1 và 2. Nó hoạt động mới làm bước 3. Lưu ý các fields của bạn có thể khác với hướng dẫn.

Trong bước 2 mình có tài thêm pip install python-slugify nhưng nó vẫn bị nuốt chữ đ b ơi

ai bị lỗi ở cách 1 thì xóa cache trang web là được nhé

1 Lượt thích

Cập nhật cho Phương pháp 1.

Giả sử bạn đã thực hiện xong bước 1 và 2 ở Phương pháp 1. Tại bước 3, chúng ta sẽ không sửa trực tiếp tệp urlify.js mà sẽ tạo một bản sao của nó trong dự án, cụ thể như sau.

Với cách mới này, chúng ta sẽ không phải chỉnh sửa urlify khi có các bản cập nhật hoặc thay đổi môi trường code.

Bước 3 (Mới): Tạo tệp urlify_vn.js tùy chỉnh

  • Tạo cấu trúc thư mục sau: (shop/app)/static/admin/js và tạo tệp urlify_vn.js (có thể đặt tên tệp tùy thích)

  • Nội dung của tệp urlify_vn.js chúng ta sao chép từ tệp gốc và thêm VIETNAM_MAP, đồng thời vô hiệu hóa SERBIAN_MAP. Để thuận tiện, mình sẽ để nội dung tệp đã chỉnh sửa bên dưới:

/*global XRegExp*/
/* Edit by Aznet.io */
'use strict';
{
    const VIETNAMESE_MAP = {
        'á': 'a', 'à': 'a', 'ạ': 'a', 'ả': 'a', 'ã': 'a', 'ă': 'a', 'ắ': 'a', 'ằ': 'a', 'ặ': 'a',
        'ẳ': 'a', 'ẵ': 'a', 'â': 'a', 'ấ': 'a', 'ầ': 'a', 'ậ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'Á': 'a',
        'À': 'a', 'Ạ': 'a', 'Ả': 'a', 'Ã': 'a', 'Ă': 'a', 'Ắ': 'a', 'Ằ': 'a', 'Ặ': 'a', 'Ẳ': 'a',
        'Ẵ': 'a', 'Â': 'a', 'Ấ': 'a', 'Ầ': 'a', 'Ậ': 'a', 'Ẩ': 'a', 'Ẫ': 'a',
        'đ': 'd', 'Đ': 'd',
        'è': 'e', 'é': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ẹ': 'e',
        'ê': 'e', 'ề': 'e', 'ế': 'e', 'ể': 'e', 'ễ': 'e', 'ệ': 'e',
        'È': 'e', 'É': 'e', 'Ẻ': 'e', 'Ẽ': 'e', 'Ẹ': 'e',
        'Ê': 'e', 'Ề': 'e', 'Ế': 'e', 'Ể': 'e', 'Ễ': 'e', 'Ệ': 'e',
        'ì': 'i', 'í': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i',
        'Ì': 'i', 'Í': 'i', 'Ỉ': 'i', 'Ĩ': 'i', 'Ị': 'i',
        'ò': 'o', 'ó': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o',
        'ô': 'o', 'ồ': 'o', 'ố': 'o', 'ổ': 'o', 'ỗ': 'o', 'ộ': 'o',
        'ơ': 'o', 'ờ': 'o', 'ớ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
        'Ò': 'o', 'Ó': 'o', 'Ỏ': 'o', 'Õ': 'o', 'Ọ': 'o',
        'Ô': 'o', 'Ồ': 'o', 'Ố': 'o', 'Ổ': 'o', 'Ỗ': 'o', 'Ộ': 'o',
        'Ơ': 'o', 'Ờ': 'o', 'Ớ': 'o', 'Ở': 'o', 'Ỡ': 'o', 'Ợ': 'o',
        'ù': 'u', 'ú': 'u', 'ủ': 'u', 'ũ': 'u', 'ụ': 'u',
        'ư': 'u', 'ừ': 'u', 'ứ': 'u', 'ử': 'u', 'ữ': 'u', 'ự': 'u',
        'Ù': 'u', 'Ú': 'u', 'Ủ': 'u', 'Ũ': 'u', 'Ụ': 'u',
        'Ư': 'u', 'Ừ': 'u', 'Ứ': 'u', 'Ử': 'u', 'Ữ': 'u', 'Ự': 'u',
        'ỳ': 'y', 'ý': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y',
        'Y': 'y', 'Ỳ': 'y', 'Ý': 'y', 'Ỷ': 'y', 'Ỹ': 'y', 'Ỵ': 'y'
    };
    const LATIN_MAP = {
        'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE',
        'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I',
        'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O',
        'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U',
        'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a',
        'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c',
        'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i',
        'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o',
        'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
        'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
    };
    const LATIN_SYMBOLS_MAP = {
        '©': '(c)'
    };
    const GREEK_MAP = {
        'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h',
        'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3',
        'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f',
        'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o',
        'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y',
        'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z',
        'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N',
        'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y',
        'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I',
        'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y'
    };
    const TURKISH_MAP = {
        'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u',
        'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G'
    };
    const ROMANIAN_MAP = {
        'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a',
        'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A'
    };
    const RUSSIAN_MAP = {
        'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
        'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm',
        'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
        'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '',
        'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
        'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo',
        'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M',
        'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U',
        'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '',
        'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya'
    };
    const UKRAINIAN_MAP = {
        'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i',
        'ї': 'yi', 'ґ': 'g'
    };
    const CZECH_MAP = {
        'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't',
        'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R',
        'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z'
    };
    const SLOVAK_MAP = {
        'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'í': 'i', 'ľ': 'l',
        'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ŕ': 'r', 'š': 's', 'ť': 't',
        'ú': 'u', 'ý': 'y', 'ž': 'z',
        'Á': 'a', 'Ä': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Í': 'I', 'Ľ': 'L',
        'Ĺ': 'L', 'Ň': 'N', 'Ó': 'O', 'Ô': 'O', 'Ŕ': 'R', 'Š': 'S', 'Ť': 'T',
        'Ú': 'U', 'Ý': 'Y', 'Ž': 'Z'
    };
    const POLISH_MAP = {
        'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's',
        'ź': 'z', 'ż': 'z',
        'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S',
        'Ź': 'Z', 'Ż': 'Z'
    };
    const LATVIAN_MAP = {
        'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l',
        'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z',
        'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L',
        'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z'
    };
    const ARABIC_MAP = {
        'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd',
        'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't',
        'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm',
        'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y'
    };
    const LITHUANIAN_MAP = {
        'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u',
        'ū': 'u', 'ž': 'z',
        'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U',
        'Ū': 'U', 'Ž': 'Z'
    };
    const SERBIAN_MAP = {
        'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz',
        'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C',
        'Џ': 'Dz', 'Đ': 'Dj'
    };
    const AZERBAIJANI_MAP = {
        'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u',
        'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U'
    };
    const GEORGIAN_MAP = {
        'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z',
        'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o',
        'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f',
        'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz',
        'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h'
    };


    const ALL_DOWNCODE_MAPS = [
        VIETNAMESE_MAP,
        LATIN_MAP,
        LATIN_SYMBOLS_MAP,
        GREEK_MAP,
        TURKISH_MAP,
        ROMANIAN_MAP,
        RUSSIAN_MAP,
        UKRAINIAN_MAP,
        CZECH_MAP,
        SLOVAK_MAP,
        POLISH_MAP,
        LATVIAN_MAP,
        ARABIC_MAP,
        LITHUANIAN_MAP,
        // SERBIAN_MAP,
        AZERBAIJANI_MAP,
        GEORGIAN_MAP
    ];

    const Downcoder = {
        'Initialize': function () {
            if (Downcoder.map) { // already made
                return;
            }
            Downcoder.map = {};
            for (const lookup of ALL_DOWNCODE_MAPS) {
                Object.assign(Downcoder.map, lookup);
            }
            Downcoder.regex = new RegExp(Object.keys(Downcoder.map).join('|'), 'g');
        }
    };

    function downcode(slug) {
        Downcoder.Initialize();
        return slug.replace(Downcoder.regex, function (m) {
            return Downcoder.map[m];
        });
    }


    function URLify(s, num_chars, allowUnicode) {
        // changes, e.g., "Petty theft" to "petty-theft"
        if (!allowUnicode) {
            s = downcode(s);
        }
        s = s.toLowerCase(); // convert to lowercase
        // if downcode doesn't hit, the char will be stripped here
        if (allowUnicode) {
            // Keep Unicode letters including both lowercase and uppercase
            // characters, whitespace, and dash; remove other characters.
            s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), '');
        } else {
            s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars
        }
        s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces
        s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens
        s = s.substring(0, num_chars); // trim to first num_chars chars
        s = s.replace(/-+$/g, ''); // trim any trailing hyphens
        return s;
    }

    window.URLify = URLify;
}

Bước 4: Tùy chỉn Admin Form

Tiếp theo, chúng ta cần ghi đè slugify_vn.js lên slugify.js.

Tạo tệp /templates/admin/change_form.html

Thêm nội dung sao vào tệp change_form.html

{% extends 'admin/change_form.html' %}

{% load static %}
{% block extrahead %}
    {{ block.super }}
    <script src="{% static 'admin/js/urlify_vn.js' %}"></script>
{% endblock %}

Kiểm tra nếu urlify_vn.js nằm dưới urlify.js là ok.

Vậy là xong.