Passed
Push — master ( 9e6aa2...b9e514 )
by Ramon
07:36 queued 02:57
created

RemarksField.searchable()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nop 1
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
import re
22
23
import six
24
25
from AccessControl import ClassSecurityInfo
26
from bika.lims import api
27
from bika.lims.browser.widgets import RemarksWidget
28
from bika.lims.events import RemarksAddedEvent
29
from bika.lims.interfaces import IRemarksField
30
from bika.lims.utils import tmpID
31
from DateTime import DateTime
32
from Products.Archetypes.event import ObjectEditedEvent
33
from Products.Archetypes.Field import ObjectField
34
from Products.Archetypes.Registry import registerField
35
from Products.CMFPlone.i18nl10n import ulocalized_time
36
from zope import event
37
from zope.interface import implements
38
39
40
class RemarksHistory(list):
41
    """A list containing a remarks history, but __str__ returns the legacy
42
    format from instances prior v1.3.3
43
    """
44
45
    def html(self):
46
        return api.text_to_html(str(self))
47
48
    def __str__(self):
49
        """Returns the remarks in legacy format
50
        """
51
        remarks = map(lambda rec: str(rec), self)
52
        remarks = filter(None, remarks)
53
        return "\n".join(remarks)
54
55
    def __eq__(self, y):
56
        if isinstance(y, six.string_types):
57
            return str(self) == y
58
        return super(RemarksHistory, self).__eq__(y)
59
60
61
class RemarksHistoryRecord(dict):
62
    """A dict implementation that represents a record/entry of Remarks History
63
    """
64
65
    def __init__(self, *arg, **kw):
66
        super(RemarksHistoryRecord, self).__init__(*arg, **kw)
67
        self["id"] = self.id or tmpID()
68
        self["user_id"] = self.user_id
69
        self["user_name"] = self.user_name
70
        self["created"] = self.created or DateTime().ISO()
71
        self["content"] = self.content
72
73
    @property
74
    def id(self):
75
        return self.get("id", "")
76
77
    @property
78
    def user_id(self):
79
        return self.get("user_id", "")
80
81
    @property
82
    def user_name(self):
83
        return self.get("user_name", "")
84
85
    @property
86
    def created(self):
87
        return self.get("created", "")
88
89
    @property
90
    def created_ulocalized(self):
91
        return ulocalized_time(self.created,
92
                               long_format=True,
93
                               context=api.get_portal(),
94
                               request=api.get_request(),
95
                               domain="senaite.core")
96
97
    @property
98
    def content(self):
99
        return self.get("content", "")
100
101
    @property
102
    def html_content(self):
103
        return api.text_to_html(self.content)
104
105
    def __str__(self):
106
        """Returns a legacy string format of the Remarks record
107
        """
108
        if not self.content:
109
            return ""
110
        if self.created and self.user_id:
111
            # Build the legacy format
112
            return "=== {} ({})\n{}".format(self.created, self.user_id,
113
                                            self.content)
114
        return self.content
115
116
117
class RemarksField(ObjectField):
118
    """A field that stores remarks.  The value submitted to the setter
119
    will always be appended to the actual value of the field.
120
    A divider will be included with extra information about the text.
121
    """
122
123
    _properties = ObjectField._properties.copy()
124
    _properties.update({
125
        'type': 'remarks',
126
        'widget': RemarksWidget,
127
        'default': '',
128
    })
129
130
    implements(IRemarksField)
131
    security = ClassSecurityInfo()
132
133
    @property
134
    def searchable(self):
135
        """Returns False, preventing this field to be searchable by AT's
136
        SearcheableText
137
        """
138
        return False
139
140
    @security.private
141
    def set(self, instance, value, **kwargs):
142
        """Adds the value to the existing text stored in the field,
143
        along with a small divider showing username and date of this entry.
144
        """
145
146
        if not value:
147
            return
148
149
        if isinstance(value, RemarksHistory):
150
            # Override the whole history here
151
            history = value
152
153
        elif isinstance(value, (list, tuple)):
154
            # This is a list, convert to RemarksHistory
155
            remarks = map(lambda item: RemarksHistoryRecord(item), value)
156
            history = RemarksHistory(remarks)
