Passed
Push — master ( be702a...def89a )
by Jordi
04:03
created

HeaderTableView.is_date_field()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nop 2
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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl.Permissions import view
22
from bika.lims import api
23
from bika.lims import bikaMessageFactory as _
24
from bika.lims import logger
25
from bika.lims.api.security import check_permission
26
from bika.lims.browser import BrowserView
27
from bika.lims.interfaces import IHeaderTableFieldRenderer
28
from bika.lims.utils import t
29
from plone.memoize import view as viewcache
30
from plone.memoize.volatile import ATTR
31
from plone.memoize.volatile import CONTAINER_FACTORY
32
from plone.memoize.volatile import cache
33
from Products.Archetypes.event import ObjectEditedEvent
34
from Products.CMFCore.permissions import ModifyPortalContent
35
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
36
from zope import event
37
from zope.component import queryAdapter
38
39
40
def store_on_portal(method, obj, *args, **kwargs):
41
    """Volatile cache storage on the portal object
42
    """
43
    portal = api.get_portal()
44
    return portal.__dict__.setdefault(ATTR, CONTAINER_FACTORY())
45
46
47
def field_type_cache_key(method, self, field):
48
    """Cache key to distinguish the type evaluation of a field
49
    """
50
    return field.getName()
51
52
53
class HeaderTableView(BrowserView):
54
    """Table rendered at the top in AR View
55
    """
56
    template = ViewPageTemplateFile("templates/header_table.pt")
57
58
    def __call__(self):
59
        if "header_table_submitted" in self.request:
60
            schema = self.context.Schema()
61
            fields = schema.fields()
62
            form = self.request.form
63
            for field in fields:
64
                fieldname = field.getName()
65
                if fieldname in form:
66
                    # Handle (multiValued) reference fields
67
                    # https://github.com/bikalims/bika.lims/issues/2270
68
                    uid_fieldname = "{}_uid".format(fieldname)
69
                    if uid_fieldname in form:
70
                        value = form[uid_fieldname]
71
                        if field.multiValued:
72
                            value = value.split(",")
73
                        field.getMutator(self.context)(value)
74
                    else:
75
                        # other fields
76
                        field.getMutator(self.context)(form[fieldname])
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.context.plone_utils.addPortalMessage(message, "info")
83
        return self.template()
84
85
    @viewcache.memoize
86
    def is_edit_allowed(self):
87
        """Check permission 'ModifyPortalContent' on the context
88
        """
89
        return check_permission(ModifyPortalContent, self.context)
90
91
    @cache(field_type_cache_key, get_cache=store_on_portal)
92
    def is_reference_field(self, field):
93
        """Check if the field is a reference field
94
        """
95
        return field.getType().find("Reference") > -1
96
97
    @cache(field_type_cache_key, get_cache=store_on_portal)
98
    def is_boolean_field(self, field):
99
        """Check if the field is a boolean
100
        """
101
        return field.getWidgetName() == "BooleanWidget"
102
103
    @cache(field_type_cache_key, get_cache=store_on_portal)
104
    def is_date_field(self, field):
105
        """Check if the field is a date field
106
        """
107
        return field.getType().lower().find("datetime") > -1
108
109
    def get_boolean_field_data(self, field):
110
        """Get boolean field view data for the template
111
        """
112
        value = field.get(self.context)
113
        fieldname = field.getName()
114
115
        return {
116
            "fieldName": fieldname,
117
            "mode": "structure",
118
            "html": t(_("Yes")) if value else t(_("No"))
119
        }
120
121
    def get_reference_field_data(self, field):
122
        """Get reference field view data for the template
123
        """
124
        targets = None
125
        fieldname = field.getName()
126
127
        accessor = getattr(self.context, "get%s" % fieldname, None)
128
        if accessor and callable(accessor):
129
            targets = accessor()
130
        else:
131
            targets = field.get(self.context)
132
133
        if targets:
134
            if not isinstance(targets, list):
135
                targets = [targets, ]
136
137
            if all([check_permission(view, target) for target in targets]):
138
                elements = [
139
                    "<div id='{id}' class='field reference'>"
140
                    "  <a class='link' uid='{uid}' href='{url}'>"
141
                    "    {title}"
142
                    "  </a>"
143
                    "</div>"
144
                    .format(id=target.getId(),
145
                            uid=target.UID(),
146
                            url=target.absolute_url(),
147
                            title=target.Title())
148
                    for target in targets]
149
150
                return {
151
                    "fieldName": fieldname,
152
                    "mode": "structure",
153
                    "html": "".join(elements),
154
                }
155
            else:
