Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -287,4 +287,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")
66 changes: 66 additions & 0 deletions src/osha/oira/ploneintranet/quaive_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from plone import api
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.namedfile.scaling import ImageScaling
from plone.supermodel.model import SchemaClass
from Products.CMFCore.permissions import AddPortalContent
from Products.Five import BrowserView
Expand Down Expand Up @@ -107,6 +108,11 @@ def __call__(self) -> str:

class QuaiveRiskView(RiskView):

image_scale_options = {
"scale": "training",
"pre": True,
}

@property
def tool_type(self):
return get_tool_type(self.my_context)
Expand Down Expand Up @@ -141,3 +147,63 @@ def solutions(self):
for solution in self.my_context.values()
if ISolution.providedBy(solution)
]

def get_scale_url(self, context, image_field: str) -> str:
"""Return the scale to use for the given image field."""
images_view: ImageScaling = api.content.get_view(
name="images", context=context, request=self.request
) # type: ignore
return images_view.scale(image_field, **self.image_scale_options).url

@property
def legacy_information_scales_and_captions(self) -> list[dict]:
"""Return the images that should go in the information content well
coming from the legacy fields:

1. image and image_caption
2. image2 and caption2
3. image3 and caption3
4. image4 and caption4
"""
legacy_fields = [
("image", "image_caption"),
("image2", "caption2"),
("image3", "caption3"),
("image4", "caption4"),
]
scales_and_captions = []
for image_field, caption_field in legacy_fields:
image = getattr(self.context, image_field, None)
caption = getattr(self.context, caption_field, None)
if image:
scale = {
"url": self.get_scale_url(self.context, image_field),
"caption": caption or None,
}
scales_and_captions.append(scale)
return scales_and_captions

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

Returns a list of dicts with a scale URL and a title.
"""
scales_and_captions = []

# Add images stored in legacy fields that will be removed one day
scales_and_captions.extend(self.legacy_information_scales_and_captions)

# 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:
scale = {
"url": self.get_scale_url(image, "image"),
"caption": relation.caption or None,
}
scales_and_captions.append(scale)

return scales_and_captions
Loading