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

senaite.core.browser.samples.manage_sample_fields   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 48
eloc 182
dl 0
loc 327
rs 8.5599
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A ManageSampleFieldsView.is_field_required() 0 7 1
A ManageSampleFieldsView.get_field_info() 0 16 1
A ManageSampleFieldsView.__call__() 0 13 5
A ManageSampleFieldsView.handle_form_save() 0 22 5
A ManageSampleFieldsView.get_header_fields() 0 18 3
A ManageSampleFieldsView.get_field_visibility() 0 18 2
A ManageSampleFieldsView.set_config() 0 20 3
C ManageSampleFieldsView.get_configuration() 0 60 9
A ManageSampleFieldsView.get_widget() 0 11 3
A ManageSampleFieldsView.is_field_visible() 0 9 1
A ManageSampleFieldsView.fields() 0 5 1
A ManageSampleFieldsView.get_field_description() 0 8 1
A ManageSampleFieldsView.get_field_label() 0 8 1
A ManageSampleFieldsView.get_config() 0 13 3
A ManageSampleFieldsView.page_url() 0 3 1
A ManageSampleFieldsView.__init__() 0 3 1
A ManageSampleFieldsView.add_status_message() 0 7 1
A ManageSampleFieldsView.is_dx_field() 0 7 1
A ManageSampleFieldsView.handle_form_reset() 0 14 3
A ManageSampleFieldsView.is_at_field() 0 7 1
A ManageSampleFieldsView.context_state() 0 6 1

How to fix   Complexity   

Complexity

Complex classes like senaite.core.browser.samples.manage_sample_fields 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
import traceback
4
5
from bika.lims import api
6
from bika.lims import senaiteMessageFactory as _
7
from plone.memoize import view as viewcache
8
from plone.protect import PostOnly
9
from Products.Archetypes.interfaces import IField as IATField
10
from Products.Five.browser import BrowserView
11
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
12
from senaite.core import logger
13
from senaite.core.registry import get_registry
14
from senaite.core.registry import get_registry_record
15
from senaite.core.registry import set_registry_record
16
from senaite.core.registry.schema import ISampleHeaderRegistry
17
from zope.component import getMultiAdapter
18
from zope.schema.interfaces import IField as IDXField
19
from ZPublisher.HTTPRequest import record as RequestRecord
20
21
_marker = object
22
23
REGISTRY_KEY_PREFIX = "sampleheader"
24
25
26
class ManageSampleFieldsView(BrowserView):
27
    """Manage Sample Fields
28
    """
29
    template = ViewPageTemplateFile("templates/manage_sample_fields.pt")
30
31
    def __init__(self, context, request):
32
        self.context = context
33
        self.request = request
34
35
    def __call__(self):
36
        submitted = self.request.form.get("submitted", False)
37
        save = self.request.form.get("save", False)
38
        reset = self.request.form.get("reset", False)
39
        # Handle form save action
40
        if submitted and save:
41
            self.handle_form_save(request=self.request)
42
            self.request.response.redirect(self.page_url)
43
        # Handle form reset action
44
        elif submitted and reset:
45
            self.handle_form_reset(request=self.request)
46
            self.request.response.redirect(self.page_url)
47
        return self.template()
48
49
    @property
50
    @viewcache.memoize
51
    def context_state(self):
52
        return getMultiAdapter(
53
            (self.context, self.request),
54
            name="plone_context_state")
55
56
    @property
57
    def page_url(self):
58
        return self.context_state.current_page_url()
59
60
    @property
61
    def fields(self):
62
        """Returns an ordered dict of all schema fields
63
        """
64
        return api.get_fields(self.context)
65
66
    def handle_form_save(self, request=None):
67
        """Handle form submission -> save
68
69
        Update all known registry records with the values from the request
70
        """
71
        PostOnly(request)
72
        config = self.get_configuration()
73
        for key, old_value in config.items():
74
            new_value = request.form.get(key, _marker)
75
            if new_value is _marker:
76
                continue
77
            # convert request records to plain dictionaries
78
            if isinstance(new_value, RequestRecord):