156
                return {
157
                    "fieldName": fieldname,
158
                    "mode": "structure",
159
                    "html": ", ".join([ta.Title() for ta in targets]),
160
                }
161
162
        return {
163
            "fieldName": fieldname,
164
            "mode": "structure",
165
            "html": ""
166
        }
167
168
    def get_date_field_data(self, field):
169
        """Render date field view data for the template
170
        """
171
        value = field.get(self.context)
172
        fieldname = field.getName()
173
174
        return {
175
            "fieldName": fieldname,
176
            "mode": "structure",
177
            "html": self.ulocalized_time(value, long_format=True)
178
        }
179
180
    def three_column_list(self, input_list):
181
        list_len = len(input_list)
182
183
        # Calculate the length of the sublists
184
        sublist_len = (list_len % 3 == 0 and list_len / 3 or list_len / 3 + 1)
185
186
        def _list_end(num):
187
            # Calculate the list end point given the list number
188
            return num == 2 and list_len or (num + 1) * sublist_len
189
190
        # Generate only filled columns
191
        final = []
192
        for i in range(3):
193
            column = input_list[i * sublist_len:_list_end(i)]
194
            if len(column) > 0:
195
                final.append(column)
196
        return final
197
198
    def render_field_view(self, field):
199
        fieldname = field.getName()
200
201
        # lookup custom render adapter
202
        adapter = queryAdapter(self.context,
203
                               interface=IHeaderTableFieldRenderer,
204
                               name=fieldname)
205
206
        # Note: Adapters for contact fields:
207
        #       bika.lims.browser.analysisrequest.mailto_link_from_contacts
208
        #       bika.lims.browser.analysisrequest.mailto_link_from_ccemails
209
        #
210
        # -> TODO Remove?
211
212
        # return immediately if we have an adapter
213
        if adapter is not None:
214
            return {"fieldName": fieldname,
215
                    "mode": "structure",
216
                    "html": adapter(field)}
217
218
        # field data for *view* mode for the template
219
        data = {"fieldName": fieldname, "mode": "view"}
220
221
        if self.is_boolean_field(field):
222
            data = self.get_boolean_field_data(field)
223
        elif self.is_reference_field(field):
224
            data = self.get_reference_field_data(field)
225
        elif self.is_date_field(field):
226
            data = self.get_date_field_data(field)
227
228
        return data
229
230
    def get_field_visibility_mode(self, field):
231
        """Returns "view" or "edit" modes, together with the place within where
232
        this field has to be rendered, based on the permissions the current
233
        user has for the context and the field passed in
234
        """
235
        fallback_mode = ("hidden", "hidden")
236
        widget = field.widget
237
238
        # TODO This needs to be done differently
239
        # Check where the field has to be located
240
        layout = widget.isVisible(self.context, "header_table")
241
        if layout in ["invisible", "hidden"]:
242
            return fallback_mode
243
244
        # Check permissions. We want to display field (either in view or edit
245
        # modes) only if the current user has enough privileges.
246
        if field.checkPermission("edit", self.context):
247
            mode = "edit"
248
            if not self.is_edit_allowed():
249
                logger.warn("Permission '{}' granted for the edition of '{}', "
250
                            "but 'Modify portal content' not granted"
251
                            .format(field.write_permission, field.getName()))
252
        elif field.checkPermission("view", self.context):
253
            mode = "view"
254
        else:
255
            return fallback_mode
256
257
        # Check if the field needs to be displayed or not, even if the user has
258
        # the permissions for edit or view. This may depend on criteria other
259
        # than permissions (e.g. visibility depending on a setup setting, etc.)
260
        if widget.isVisible(self.context, mode, field=field) != "visible":
261
            if mode == "view":
262
                return fallback_mode
263
            # The field cannot be rendered in edit mode, but maybe can be
264
            # rendered in view mode.
265
            mode = "view"
266
            if widget.isVisible(self.context, mode, field=field) != "visible":
267
                return fallback_mode
268
        return (mode, layout)
269
270
    def get_fields_grouped_by_location(self):
271
        standard = []
272
        prominent = []
273
        for field in self.context.Schema().fields():
274
            mode, layout = self.get_field_visibility_mode(field)
275
            if mode == "hidden":
276
                # Do not render this field
277
                continue
278
            field_mapping = {"fieldName": field.getName(), "mode": mode}
279
            if mode == "view":
280
                # Special formatting (e.g. links, etc.)
281
                field_mapping = self.render_field_view(field)
282
            if layout == "prominent":
283
                # Add the field at the top of the fields table
284
                prominent.append(field_mapping)
285
            else:
286
                # Add the field at standard location
287
                standard.append(field_mapping)
288
        return prominent, self.three_column_list(standard)
289