idlesign / django-etc

Tiny stuff for Django that won't fit into separate apps.
https://github.com/idlesign/django-etc
BSD 3-Clause "New" or "Revised" License
39 stars 1 forks source link

Get help_text from model instance #3

Open djjudas21 opened 3 years ago

djjudas21 commented 3 years ago

Hi there. I'm a django beginner and I'm trying to figure out how to display a field's help_text in a DetailView. I see you've got a template tag that can display the help text based on a model.field reference, but I need to display the text based on a field from a model instance object, where object is the entire record and object.field is the field.

I want to do something like this:

{% model_field_help_text from object %}

In the wider context of displaying some data in a DetailView template like this:

  <tr>
    <td>
      <strong>{% model_field_verbose_name from object.elements %}</strong>
      {% model_field_help_text from object.elements %}
    </td>
    <td>{{ object.elements }}</td>
  </tr>

Is it possible to somehow use the existing tags by referencing an instance, instead of a model? Thanks

idlesign commented 3 years ago

Hi. Those tags are made to accept both model instances and classes. So if your object is an instance it should be fine. Have you tried it?

djjudas21 commented 3 years ago

Hi, thanks for your reply. Yes I did try passing in an instance as object but I think I ran into problems because I was using a custom template tag to wrap up the complexity. This meant that my custom tag was being passed object.elements, but then inside my custom tag it only had a simple variable like object, which can't be used directly with {% model_field_help_text %} because that expects a variable formatted as model.field.

I hope I've explained this properly. Basically I think my issue is not with getting the right data, but with validation on {% model_field_help_text %}.

My end goal is to be able to use simple syntax in the main template like this:

 <tr>
    <td>
      {% titlecell object.elements %}
    </td>
    <td>{{ object.elements }}</td>
  </tr>

And then the titlecell template tag would render this:

{% load model_field %}
<td>
  {% model_field_verbose_name from object %}
  <br>
  <small class="text-muted">{% model_field_help_text from object %}</small>
</td>

Is there an easy way to achieve this? Thanks.

idlesign commented 3 years ago

So the behaviour or your custom tag compleately depends on its (tag) implementation. To implement it properly you may want to take a look at how FieldAttrNode works.

djjudas21 commented 3 years ago

OK, I've read your code and I don't 100% understand everything, but what I want is to effectively skip the check in https://github.com/idlesign/django-etc/blob/master/etc/templatetags/model_field.py#L94

Logically in my my main template I want to set variable = object.field, pass variable into a custom tag, and have that tag render {% model_field_help_text from object %}, so it has access to everything it needs, just the calling syntax is different.

I want to write as little code as possible to wrap your template tags because anything I write will have to be maintained.

idlesign commented 3 years ago

but what I want is to effectively skip the check in

I'd advise to reformulate the task to make it more easy: "make it not to skip but pass the check". In that case the main objective of your custom tag would be just to accept object as token, make it object.elements, and to pass that object.elements to FieldAttrNode as field argument. That should solve the task.

djjudas21 commented 3 years ago

Good point. This is basically what I've been trying to do with with my experiments with tags and templates, but I haven't managed to make it work yet. Could you give a brief code example, please? I'm trying to understand exactly what you mean - you mean my custom tag would call some of the functions from your module, but not use your tags? Thanks

idlesign commented 3 years ago

Could you give a brief code example, please?

Sorry, have work to do these days.

I'm trying to understand exactly what you mean - you mean my custom tag would call some of the functions from your module, but not use your tags

Yeah, if you want a quick solution with code reuse. Basically:

djjudas21 commented 3 years ago

Great, thanks, this helps a lot. I'll have another go tonight when I get off work (sysadmin) :+1:

djjudas21 commented 3 years ago

OK. This is what I've got now:

Detail view template:

...
{% if object.manufacturer is not None %}
  <tr>
    <td>{% titledescription from object.manufacturer %}</td>
  </tr>
{% endif %}
...

And here's my custom tag, which is mostly copied from your function _get_model_field_attr:

from django import template
from etc.templatetags.model_field import FieldAttrNode