79
                new_value = dict(new_value)
80
            success = self.set_config(key, new_value)
81
            # return immediately if a record could not be set
82
            if not success:
83
                message = _("Failed to update registry records. "
84
                            "Please check the server log for details.")
85
                return self.add_status_message(message, level="warning")
86
        message = _("Changes saved.")
87
        return self.add_status_message(message)
88
89
    def handle_form_reset(self, request=None):
90
        """Handle form submission -> reset
91
92
        Set all known registry record values to `None`
93
        """
94
        PostOnly(request)
95
        identifier = ISampleHeaderRegistry.__identifier__
96
        registry = get_registry()
97
        for key in registry.records:
98
            if key.startswith(identifier):
99
                logger.info("Flushing registry key %s" % key)
100
                registry.records[key].value = None
101
        message = _("Configuration restored to default values.")
102
        self.add_status_message(message)
103
104
    def set_config(self, name, value, prefix=REGISTRY_KEY_PREFIX):
105
        """Set registry record by name
106
107
        :param name: Name of the registry record to update
108
        :param value: Value to set
109
        :returns: True if the record was updated successfully, otherwise False
110
        """
111
        if prefix:
112
            name = "{}_{}".format(REGISTRY_KEY_PREFIX, name)
113
        logger.info("Set registry record '{}' to value '{}'"
114
                    .format(name, value))
115
        try:
116
            set_registry_record(name, value)
117
        except (NameError, TypeError):
118
            exc = traceback.format_exc()
119
            logger.error("Failed to set registry record '{}' -> '{}'"
120
                         "\nTraceback: {}\nForgot to run the migration steps?"
121
                         .format(name, value, exc))
122
            return False
123
        return True
124
125
    def get_config(self, name, prefix=REGISTRY_KEY_PREFIX, default=None):
126
        """Get registry record value by name
127
128
        :param name: Name of the registry record to fetch
129
        :returns: Value of the registry record, otherwise the default
130
        """
131
        if prefix:
132
            name = "{}_{}".format(REGISTRY_KEY_PREFIX, name)
133
        # only return the default when the registry record is not set
134
        record = get_registry_record(name, default=_marker)
135
        if record is _marker:
136
            return default
137
        return record
138
139
    def get_header_fields(self):
140
        """Return the (re-arranged) fields
141
142
        :returns: Dictionary with ordered lists of field names
143
        """
144
        header_fields = {
145
            "prominent": [],
146
            "visible": [],
147
            "hidden": [],
148
        }
149
150
        for name, field in self.fields.items():
151
            if not self.is_field_visible(field):
152
                continue
153
            vis = self.get_field_visibility(field)
154
            header_fields[vis].append(name)
155
156
        return header_fields
157
158
    def get_field_info(self, name):
159
        """Return field information required for the template
160
161
        :param name: Name of the field
162
        :returns: Dictionary with template specific data
163
        """
164
        field = self.fields.get(name)
165
        label = self.get_field_label(field)
166
        description = self.get_field_description(field)
167
        required = self.is_field_required(field)
168
        return {
169
            "name": name,
170
            "field": field,
171
            "label": label,
172
            "description": description,
173
            "required": required,
174
        }
175
176
    def get_configuration(self):
177
        """Return the header fields configuration
178
179
        NOTE: This method is used in the sample header viewlet to render the
180
              fields according to their order and visibility.
181
182
        :returns: Field configuration dictionary for the sample header viewlet
183
        """
184
        show_standard_fields = self.get_config("show_standard_fields")
185
        prominent_columns = self.get_config("prominent_columns")
186
        standard_columns = self.get_config("standard_columns")
187
        prominent_fields = self.get_config("prominent_fields")
188
        standard_fields = self.get_config("standard_fields")
189
        field_visibility = self.get_config("field_visibility")
190
191
        # all available fields looked up from schema
192
        fields = self.get_header_fields()
193
194
        # Handle flushed or not set registry keys
195
        if show_standard_fields is None:
196
            show_standard_fields = True
197
        if prominent_columns is None:
