How in Django to override the "read" method only in/for the admin?

Viewed 48

I know that they don't do this, but for one of my pet-projects I want a strange thing: store jinja-templates in the database (and be able to edit them through the admin panel).

There is something like this model (in 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]'

Next, in view.py you can do something like this:

# -*- 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)

And everything works. Templates to available for editing through the admin panel (of course, you need to hang a "mirror"-like widget for syntax highlighting, etc.)...

But I want to use in jinja templates like: {% include "something_template.jinja2" %} ... And for this it is necessary that the templates are not only in the database, but also stored as files in the templates-folder.

In addition, templates are easier to create and edit in IDEs, and access to templates through the admin panel only for cosmetic changes.

And then I need to somehow intercept the "read" method in/for the admin panel. So that if a template in the TbTemplate table is opened for editing in the admin panel, then for the szJinjaCode it was read not from the database, but from the corresponding szFileName-file.

How to do this?

2 Answers

Interesting question.

The first part - inclusion tag

Your foundation - you want to use inclusion tag. Therefore you want to save something in file. But you can simply override template loader, who get before file the template from the database:

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

in 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)

After that - every template can be saved in DB, {% include %} call template_backend which get template from database before file-template.

The second part. Save template to file/database.

If you do it, you don't need the first part, every time the template-file should be saved/changed.

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)

The Third part - get the file in admin on 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

The Last part - convert the new file templates to objects:

In our projects we add automatically the new templates to database, spoiler - with inclusion tags. In your case - you can create an ModelAdmin.action to add templates in database. I don't solve it for you, try to do something yourself. I hope for your understanding

Only one fing I lost here. If you use Cached Template Loader, and you should use it on production, in this case you should refresh cache for changed templates. Don't forget about it.

It is done in two steps:

Firstly,

in models.py we will override the save() method for the TbTemplate model. At the same time, we can override delete() method, so that it do not delete anything (or vice versa, it deletes not only the entry in the database, but also the corresponding teplate-file... or deletes the entry in the database, and renames the corresponding file...). We get this model:

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


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):
        path_filename = TEMPLATES_DIR / self.szFileName
        if not os.path.exists(os.path.dirname(path_filename)):
            os.makedirs(os.path.dirname(path_filename))
        with open(path_filename, "w+", encoding="utf-8") as file:
            file.write(self.szJinjaCode)
        # TODO: for production, need to add some code for modify
        #       touch_reload file for uWSGI reload
        super(TbTemplate, self).save(*args, **kwargs)

    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]'

now, when changing and creating a template through Django-Admin, a corresponding template file will be create/modify.

Secondly,

in the admin.py file, when define the admin.ModelAdmin class for the TbTemplate control model, it is necessary to override the get_fields () method, which is responsible for getting the fields in the admin form. (I had to look for a method stupidly trying many cases that are made something similar, but not that). As a result, something like this admin.py:

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

class AdminTemplate(admin.ModelAdmin):
    list_display = ('szFileName', 'szDescription')
    list_display_links = ('szFileName', 'szDescription', )

    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)

Now, if some "external forces" change the template file (or someone changes the template code in the database bypassing the admin panel), then when you open the template for editing in the admin panel, the data will still be received from the file.

It's all

P.S. in the settnig.py of the project, you need to add something like:

TEMPLATES_DIR = BASE_DIR / 'templates-jinja2'

So that the model and the admin panel know in which directory to pour the template files.

Related