django-etc icon indicating copy to clipboard operation
django-etc copied to clipboard

Get help_text from model instance

Open djjudas21 opened this issue 4 years ago • 14 comments

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

djjudas21 avatar Apr 27 '21 21:04 djjudas21

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?

idlesign avatar Apr 28 '21 01:04 idlesign

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.

djjudas21 avatar Apr 28 '21 07:04 djjudas21

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.

idlesign avatar Apr 28 '21 08:04 idlesign

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.

djjudas21 avatar Apr 28 '21 17:04 djjudas21

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.

idlesign avatar Apr 30 '21 12:04 idlesign

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

djjudas21 avatar Apr 30 '21 12:04 djjudas21

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:

  • create a directory for your template tag,
  • make a module for your tag
  • define a tag function (see examples in django docs or use model_field.py from etc as such)
  • import FieldAttrNode from etc.templatetags.model_field
  • from your tag function call FieldAttrNode with proper args (similar to _get_model_field_attr)

idlesign avatar Apr 30 '21 13:04 idlesign

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

djjudas21 avatar Apr 30 '21 13:04 djjudas21

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.

djjudas21 avatar Apr 30 '21 21:04 djjudas21

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.

idlesign avatar May 10 '21 01:05 idlesign

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 avatar May 10 '21 14:05 djjudas21

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 avatar May 10 '21 14:05 djjudas21

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'

djjudas21 avatar May 10 '21 16:05 djjudas21

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.

idlesign avatar May 11 '21 02:05 idlesign