Как в Django переопределить метод "read" только в/для администратора?

Я знаю, что они этого не делают, но для одного из моих домашних проектов я хочу странную вещь: хранить jinja-шаблоны в базе данных (и иметь возможность редактировать их через админ-панель).

Есть что-то вроде этой модели (в models.py):

class TbTemplate(models.Model):
    szFileName = models.CharField(
        primary_key=True,
        db_index=True,
        unique=True, 
        verbose_name="Path/Name"
        )
    szJinjaCode = models.TextField(
        verbose_name='Template',
        help_text='Template Code (jinja2)'
    )
    szDescription = models.CharField(
        max_length=100,
        verbose_name='Description'
    )

    def __unicode__(self):
        return f"{self.szFileName} ({self.szDescription})"

    def __str__(self):
        return self.__unicode__()

    class Meta:
        verbose_name = '[…Template]'
        verbose_name_plural = '[…Templates]'

Далее, в view.py вы можете сделать что-то вроде этого:

# -*- coding: utf-8 -*-
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string
from web.models import TbTemplate

def something(request: HttpRequest, template: str) -> HttpResponse:
    """
    :param request: http-in
    :param template: Template name
    :return response: http-out
    """
    to_template = {}
    # ...
    # ... do smth
    # ...
    tmpl = TbTemplate.objects.get(pk=template)
    html = render_to_string(tmpl.szJinjaCode, to_template)
    return HttpResponse(html)

И все работает. Шаблоны доступны для редактирования через админ-панель (конечно, нужно повесить "зеркало"-подобный виджет для подсветки синтаксиса и т.д.)...

Но я хочу использовать в jinja шаблоны типа: {% include "something_template.jinja2" %} ... А для этого необходимо, чтобы шаблоны были не только в базе данных, но и хранились в виде файлов в папке templates-folder.

Кроме того, шаблоны проще создавать и редактировать в IDE, а доступ к шаблонам через панель администратора только для косметических изменений.

Затем мне нужно как-то перехватить метод "read" в/из админпанели. Чтобы если шаблон в таблице TbTemplate открыт для редактирования в админ панели, то для szJinjaCode он был прочитан не из базы данных, а из соответствующего szFileName-файла.

Как это сделать?

Интересный вопрос.

Первая часть - тег включения

Ваша основа - вы хотите использовать тег включения. Поэтому вы хотите сохранить что-то в файл. Но вы можете просто переопределить загрузчик шаблонов, который получает перед файлом шаблон из базы данных:

#settings.py:
TEMPLATES = [
    {
        'BACKEND': 'myapp.backends.MyTemplate',
    ...  # other staff 
    },
]

в myapp/backends.py:

class MyTemplate(Jinja2):

    def get_template(self, template_name):
        template = TbTemplate.objects.filter(pk=template_name).first()
        if template:
            return self.from_string(template.szJinjaCode)
        return super().get_template(template_name)

После этого - каждый шаблон может быть сохранен в БД, {% include %} вызовите template_backend, который получит шаблон из БД перед файлом-шаблоном.

Вторая часть. Сохраните шаблон в файл/базу данных.

Если вы сделаете это, вам не понадобится первая часть, каждый раз, когда шаблон-файл должен быть сохранен/изменен.

class TbTemplateAdmin(ModelAdmin):

    def save_model(self, request, obj, *args, **kwargs):
        super().save(request, obj, *args, **kwargs)
        with open( Path(path_to_templates) / obj.szFileName, "w+" ) as template:
            template.write(obj.szJinjaCode)

Третья часть - получить файл в админке на get_object:

class TbTemplateAdmin(ModelAdmin):
    
    def get_object(self, *args, **kwargs):
        obj = super().get_object(self, *args, **kwargs)
        with open( Path(path_to_templates) / obj.szFileName, "r" ) as template:
            obj.szJinjaCode = template.read()
        return obj

Последняя часть - преобразование новых шаблонов файлов в объекты:

В наших проектах мы автоматически добавляем новые шаблоны в базу данных, спойлер - с тегами включения. В вашем случае - вы можете создать ModelAdmin.action для добавления шаблонов в базу данных. Я не решаю эту проблему за вас, попробуйте сделать что-то сами. Надеюсь на ваше понимание

Только один палец я потерял здесь. Если вы используете Cached Template Loader, а вы должны использовать его на продакшене, то в этом случае вы должны обновлять кэш для измененных шаблонов. Не забывайте об этом.

Это делается в два этапа:

Во-первых,

в models.py мы переопределим метод save() для модели TbTemplate. В то же время мы можем переопределить метод delete(), чтобы он ничего не удалял (или наоборот, удалял не только запись в базе данных, но и соответствующий teplate-файл... или удалял запись в базе данных, а соответствующий файл переименовывал...). Получаем такую модель:

# -*- coding: utf-8 -*-
from django.db import models
from project.settings import *


class TbTemplate(models.Model):
    szFileName = models.CharField(
        primary_key=True, db_index=True, unique=True, 
        verbose_name="Path/Name"
        )
    szJinjaCode = models.TextField(
        default='', null=True, blank=True,
        verbose_name='Template',
        help_text='Template Code (jinja2)'
    )
    szDescription = models.CharField(
        max_length=100,
        verbose_name='Description'
    )

    def __unicode__(self):
        return f"{self.szFileName} ({self.szDescription})"

    def __str__(self):
        return self.__unicode__()

    def save(self, *args, **kwargs):
        with open(TEMPLATES_DIR / self.szFileName, "w+", encoding="utf-8") as file:
            file.write(self.szJinjaCode)
        super(TbTemplate, self).save(*args, **kwargs)
        # TODO: for production, need to add some code for modify
        #       touch_reload file for uWSGI reload

    def delete(self, *args, **kwargs):
        pass
        # ... or do smth ... and after:
        # super(TbTemplate, self).delete(*args, **kwargs)

    class Meta:
        verbose_name = '[…Template]'
        verbose_name_plural = '[…Templates]'

теперь, при изменении и создании шаблона через Django-Admin, будет создаваться/изменяться соответствующий файл шаблона.

Во-вторых,

в файле admin.py при определении класса admin.ModelAdmin для модели управления TbTemplate необходимо переопределить метод get_fields (), который отвечает за получение полей в админской форме. ( мне пришлось искать метод тупо перебирая множество вариантов, в которых сделано что-то похожее, но не то). В результате получилось что-то вроде этого admin.py:

# -*- coding: utf-8 -*-
from django.contrib import admin
from my_appweb.models import TbTemplate
from project.settings import *

class AdminTemplate(admin.ModelAdmin):
    search_fields = ['szFileName', 'szDescription', 'szJinjaCode']
    list_display = ('szFileName', 'szDescription')
    list_display_links = ('szFileName', 'szDescription', )
    empty_value_display = '<b style=\'color:red;\'>—//—</b>'
    actions_on_top = False
    actions_on_bottom = True

    def get_fields(self, request, obj=None):
        try:
            with open(Path(TEMPLATES_DIR) / obj.szFileName, "r", encoding="utf-8") as file:
                obj.szJinjaCode = file.read()
        except (AttributeError, FileNotFoundError, TypeError):
            pass
        return ['szFileName', 'szDescription', 'szJinjaCode']

admin.site.register(TbTemplate, AdminTemplate)

Теперь, если какие-то "внешние силы" изменят файл шаблона (или кто-то изменит код шаблона в базе данных в обход панели администратора), то когда вы откроете шаблон для редактирования в панели администратора, данные все равно будут получены из файла.

Все

P.S. в settnig.py проекта нужно добавить что-то вроде:

TEMPLATES_DIR = BASE_DIR / 'templates-jinja2'

Чтобы модель и админ-панель знали, в какую директорию заливать файлы шаблона.

Вернуться на верх