Passed
Push — 2.x ( 8f67cf...3a5aed )
by Jordi
08:57
created

SampleHeaderViewlet.__init__()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nop 5
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from itertools import islice
22
23
from bika.lims import api
24
from bika.lims import senaiteMessageFactory as _
25
from bika.lims.api.security import check_permission
26
from bika.lims.api.security import get_roles
27
from bika.lims.interfaces import IAnalysisRequestWithPartitions
28
from bika.lims.interfaces import IHeaderTableFieldRenderer
29
from bika.lims.interfaces.field import IUIDReferenceField
30
from plone.app.layout.viewlets import ViewletBase
31
from plone.memoize import view as viewcache
32
from plone.protect import PostOnly
33
from Products.Archetypes.event import ObjectEditedEvent
34
from Products.Archetypes.interfaces import IField as IATField
35
from Products.CMFCore.permissions import ModifyPortalContent
36
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
37
from senaite.core.interfaces import IDataManager
38
from zope import event
39
from zope.component import queryAdapter
40
from zope.schema.interfaces import IField as IDXField
41
42
_fieldname_not_in_form = object()
43
44
45
class SampleHeaderViewlet(ViewletBase):
46
    """Header table with editable sample fields
47
    """
48
    template = ViewPageTemplateFile("templates/sampleheader.pt")
49
50
    def __init__(self, context, request, view, manager=None):
51
        super(SampleHeaderViewlet, self).__init__(
52
            context, request, view, manager=manager)
53
        self.show_save = False
54
55
    def render(self):
56
        """Renders the viewlet and handles form submission
57
        """
58
        request = self.request
59
        submitted = request.form.get("sampleheader_form_submitted", False)
60
        save = request.form.get("sampleheader_form_save", False)
61
        errors = {}
62
        if submitted and save:
63
            errors = self.handle_form_submit(request=self.request)
64
            # NOTE: we only redirect if no validation errors occured,
65
            #       because otherwise the fields are not error-indicated!
66
            if not errors:
67
                # redirect needed to show status message
68
                self.request.response.redirect(self.context.absolute_url())
69
        return self.template(errors=errors)
70
71
    def handle_form_submit(self, request=None):
72
        """Handle form submission
73
        """
74
        PostOnly(request)
75
76
        errors = {}
77
        field_values = {}
78
        form = request.form
79
80
        for name, field in self.fields.items():
81
            # get the raw value from the form
82
            value = self.get_field_value(field, form)
83
            if value is _fieldname_not_in_form:
84
                continue
85
86
            # some legacy widgets need values with <fieldname>_ as prefix
87
            prefix = "{}_".format(name)
88
            extra = filter(lambda key: key.startswith(prefix), form.keys())
0 ignored issues
show
introduced by
The variable prefix does not seem to be defined in case the for loop on line 80 is not entered. Are you sure this can never be the case?
Loading history...
89
            form_values = dict((key, form[key]) for key in extra)
90
            form_values[name] = value
91
92
            # process the value as the widget would usually do
93
            process_value = field.widget.process_form
94
            value, msgs = process_value(self.context, field, form_values)
95
96
            # Keep track of field-values
97
            field_values.update({name: value})
98
99
            # Validate the field values
100
            error = field.validate(value, self.context)
101
            if error:
102
                errors.update({name: error})
103
104
        if errors:
105
            return errors
106
        else:
107
            # we want to set the fields with the data manager
108
            dm = IDataManager(self.context)
109
110
            # Store the field values
111
            for name, value in field_values.items():
112
                dm.set(name, value)
113
114
            message = _("Changes saved.")
115
            # reindex the object after save to update all catalog metadata
116
            self.context.reindexObject()
117
            # notify object edited event
118
            event.notify(ObjectEditedEvent(self.context))
119
            self.add_status_message(message, level="info")
120
121
    def get_configuration(self):
122
        """Return header configuration
123
124
        This method retrieves the customized field and column configuration
125
        from the management view directly.
126
127
        :returns: Field and columns configuration dictionary
128
        """
129
        mv = api.get_view(name="manage-sample-fields", context=self.context)
130
        settings = mv.get_configuration()
131
        visibility = settings.get("field_visibility")
132
133
        def is_visible(name):
134
            return visibility.get(name, True)
135
136
        # filter out fields that are configured as invisible
137
        prominent_fields = filter(is_visible, settings.get("prominent_fields"))
138
        standard_fields = filter(is_visible, settings.get("standard_fields"))
139
140
        config = {}
141
        config.update(settings)
142
        config["prominent_fields"] = prominent_fields
143
        config["standard_fields"] = standard_fields
144
145
        return config
146
147
    @property
148
    def fields(self):
149
        """Returns an ordered dict of all schema fields
150
        """
151
        return api.get_fields(self.context)
152
153
    def get_field_value(self, field, form):
154
        """Returns the submitted value for the given field
155
        """
156
        fieldname = field.getName()
157
        if fieldname not in form:
158
            return _fieldname_not_in_form
159
160
        fieldvalue = form[fieldname]
