Passed
Push — 2.x ( ab37e3...da17b1 )
by Jordi
05:29
created

senaite.core.browser.viewlets.sampleheader   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 182
dl 0
loc 302
rs 7.92
c 0
b 0
f 0

22 Methods

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