Passed
Push — 2.x ( 45ecec...8ca1e6 )
by Jordi
05:58
created

senaite.core.browser.viewlets.sampleheader   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 186
dl 0
loc 314
rs 6.96
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A SampleHeaderViewlet.grouper() 0 5 3
A SampleHeaderViewlet.render() 0 15 4
A SampleHeaderViewlet.get_configuration() 0 25 1
A SampleHeaderViewlet.get_field_info() 0 17 1
A SampleHeaderViewlet.is_edit_allowed() 0 4 1
A SampleHeaderViewlet.is_at_field() 0 4 1
B SampleHeaderViewlet.handle_form_submit() 0 39 6
A SampleHeaderViewlet.add_status_message() 0 4 1
A SampleHeaderViewlet.get_field_html() 0 24 5
A SampleHeaderViewlet.get_field_label() 0 5 1
A SampleHeaderViewlet.get_widget() 0 8 3
A SampleHeaderViewlet.is_field_required() 0 6 2
A SampleHeaderViewlet.fields() 0 5 1
A SampleHeaderViewlet.is_datetime_field() 0 7 2
A SampleHeaderViewlet.is_dx_field() 0 4 1
A SampleHeaderViewlet.render_widget() 0 4 1
B SampleHeaderViewlet.get_field_mode() 0 31 7
A SampleHeaderViewlet.is_primary_with_partitions() 0 5 1
A SampleHeaderViewlet.get_field_value() 0 24 5
A SampleHeaderViewlet.render_field_description() 0 5 1
A SampleHeaderViewlet.can_manage_sample_fields() 0 11 3
A SampleHeaderViewlet.is_primary_bound() 0 6 2

How to fix   Complexity   

Complexity

Complex classes like senaite.core.browser.viewlets.sampleheader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
            # allow to reset the reference
136
            if not fieldvalue:
137
                value = ""
138
            else:
139
                value = form[uid_fieldname]
140
            if field.multiValued:
141
                value = filter(None, value.split(","))
142
            return value
143
144
        # other fields
145
        return fieldvalue
146
147
    def grouper(self, iterable, n=3):
148
        """Splits an iterable into chunks of `n` items
149
        """
150
        for chunk in iter(lambda it=iter(iterable): list(islice(it, n)), []):
151
            yield chunk
152
153
    def get_field_info(self, name):
154
        """Return field information required for the template
155
        """
156
        field = self.fields.get(name)
157
        mode = self.get_field_mode(field)
158
        html = self.get_field_html(field, mode=mode)
159
        label = self.get_field_label(field, mode=mode)
160
        description = self.render_field_description(field, mode=mode)
161
        required = self.is_field_required(field, mode=mode)
162
        return {
163
            "name": name,
164
            "mode": mode,
165
            "html": html,
166
            "field": field,
167
            "label": label,
168
            "description": description,
169
            "required": required,
170
        }
171
172
    def get_field_html(self, field, mode="view"):
173
        """Render field HTML
174
        """
175
        if mode == "view":
176
            # Lookup custom view adapter
177
            adapter = queryAdapter(self.context,
178
                                   interface=IHeaderTableFieldRenderer,
179
                                   name=field.getName())
180
            # return immediately if we have an adapter
181
            if adapter is not None:
182
                return adapter(field)
183
184
            # TODO: Refactor to adapter
185
            # Returns the localized date
186
            if self.is_datetime_field(field):
187
                value = field.get(self.context)
188
                if not value:
189
                    return None
190
                return dtime.ulocalized_time(value,
191
                                             long_format=True,
192
                                             context=self.context,
193
                                             request=self.request)
194
195
        return None
196
197
    def get_field_label(self, field, mode="view"):
198
        """Renders the field label
199
        """
200
        widget = self.get_widget(field)
201
        return getattr(widget, "label", "")
202
203
    def render_field_description(self, field, mode="view"):
204
        """Renders the field description
205
        """
206
        widget = self.get_widget(field)
207
        return getattr(widget, "description", "")
208
209
    def render_widget(self, field, mode="view"):
210
        """Render the field widget
211
        """
212
        return self.context.widget(field.getName(), mode=mode)
213
214
    def get_field_mode(self, field, default="hidden"):
215
        """Returns the field mode in the header
216
217
        Possible values are:
218
219
          - edit: field is rendered in edit mode
220
          - view: field is rendered in view mode
221
        """
222
        mode = "view"
223
        if field.checkPermission("edit", self.context):
224
            mode = "edit"
225
            if not self.is_edit_allowed():
226
                logger.warn("Permission '{}' granted for the edition of '{}', "
227
                            "but 'Modify portal content' not granted"
228
                            .format(field.write_permission, field.getName()))
229
        elif field.checkPermission("view", self.context):
230
            mode = "view"
231
232
        widget = self.get_widget(field)
233
        mode_vis = widget.isVisible(self.context, mode=mode, field=field)
234
        if mode_vis != "visible":
235
            if mode == "view":
236
                return default
237
            # The field cannot be rendered in edit mode, but maybe can be
238
            # rendered in view mode.
239
            mode = "view"
240
            view_vis = widget.isVisible(self.context, mode=mode, field=field)
241
            if view_vis != "visible":
242
                return default
243
244
        return mode
245
246
    def get_widget(self, field):
247
        """Returns the widget of the field
248
        """
249
        if self.is_at_field(field):
250
            return field.widget
251
        elif self.is_dx_field(field):
252
            raise NotImplementedError("DX widgets not yet needed")
253
        raise TypeError("Field %r is neither a DX nor an AT field")
254
255
    def add_status_message(self, message, level="info"):
256
        """Set a portal status message
257
        """
258
        return self.context.plone_utils.addPortalMessage(message, level)
259
260
    @viewcache.memoize
261
    def is_primary_with_partitions(self):
262
        """Check if the Sample is a primary with partitions
263
        """
264
        return IAnalysisRequestWithPartitions.providedBy(self.context)
265
266
    def is_primary_bound(self, field):
267
        """Checks if the field is primary bound
268
        """
269
        if not self.is_primary_with_partitions():
270
            return False
271
        return getattr(field, "primary_bound", False)
272
273
    def is_edit_allowed(self):
274
        """Check permission 'ModifyPortalContent' on the context
275
        """
276
        return check_permission(ModifyPortalContent, self.context)
277
278
    def is_field_required(self, field, mode="edit"):
279
        """Check if the field is required
280
        """
281
        if mode == "view":
282
            return False
283
        return field.required
284
285
    def is_datetime_field(self, field):
286
        """Check if the field is a date field
287
        """
288
        if self.is_at_field(field):
289
            widget = self.get_widget(field)
290
            return isinstance(widget, DateTimeWidget)
291
        return False
292
293
    def is_at_field(self, field):
294
        """Check if the field is an AT field
295
        """
296
        return IATField.providedBy(field)
297
298
    def is_dx_field(self, field):
299
        """Check if the field is an DX field
300
        """
301
        return IDXField.providedBy(field)
302
303
    def can_manage_sample_fields(self):
304
        """Checks if the user is allowed to manage the sample fields
305
306
        TODO: Better use custom permission (same as used for view)
307
        """
308
        roles = get_roles()
309
        if "Manager" in roles:
310
            return True
311
        elif "LabManager" in roles:
312
            return True
313
        return False
314