161
162
        # Handle  reference fields
163
        if IUIDReferenceField.providedBy(field):
164
            value = fieldvalue
165
166
            # extract the assigned UIDs for multi-reference fields
167
            if field.multiValued:
168
                value = filter(None, fieldvalue.split("\r\n"))
169
170
            # allow to flush single reference fields
171
            if not field.multiValued and not fieldvalue:
172
                value = ""
173
174
            return value
175
176
        # other fields
177
        return fieldvalue
178
179
    def grouper(self, iterable, n=3):
180
        """Splits an iterable into chunks of `n` items
181
        """
182
        for chunk in iter(lambda it=iter(iterable): list(islice(it, n)), []):
183
            yield chunk
184
185
    def get_field_info(self, name):
186
        """Return field information required for the template
187
        """
188
        field = self.fields.get(name)
189
        mode = self.get_field_mode(field)
190
        html = self.get_field_html(field, mode=mode)
191
        label = self.get_field_label(field, mode=mode)
192
        description = self.render_field_description(field, mode=mode)
193
        required = self.is_field_required(field, mode=mode)
194
        return {
195
            "name": name,
196
            "mode": mode,
197
            "html": html,
198
            "field": field,
199
            "label": label,
200
            "description": description,
201
            "required": required,
202
        }
203
204
    def get_field_html(self, field, mode="view"):
205
        """Render field HTML
206
        """
207
        if mode == "view":
208
            # Lookup custom view adapter
209
            adapter = queryAdapter(self.context,
210
                                   interface=IHeaderTableFieldRenderer,
211
                                   name=field.getName())
212
            # return immediately if we have an adapter
213
            if adapter is not None:
214
                return adapter(field)
215
216
        return None
217
218
    def get_field_label(self, field, mode="view"):
219
        """Renders the field label
220
        """
221
        widget = self.get_widget(field)
222
        return getattr(widget, "label", "")
223
224
    def render_field_description(self, field, mode="view"):
225
        """Renders the field description
226
        """
227
        widget = self.get_widget(field)
228
        return getattr(widget, "description", "")
229
230
    def render_widget(self, field, mode="view"):
231
        """Render the field widget
232
        """
233
        return self.context.widget(field.getName(), mode=mode)
234
235
    def get_field_mode(self, field, default="hidden"):
236
        """Returns the field mode in the header
237
238
        Possible values are:
239
240
          - edit: field is rendered in edit mode
241
          - view: field is rendered in view mode
242
        """
243
        mode = "view"
244
        if field.checkPermission("edit", self.context):
245
            mode = "edit"
246
            self.show_save = True
247
        elif field.checkPermission("view", self.context):
248
            mode = "view"
249
250
        widget = self.get_widget(field)
251
        mode_vis = widget.isVisible(self.context, mode=mode, field=field)
252
        if mode_vis != "visible":
253
            if mode == "view":
254
                return default
255
            # The field cannot be rendered in edit mode, but maybe can be
256
            # rendered in view mode.
257
            mode = "view"
258
            view_vis = widget.isVisible(self.context, mode=mode, field=field)
259
            if view_vis != "visible":
260
                return default
261
262
        return mode
263
264
    def get_widget(self, field):
265
        """Returns the widget of the field
266
        """
267
        if self.is_at_field(field):
268
            return field.widget
269
        elif self.is_dx_field(field):
270
            raise NotImplementedError("DX widgets not yet needed")
271
        raise TypeError("Field %r is neither a DX nor an AT field")
272
273
    def add_status_message(self, message, level="info"):
274
        """Set a portal status message
275
        """
276
        return self.context.plone_utils.addPortalMessage(message, level)
277
278
    @viewcache.memoize
279
    def is_primary_with_partitions(self):
280
        """Check if the Sample is a primary with partitions
281
        """
282
        return IAnalysisRequestWithPartitions.providedBy(self.context)
283
284
    def is_primary_bound(self, field):
285
        """Checks if the field is primary bound
286
        """
287
        if not self.is_primary_with_partitions():
288
            return False
289
        return getattr(field, "primary_bound", False)
290
291
    def is_edit_allowed(self):
292
        """Check permission 'ModifyPortalContent' on the context
293
        """
294
        return check_permission(ModifyPortalContent, self.context)
295
296
    def is_field_required(self, field, mode="edit"):
297
        """Check if the field is required
298
        """
299
        if mode == "view":
300
            return False
301
        return field.required
302
303
    def is_at_field(self, field):
304
        """Check if the field is an AT field
305
        """
306
        return IATField.providedBy(field)
307
308
    def is_dx_field(self, field):
309
        """Check if the field is an DX field
310
        """
311
        return IDXField.providedBy(field)
312
313
    def can_manage_sample_fields(self):
314
        """Checks if the user is allowed to manage the sample fields
315
316
        TODO: Better use custom permission (same as used for view)
317
        """
318
        roles = get_roles()
319
        if "Manager" in roles:
320
            return True
321
        elif "LabManager" in roles:
322
            return True
323
        return False
324