157
158
        elif isinstance(value, RemarksHistoryRecord):
159
            # This is a record, append to the history
160
            history = self.get_history(instance)
161
            history.insert(0, value)
162
163
        elif isinstance(value, six.string_types):
164
            # Create a new history record
165
            record = self.to_history_record(value)
166
167
            # Append the new record to the history
168
            history = self.get_history(instance)
169
            history.insert(0, record)
170
171
        else:
172
            raise ValueError("Type not supported: {}".format(type(value)))
173
174
        # Store the data
175
        ObjectField.set(self, instance, history)
176
177
        # N.B. ensure updated catalog metadata for the snapshot
178
        instance.reindexObject()
179
180
        # notify object edited event
181
        event.notify(ObjectEditedEvent(instance))
182
183
        # notify new remarks for e.g. later email notification etc.
184
        event.notify(RemarksAddedEvent(instance, history))
185
186
    def get(self, instance, **kwargs):
187
        """Returns a RemarksHistory object
188
        """
189
        return self.get_history(instance)
190
191
    def getRaw(self, instance, **kwargs):
192
        """Returns raw field value (possible wrapped in BaseUnit)
193
        """
194
        value = ObjectField.get(self, instance, **kwargs)
195
        # getattr(instance, "Remarks") returns a BaseUnit
196
        if callable(value):
197
            value = value()
198
        return value
199
200
    def to_history_record(self, value):
201
        """Transforms the value to an history record
202
        """
203
        user = api.get_current_user()
204
        contact = api.get_user_contact(user)
205
        fullname = contact and contact.getFullname() or ""
206
        if not contact:
207
            # get the fullname from the user properties
208
            props = api.get_user_properties(user)
209
            fullname = props.get("fullname", "")
210
        return RemarksHistoryRecord(user_id=user.id,
211
                                    user_name=fullname,
212
                                    content=value.strip())
213
214
    def get_history(self, instance):
215
        """Returns a RemarksHistory object with the remarks entries
216
        """
217
        remarks = instance.getRawRemarks()
218
        if not remarks:
219
            return RemarksHistory()
220
221
        # Backwards compatibility with legacy from < v1.3.3
222
        if isinstance(remarks, six.string_types):
223
            parsed_remarks = self._parse_legacy_remarks(remarks)
224
            if parsed_remarks is None:
225
                remark = RemarksHistoryRecord(content=remarks.strip())
226
                remarks = RemarksHistory([remark, ])
227
            else:
228
                remarks = RemarksHistory(
229
                    map(lambda r: RemarksHistoryRecord(r), parsed_remarks))
230
231
        return remarks
232
233
    def _parse_legacy_remarks(self, remarks):
234
        """Parse legacy remarks
235
        """
236
        records = []
237
        # split legacy remarks on the "===" delimiter into lines
238
        lines = remarks.split("===")
239
        for line in lines:
240
            # skip empty lines
241
            if line == "":
242
                continue
243
244
            # strip leading and trailing whitespaces
245
            line = line.strip()
246
247
            # split the line into date, user and content
248
            groups = re.findall(r"(.*) \((.*)\)\n(.*)", line, re.DOTALL)
249
250
            # we should have one tuple in the list
251
            if len(groups) != 1:
252
                continue
253
254
            group = groups[0]
255
256
            # cancel the whole parsing
257
            if len(group) != 3:
258
                return None
259
260
            created, userid, content = group
261
262
            # try to get the full name of the user id
263
            fullname = self._get_fullname_from_user_id(userid)
264
265
            # append the record
266
            records.append({
267
                "created": created,
268
                "user_id": userid,
269
                "user_name": fullname,
270
                "content": content,
271
            })
272
273
        return records
274
275
    def _get_fullname_from_user_id(self, userid, default=""):
276
        """Try the fullname of the user
277
        """
278
        fullname = default
279
        user = api.get_user(userid)
280
        if user:
281
            props = api.get_user_properties(user)
282
            fullname = props.get("fullname", fullname)
283
            contact = api.get_user_contact(user)
284
            fullname = contact and contact.getFullname() or fullname
285
        return fullname
286
287
288
registerField(RemarksField, title="Remarks", description="")
289