Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions news/3447.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Add a new behavior to link images on a risk

TODO: we still have lots of issues, please do not merge until these are fixed.


About image management

- [ ] I could not find in proto how to add a new image, I had to add the images manually
(calling ``/++add++Image``)
- [ ] There is no clear way to manage uploaded images (editing, deleting, ...)

About the relation management:

- [ ] Only one relation can be picked at a time
- [ ] Removing a relation is not yet implemented
- [ ] When sorting the relations some injection happens (it should not)
- [ ] When sorting the relations the images are (apparently) not sorted,
but reloading the page shows them in the proper order

Minor UI issues:

- [ ] The picker that opens in the panel has not the proper classes in the buttons
- [ ] We do not have a proper macro for the tabbed panels
- [ ] There is no page to display the image view
- [ ] In the navigation tree we do not have an icon for the image

Rest API issues:

- [ ] The ``related_images`` field is not serialized as ``null``
Empty file.
13 changes: 13 additions & 0 deletions src/osha/oira/behaviors/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
>

<plone:behavior
name="euphorie.related_images"
title="Related images behavior"
description="Adds fields to manage related images"
provides=".related_images.IRelatedImagesBehavior"
/>

</configure>
97 changes: 97 additions & 0 deletions src/osha/oira/behaviors/related_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from euphorie.content import MessageFactory as _
from plone import api
from plone.app.vocabularies.catalog import CatalogSource
from plone.autoform import directives
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from z3c.form.interfaces import IEditForm
from z3c.form.object import registerFactoryAdapter
from z3c.relationfield import RelationValue
from z3c.relationfield.schema import RelationChoice
from zope import schema
from zope.component import getUtility
from zope.interface import implementer
from zope.interface import Interface
from zope.interface import provider
from zope.intid.interfaces import IIntIds
from zope.schema.interfaces import IList


class IImageWithCaption(Interface):
"""Interface for the value that will store the relation to the image
and the caption.
"""

image = RelationChoice(
title=_("Related image"),
source=CatalogSource(portal_type="Image"),
required=False,
)

caption = schema.Text(
title=_("Image caption"),
description=_("Write a caption for this image. (Optional)"),
required=False,
)


@implementer(IImageWithCaption)
class ImageWithCaption:
"""A class that stores a relation to an image and an optional caption."""

def __init__(self, image=None, caption=None):
self.image = image
self.caption = caption

@classmethod
def from_uid(cls, uid):
"""Helper to initialize this class from an object uid"""
image = api.content.get(UID=uid)
intids = getUtility(IIntIds)
return cls(RelationValue(intids.getId(image)), "")


registerFactoryAdapter(IImageWithCaption, ImageWithCaption)


class IRelatedImagesField(IList):
"""A field that allows to edit a list of ImageWithCaption instances"""


@implementer(IRelatedImagesField)
class RelatedImagesField(schema.List):
"""A field that allows to edit a list of ImageWithCaption instances"""

def __init__(self, **kwargs):
if "value_type" in kwargs:
raise ValueError("value_type must not be set")

super().__init__(
value_type=schema.Object(
schema=IImageWithCaption, title=_("Related image with caption")
),
**kwargs
)


@provider(IFormFieldProvider)
class IRelatedImagesBehavior(model.Schema):
"""A behavior that adds to a dexterity object a related_images field
used to store a list of images with captions.
"""

related_images = RelatedImagesField(
title=_(
"Add an image gallery by uploading "
"a set of images or by selecting them from Image bank."
),
description=_("List of related images with captions."),
required=False,
)
model.fieldset(
"information",
label=_("Information"),
fields=["related_images"],
)
directives.omitted("related_images")
directives.no_omit(IEditForm, "related_images")
3 changes: 3 additions & 0 deletions src/osha/oira/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@
<include package="pas.plugins.ldap" />
<include package="euphorie.deployment" />
<include package="plone.restapi" />
<include package=".behaviors" />
<include package=".client" />
<include package=".content" />
<include package=".tiles" />
<include package=".nuplone" />
<include package=".ploneintranet" />
<include package=".upgrade" />
<include package=".statistics" />
<include package=".serializer" />
<include package=".services" />
<include package=".widgets" />

<!-- Vocabularies -->
<utility
Expand Down
9 changes: 9 additions & 0 deletions src/osha/oira/ploneintranet/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,13 @@
layer="osha.oira.interfaces.IOSHAContentSkinLayer"
/>

<browser:page
name="panel-select-image-image-bank"
for="*"
class=".image_bank.ImageBankPanel"
template="templates/panel-select-image-image-bank.pt"
permission="cmf.ModifyPortalContent"
layer="osha.oira.nuplone.interfaces.IOiRAFormLayer"
/>

