Passed
Push — 2.x ( 33260b...59bb1a )
by Ramon
06:08
created

SampleHeaderViewlet.get_field_value()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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