Reputation: 1855
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
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:
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
Reputation: 1855
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.
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
Reputation: 1438
Regarding your first question, why not just:
class MyBlock(blocks.StructBlock):
background = ImageChooserBlock()
extra = blocks.Charfield()
?
Upvotes: 0