register = template.Library()

@register.tag
def titledescription(parser, token):
    tag_name = 'titledescription'

    tokens = token.split_contents()
    tokens_num = len(tokens)

    if tokens_num not in (3, 5):
        raise template.TemplateSyntaxError(
            '`%(tag_name)s` tag requires two or four arguments. '
            'E.g.: {%% %(tag_name)s from model.field %%} or {%% %(tag_name)s from model.field as myvar %%}.'
            % {'tag_name': tag_name}
        )

    field = tokens[2]
    as_var = None

    tokens = tokens[3:]
    if len(tokens) >= 2 and tokens[-2] == 'as':
        as_var = tokens[-1]

    # FieldAttrNode(field, attr_name, tag_name, as_var)
    verbose_name = FieldAttrNode(field, 'verbose_name', 'td') #, as_var)
    help_text = FieldAttrNode(field, 'help_text', 'td') #, as_var)

    return "{}<br><small class=\"text-muted\">{}</small>".format(verbose_name, help_text)

Rendering this template errors with 'str' object has no attribute 'must_be_first' and the highlighted line of the template is

{% if object.manufacturer is not None %}

I found a StackOverflow answer which suggests a link to django.template.Node but that's already imported in your class. Sorry to bug you but I have no idea how to troubleshoot this - thanks.

idlesign commented 3 years ago

Sorry for the delay. The following will render help text for elements attribute when used with {% mytag mymodel %}.

@register.tag
def mytag(parser, token):
    return FieldAttrNode(
        field=f'{token.split_contents()[1]}.elements',
        attr_name='help_text',
        tag_name='mytag'
    )

Hope it helps somehow.

djjudas21 commented 3 years ago

Thanks for the explanation. Using your example I can correctly use the tag in the top-scope template like `{% mytag mymodel %}. But I can't get it to work when called from an inclusion template, or a sub template. I want to minimise the number of template tag calls and reuse the formatting, so I want to do maybe this:

option 1

@register.tag
def mytag3(parser, token):
    help_text = FieldAttrNode(
        field=f'{token.split_contents()[1]}.elements',
        attr_name='help_text',
        tag_name='mytag'
    )
    verbose_name = FieldAttrNode(
        field=f'{token.split_contents()[1]}.elements',
        attr_name='verbose_name',
        tag_name='mytag'
    )
    return str("{}<br><small class=\"text-muted\">{}</small>".format(verbose_name, help_text))

Fails with

'str' object has no attribute 'must_be_first'

option 2

or this

@register.inclusion_tag('td2.html')
def mytag2(parser, token):
    help_text = FieldAttrNode(
        field=f'{token.split_contents()[1]}.elements',
        attr_name='help_text',
        tag_name='mytag'
    )
    verbose_name = FieldAttrNode(
        field=f'{token.split_contents()[1]}.elements',
        attr_name='verbose_name',
        tag_name='mytag'
    )
    return {
        'help_text': str(help_text),
        'verbose_name': str(verbose_name),
    }
  {{ verbose_name }}
  <br>
  <small class="text-muted">{{ help_text }}</small>

Fails with

'Format' object has no attribute 'split_contents'
djjudas21 commented 3 years ago

Basically I've got a large table with many rows, and each row is one field. So I don't want each cell title to have manual <small class="text-muted"> etc

djjudas21 commented 3 years ago

I also tried

@register.tag
def mytag4(parser, token):
    verbose_name = _get_model_field_attr('model_field_verbose_name', 'verbose_name', token)
    help_text = _get_model_field_attr('model_field_help_text', 'help_text', token)
    return "{}<br><small class=\"text-muted\">{}</small>".format(verbose_name, help_text)

but it fails with

'str' object has no attribute 'must_be_first'
idlesign commented 3 years ago

But I can't get it to work when called from an inclusion template, or a sub template.

  1. Use as clause to put tag result into a variable.
  2. Use with of include tag to pass this variable to a but subtemplate explicitly.

'str' object has no attribute 'must_be_first

Have to idea where and why you address %%must_be_first%% attribute.