</configure>
121 changes: 121 additions & 0 deletions src/osha/oira/ploneintranet/image_bank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from functools import cached_property
from osha.oira import _
from osha.oira.behaviors.related_images import ImageWithCaption
from osha.oira.ploneintranet.interfaces import IQuaiveForm
from plone import api
from plone.autoform.form import AutoExtensibleForm
from Products.ZCatalog.CatalogBrains import AbstractCatalogBrain
from z3c.form import form
from zope import schema
from zope.interface import implementer
from zope.interface import Interface


class IImageBank(Interface):
"""This schema is used to select the images to be used in the image bank by UID."""

uids = schema.List(
title=_("Image UIDs"),
description=_("List of image UIDs"),
value_type=schema.TextLine(
title=_("Image UID"),
description=_("UID of the image"),
),
required=False,
)


@implementer(IQuaiveForm)
class ImageBankPanel(AutoExtensibleForm, form.Form):
"""Update the company details.

View name: @@panel-select-image-image-bank
"""

schema = IImageBank
ignoreContext = True
oira_type = ""

@cached_property
def template(self):
return self.index

@property
def available_images(self):
"""Return the available images from the catalog"""
brains = api.content.find(
portal_type="Image",
sort_on="sortable_title",
)
return brains

def get_image_scale(self, image: AbstractCatalogBrain) -> str:
"""Return the image scale URL."""
try:
scale_path = image.image_scales["image"][0]["scales"]["mini"]["download"]
except (KeyError, IndexError):
# Handle the case where the scale is not available
return ""

return f"{image.getURL()}/{scale_path}"

def redirect(self):
"""Redirect to the edit form."""
self.request.response.redirect(f"{self.context.absolute_url()}/@@quaive-edit")

@property
def existing_relations(self):
"""Return a mapping with the existing valid relations grouped by UID."""
related_images = self.context.related_images or []
mapping = {}
for relation in related_images:
try:
image = relation.image.to_object
except AttributeError:
image = None
if image:
mapping[image.UID()] = relation
return mapping

def set_relations(self, data):
"""Set the relations based on the selected images."""
existing_relations = self.existing_relations
new_relations = []

# Transform the user input in to a list of relations
uids = data.get("uids", []) or []
for uid in uids:
if not uid or uid in existing_relations:
# This disallows duplicates
continue

image = api.content.get(UID=uid)
if image:
new_relations.append(ImageWithCaption.from_uid(uid))

if new_relations:
update_relations = self.context.related_images or []
update_relations.extend(new_relations)

# This ensures the object is marked as changed
self.context.related_images = update_relations

@form.button.buttonAndHandler(_("Insert"), name="insert")
def handle_insert(self, action):
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
else:
self.set_relations(data)
self.redirect()

@form.button.buttonAndHandler(_("Cancel"), name="cancel")
def handle_cancel(self, action):
"""Cancel button"""
self.redirect()

def updateActions(self):
super().updateActions()
for action in self.actions.values():
action.addClass("close-panel")
self.actions["insert"].addClass("btn-primary")
62 changes: 62 additions & 0 deletions src/osha/oira/ploneintranet/quaive_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,65 @@ def solutions(self):
for solution in self.my_context.values()
if ISolution.providedBy(solution)
]

@property
def information_images(self):
"""Return the images that should go in the information content well.

Returns a list of dicts with a URL and a title.
"""
risk_images_view = api.content.get_view(
name="images", context=self.context, request=self.request
)
image_scale = "training"
images = []
# For legacy reasons we can the images coming from four couple of fields:
# 1. image/caption
# 2. image2/caption2
# 3. image3/caption3
# 4. image4/caption4
if self.context.image:
images.append(
{
"url": risk_images_view.scale("image", scale=image_scale).url,
"title": self.context.image_caption or None,
}
)
if self.context.image2:
images.append(
{
"url": risk_images_view.scale("image2", scale=image_scale).url,
"title": self.context.caption2 or None,
}
)
if self.context.image3:
images.append(
{
"url": risk_images_view.scale("image3", scale=image_scale).url,
"title": self.context.caption3 or None,
}
)
if self.context.image4:
images.append(
{
"url": risk_images_view.scale("image4", scale=image_scale).url,
"title": self.context.caption4 or None,
}
)

# The modern and preferred approach is to use
# a list field with captioned images.
related_images = self.context.related_images or []
for relation in related_images:
image = relation.image.to_object
if image:
images_view = api.content.get_view(
name="images", context=image, request=self.request
)
images.append(
{
"url": images_view.scale("image", scale=image_scale).url,
"title": relation.caption or None,
}
)
return images
Loading