198
            prominent_columns = 1
199
        if standard_columns is None:
200
            standard_columns = 3
201
        if prominent_fields is None:
202
            prominent_fields = fields["prominent"]
203
        if standard_fields is None:
204
            standard_fields = fields["visible"]
205
        if field_visibility is None:
206
            field_visibility = dict.fromkeys(self.fields.keys(), True)
207
208
        # Always update added or removed fields, e.g. when the sampling
209
        # workflow was activated or deactivated in the setup
210
        default_fields = fields["prominent"] + fields["visible"]
211
        configured_fields = prominent_fields + standard_fields
212
213
        added = set(default_fields).difference(configured_fields)
214
        removed = set(configured_fields).difference(default_fields)
215
216
        # add new appeard fields always to the standard fields
217
        standard_fields += added
218
219
        # remove fields from prominent and standard fields
220
        prominent_fields = filter(lambda f: f not in removed, prominent_fields)
221
        standard_fields = filter(lambda f: f not in removed, standard_fields)
222
223
        # make new fields visible
224
        field_visibility.update(dict.fromkeys(added, True))
225
226
        config = {
227
            "show_standard_fields": show_standard_fields,
228
            "prominent_columns": prominent_columns,
229
            "standard_columns": standard_columns,
230
            "prominent_fields": prominent_fields,
231
            "standard_fields": standard_fields,
232
            "field_visibility": field_visibility,
233
        }
234
235
        return config
236
237
    def get_field_visibility(self, field, default="hidden"):
238
        """Returns the field visibility in the header
239
240
        Possible return values are:
241
242
          - prominent: Rendered as a promient field
243
          - visible: Rendered as a odestandard field
244
          - hidden: Not displayed
245
246
        :param field: Field object
247
        :returns: Visibility string
248
        """
249
        widget = self.get_widget(field)
250
        visibility = widget.isVisible(
251
            self.context, mode="header_table", field=field)
252
        if visibility not in ["prominent", "visible"]:
253
            return default
254
        return visibility
255
256
    def get_field_label(self, field):
257
        """Get the field label
258
259
        :param field: Field object
260
        :returns: Label of the fields' widget
261
        """
262
        widget = self.get_widget(field)
263
        return getattr(widget, "label", "")
264
265
    def get_field_description(self, field):
266
        """Get the field description
267
268
        :param field: Field object
269
        :returns: Description of the fields' widget
270
        """
271
        widget = self.get_widget(field)
272
        return getattr(widget, "description", "")
273
274
    def get_widget(self, field):
275
        """Returns the widget of the field
276
277
        :param field: Field object
278
        :returns: Widget object
279
        """
280
        if self.is_at_field(field):
281
            return field.widget
282
        elif self.is_dx_field(field):
283
            raise NotImplementedError("DX widgets not yet needed")
284
        raise TypeError("Field %r is neither a DX nor an AT field")
285
286
    def is_field_visible(self, field):
287
        """Checks if the field is visible in view mode
288
289
        :param field: Field object
290
        :returns: True if field is visible, otherwise False
291
        """
292
        widget = self.get_widget(field)
293
        visibility = widget.isVisible(self.context, mode="view", field=field)
294
        return visibility == "visible"
295
296
    def is_field_required(self, field):
297
        """Check if the field is required
298
299
        :param field: Field object
300
        :returns: True if field is set to required, otherwise False
301
        """
302
        return getattr(field, "required", False)
303
304
    def is_at_field(self, field):
305
        """Check if the field is an AT field
306
307
        :param field: Field object
308
        :returns: True if field is an AT based field
309
        """
310
        return IATField.providedBy(field)
311
312
    def is_dx_field(self, field):
313
        """Check if the field is an DX field
314
315
        :param field: Field object
316
        :returns: True if field is an DX based field
317
        """
318
        return IDXField.providedBy(field)
319
320
    def add_status_message(self, message, level="info"):
321
        """Set a portal status message
322
323
        :param message: The status message to render
324
        :returns: None
325
        """
326
        self.context.plone_utils.addPortalMessage(message, level)
327