Skip to content

Extend Func using Django's template engine #117

Closed

Description

@adonig

Hi @opus10! I'm not sure whether this is something that might be interesting to include or mention in the documentation:

import pgtrigger
from django.template import Context, Template


class TemplateFunc(pgtrigger.Func):
    def render(self, model):
        fields = {field.name: field for field in model._meta.fields}
        columns = {field.name: field.column for field in model._meta.fields}
        template = Template(self.func)
        context = Context({"meta": model._meta, "fields": fields, "columns": columns})
        return template.render(context)

It allows you to use Django's template engine to write trigger functions. I had the following use case:

   class Meta:
        triggers = [
            pgtrigger.Trigger(
                name="product_upsert",
                operation=pgtrigger.Insert,
                timing=pgtrigger.Immediate,
                when=pgtrigger.After,
                func=TemplateFunc(
                    """
                        IF EXISTS (
                            SELECT *
                            FROM {{ meta.db_table }}
                            WHERE {{ columns.gtin }}=NEW.{{ columns.gtin }} OR (
                                {{ columns.category }}=NEW.{{ columns.category }} AND
                                {{ columns.subcategory }}=NEW.{{ columns.subcategory }} AND
                                {{ columns.vendor }}=NEW.{{ columns.vendor }} AND
                                {{ columns.model }}=NEW.{{ columns.model }}
                            )
                        ) THEN
                            UPDATE {{ meta.db_table }} SET
                            {% for column in columns %}
                            {% if column != "id" and column != "created" %}
                                {{ column }}=NEW.{{ column }}{% if not forloop.last %},{% endif %}
                            {% endif %}
                            {% endfor %}
                            WHERE {{ columns.gtin }}=NEW.{{ columns.gtin }} OR (
                                {{ columns.category }}=NEW.{{ columns.category }} AND
                                {{ columns.subcategory }}=NEW.{{ columns.subcategory }} AND
                                {{ columns.vendor }}=NEW.{{ columns.vendor }} AND
                                {{ columns.model }}=NEW.{{ columns.model }}
                            );
                            RETURN NULL;
                        ELSE
                            RETURN NEW;
                        END IF;
                    """
                ),
            ),
        ]

Activity

  1. adonig commented on Feb 20, 2023

    @adonig
    Author

    Here's a typed version that allows to specify an extra context:

    class TemplateFunc(pgtrigger.Func):
        def __init__(self, func: str, extra_context: dict[str, Any] | None = None) -> None:
            super().__init__(func)
            self.extra_context = extra_context or {}
    
        def render(self, model: type[models.Model]) -> str:
            fields = {field.name: field for field in model._meta.fields}
            columns = {field.name: field.column for field in model._meta.fields}
            template = Template(self.func)
            context = Context({**{"meta": model._meta, "fields": fields, "columns": columns}, **self.extra_context})
            return template.render(context)
  2. wesleykendall commented on Jun 8, 2023

    @wesleykendall
    Member

    @adonig I really like this suggestion!

    I'm happy to accept this as a PR to django-pgtrigger as is. I can also open a PR and add you as a Co-authored-by in the comment.

    One suggestion - I wonder if there's a way to grab these variables for other models. It's common to write a trigger across a few tables.

    Could also throw in a basic template tag for people that want to write functions over other models:

    """
    {% load meta from pgtrigger %}
    
    {% meta "app.MyModel" as my_model_meta %}
    
    SELECT * from {{ my_model_meta.db_name }}
    """
    

    Not sure if that's totally correct, but I hope it makes sense.

  3. adonig commented on Jun 9, 2023

    @adonig
    Author

    @wesleykendall I'm glad it turns out to become useful. The template tag sounds like a good idea. It might look like this:

    from django import template
    from django.apps import apps
    from django.db.models.options import Options
    
    register = template.Library()
    
    @register.simple_tag(name="meta")
    def meta(model_name: str) -> Options:    
        model = apps.get_model(model_name)
        return model._meta
  4. locked and limited conversation to collaborators on Aug 27, 2024
  5. converted this issue into a discussion #165 on Aug 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions