Nebulosar
Nebulosar

Reputation: 1855

Hide `***Block` in Wagtail

In Wagtail, I have made a Block with an ImageChooserBlock in it like this:

class MyBlock(blocks.StructBlock):
    background = ImageChooserBlock()

Now i want to add some extra fields to the ImageChooserBlock so I moved it to its own Block so now it looks like this:

class FancyImageChooserBlock(ImageChooserBlock):
    extra = blocks.Charfield()

class MyBlock(blocks.StructBlock):
    background = FancyImageChooserBlock()

My first issue is that the extra field doesn't get included. (Maybe because the block inherits from ImageChooserBlock?

My second and most important issue is that I want to able to have the extra field to be hidden in the form, but included in the template rendering. Does someone know how and if this is possible? I don't want to do any hacky stuff injecting js or css for this. There must be a way to do this using Blocks, Widgets and forms.HiddenInput or something like that.

I know i can do some calculations in the clean method of my FancyImageChooserBlock to manually set the value of extra. This is exactly what I want to do.

Any help is appreciated, I'm really stuck here.

Upvotes: 0

Views: 2067

Answers (3)

user1936977
user1936977

Reputation: 151

ImageBlockChooser isn't like 'StructBlock' or 'ListBlock' or 'StreamBlock' which are all structural block types - that is which are designed to 'look for' any child fields you might define. Only the structural block types are prepared for that 'out of the box'. For a block to use fields it needs to be configured to use/generate a template with those fields.

Personally, I think there are better ways of achieving what you want than to subclass ImageChooser as it will generally be more robust to try and find ways of using the features that Wagtail provide, even if you have to be a bit creative with them.

However, in case you still want to know how it might be done (hacked) by subclassing ImageChooser:

SOLUTION 1 - subclassing ImageChooser (this wouldn't be my preferred option for this):

#the chooser block class:
class MyImageChooserBlock(ImageChooserBlock):

  @cached_property
  def widget(self):
    from .mywidgetsfolder import MyAdminImageChooser
    return MyAdminImageChooser

from wagtail.admin.widgets import AdminChooser
from wagtail.images import get_image_model

#the chooser admin class...
class MyAdminImageChooser(AdminChooser):

  """the only difference between this class and AdminImageChooser
  is that this one provides a different template value to 
  render in the render_to_string method and the addition of certain 
  variables to the attrs dictionary. You could probably get away 
  with subclassing AdminImageChooser instead of AdminChooser and just 
  overriding the render_html method, but for some reason that seemed 
  to give me a duplicate 'choose image' button and it didn't seem 
  essential to fix it to demonstrate this principle"""

  choose_one_text = _('Choose an image')
  choose_another_text = _('Change image')
  link_to_chosen_text = _('Edit this image')

  def __init__(self, **kwargs):
    super().__init__(**kwargs)
    self.image_model = get_image_model()

  def render_html(self, name, value, attrs):
    instance, value = self.get_instance_and_id(self.image_model, 
    value)
    attrs['extra_hidden_fields'] = ('extra_1', 'extra_2')
    original_field_html = super().render_html(name, value, attrs)
    return render_to_string("my-widgets-folder/my_image_chooser.html", {
        'widget': self,
        'original_field_html': original_field_html,
        'attrs': attrs,
        'value': value,
        'image': instance,
    })

  def render_js_init(self, id_, name, value):
    return "createImageChooser({0});".format(json.dumps(id_))

#my-widgets-folder/my_image_chooser.html template:
{% extends "wagtailadmin/widgets/chooser.html" %}
{% load wagtailimages_tags %}
{% block chooser_class %}image-chooser{% endblock %}
{% block chosen_state_view %}
  {% for a in attrs.extra_hidden_fields %}
    <input type="hidden", name={{a}}, value="">
  {% endfor %}
  <div class="preview-image">
    {% if image %}
        {% image image max-300x300 class="show-transparency" %}
    {% else %}
        <img>
    {% endif %}
 </div>
{% endblock %}
{% block edit_chosen_item_url %}{% if image %}{% url 'wagtailimages:edit' image.id %}{% endif %}{% endblock %}

SOLUTION 2 - using a custom struct block and the group meta value:

  • This solution uses a custom struct block to hide the fields and labels. I'd recommend this approach for now as it uses customisation features provided by wagtail for your use. Although the documentation does not mention using the group meta in this way the group meta is documented and should be ok to rely on (and it could probably be quite easily replaced with another detail if necessary).
  • For anyone happening on this answer I'd suggest checking the wagtail docs before using this as it seems development on the project is fast and I wouldn't be at all surprised if they offered a 'built in' way of generating hidden fields soon.
  • In my own tests I didn't get the indent mentioned by the OP in his comment. As long as the struct is itself a top level element all the children were left aligned by default - so they were aligned with any fields outside of the struct block.
  • Of course you can create custom blocks of the basic types (say a custom CharBlock) and specify the widget kwarg as forms.HiddenInput but you still have the label to deal with - and passing a classname kwarg only applies it to the input not the label etc. Also using custom basic blocks means keeping them forever or providing deconstruct methods to avoid migration trouble. This circumvents all these sorts of issues.
  • Of course any of this could be achieved easily with some JS/css but this is offered assuming we want an html only solution.

    class StructWithHiddenFields(StructBlock):
      classMeta:
        form_template = "blocks/admin/struct_with_hidden_fields.html"
    
    """Obviously you'd want to copy the template from the wagtail one 
    for StructBlocks (wagtailadmin/block_forms/struct.html) to ensure 
    similar behaviour and then add a bit of logic for the hiding.  
    This might look like this:"""  
    
    #blocks/admin/struct_with_hidden_fields.html template:
    
    <div class="{{ classname }}">
      {% if help_text %}
       <div class="sequence-member__help help"><span class="icon- 
         help-inverse" aria-hidden="true"></span>{{ help_text }} 
       </div>
      {% endif %}
    
      <ul class="fields">
        {% for child in children.values %}
          {% if child.block.meta.group != "hidden-input" %}
            <li{% if child.block.required %} class="required"{% endif %}>
            {% if child.block.label %}
                <label{% if child.id_for_label %} for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}:</label>
            {% endif %}
            {{ child.render_form }}
            </li>
         {% endif %}
        {% endfor %}
      </ul>
      {% for child in children.values %}
        {% if child.block.meta.group == "hidden-input" %}
           <input type="hidden" id="{{ prefix }}-{{child.block.label}}" name="{{ prefix }}-{{child.block.label}}" value="{{child.block.value}}">
        {% endif %}
      {% endfor %}
    </div>
    
    #Usage:
    
    class MySpecificBlockWithHiddenFields(StructWithHiddenFields):
      normal_field = CharBlock(required=False)
      hidden_field = IntegerBlock(required=False, group="hidden-input")
    

Upvotes: 3

Nebulosar
Nebulosar

Reputation: 1855

Sweet solution in the model

This answer isn't really the answer on the question but a better option to go with when trying to add some fields to the ImageChooser on the background. As learned from the Wagtail docs there is this thing called the Custom Image Model

So instead of trying to add the fields on the Block "layer", I added them on the Model. For me the code looks something like this:

class ImageModel(AbstractImage):
    extra = models.CharField(max_length=255, blank=True, null=True)

    admin_form_fields = Image.admin_form_fields # So that in the image edit page, the fields are shown

    def save(self, **kwargs):
        self.clean_extra()
        return super(ImageModel, self).save(**kwargs)

    def clean_extra(self):
        if something():
            extra = 'This gets added as an attribute of image'

# Needed for the rendition relation
class ImageRenditionModel(AbstractRendition):
    image = models.ForeignKey(ImageModel, related_name='renditions')

    class Meta:
        unique_together = (
            ('image', 'filter_spec', 'focal_point_key'),
        )

Also, the WAGTAILIMAGES_IMAGE_MODEL must point to your own model, more about this you can find in the docs.

A very hacky solution in the blocks

There is another way to do this, without using extra html or extra models, but it is very hacky and disapproved.

class FancyImageChooserBlock(ImageChooserBlock):

    def clean_extra(self, value):
        if something():
            value['extra'] = 'This will get rendered in template'
        return value

    # for rendering in preview
    def clean(self, value):
        value = super(FancyImageChooserBlock, self).clean(value)
        value = self.clean_extra(value)
        return value

    # for rendering the live view
    def to_python(self, value):
        value = super(FancyImageChooserBlock, self).to_python(value)
        value = self.clean_extra(value)
        return value

This way you don't need extra html, css or js when adding an extra value. The point why this is disencouraged is because it takes up a lot of UX performance and also overriding the to_python function and clean function to inject an extra variable is very, very hacky and dirty as can be. But it works, so if you don't mind design standards or performance, work alone and nobody else will ever, ever see your code, you could use this.

So don't..

Upvotes: 0

Alexey
Alexey

Reputation: 1438

Regarding your first question, why not just:

class MyBlock(blocks.StructBlock):
    background = ImageChooserBlock()
    extra = blocks.Charfield()

?

Upvotes: 0

Related Questions