Passed
Push — 2.x ( 041ff6...87c3c9 )
by Jordi
08:54
created

bika.lims.content.contact.Contact.getContacts()   B

Complexity

Conditions 6

Size

Total Lines 10
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 10
rs 8.6666
c 0
b 0
f 0
cc 6
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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import types
22
23
from AccessControl import ClassSecurityInfo
24
from Acquisition import aq_inner
25
from Acquisition import aq_parent
26
from bika.lims import bikaMessageFactory as _
27
from bika.lims import logger
28
from bika.lims.api import is_active
29
from bika.lims.api import get_path
30
from bika.lims.browser.fields import UIDReferenceField
31
from bika.lims.config import PROJECTNAME
32
from bika.lims.content.person import Person
33
from bika.lims.interfaces import IClient
34
from bika.lims.interfaces import IContact
35
from bika.lims.interfaces import IDeactivable
36
from plone import api
37
from Products.Archetypes import atapi
38
from Products.CMFCore.permissions import ModifyPortalContent
39
from Products.CMFPlone.utils import safe_unicode
40
from senaite.core.browser.widgets.referencewidget import ReferenceWidget
41
from senaite.core.catalog import CONTACT_CATALOG
42
from zope.interface import implements
43
44
schema = Person.schema.copy() + atapi.Schema((
45
    UIDReferenceField(
46
        "CCContact",
47
        schemata="Publication preference",
48
        multiValued=1,
49
        allowed_types=("Contact",),
50
        widget=ReferenceWidget(
51
            label=_(
52
                "label_contact_cccontact",
53
                default="Contacts to CC"),
54
            description=_(
55
                "description_contact_cccontact",
56
                default="Contacts in CC for new samples"),
57
            catalog=CONTACT_CATALOG,
58
            query="get_widget_cccontact_query",
59
            columns=[
60
                {"name": "getFullname", "label": _("Name")},
61
                {"name": "getEmailAddress", "label": _("Email")},
62
            ],
63
        )),
64
))
65
66
67
schema["JobTitle"].schemata = "default"
68
schema["Department"].schemata = "default"
69
# Don"t make title required - it will be computed from the Person"s Fullname
70
schema["title"].required = 0
71
schema["title"].widget.visible = False
72
73
74
class Contact(Person):
75
    """A Contact of a Client which can be linked to a System User
76
    """
77
    implements(IContact, IDeactivable)
78
79
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
80
    security = ClassSecurityInfo()
81
    _at_rename_after_creation = True
82
83
    def get_widget_cccontact_query(self, **kw):
84
        """Return the query for the CCContact field
85
        """
86
        path = get_path(self.aq_parent)
87
        query = {
88
            "portal_type": "Contact",
89
            "path": {"query": path, "depth": 1},
90
            "is_active": True,
91
            "sort_on": "sortable_title",
92
            "sort_order": "ascending",
93
        }
94
        logger.info("get_widget_contact_query: %r" % query)
95
        return query
96
97
    @classmethod
98
    def getContactByUsername(cls, username):
99
        """Convenience Classmethod which returns a Contact by a Username
100
        """
101
102
        # Check if the User is linked already
103
        cat = api.portal.get_tool(CONTACT_CATALOG)
104
        contacts = cat(portal_type=cls.portal_type,
105
                       getUsername=username)
106
107
        # No Contact assigned to this username
108
        if len(contacts) == 0:
109
            return None
110
111
        # Multiple Users assigned, this should never happen
112
        if len(contacts) > 1:
113
            logger.error("User '{}' is bound to multiple Contacts '{}'".format(
114
                username, ",".join(map(lambda c: c.Title, contacts))))
115
            return map(lambda x: x.getObject(), contacts)
116
117
        # Return the found Contact object
118
        return contacts[0].getObject()
119
120
    def Title(self):
121
        """Return the contact's Fullname as title
122
        """
123
        return safe_unicode(self.getFullname()).encode('utf-8')
124
125
    def isActive(self):
126
        """Checks if the Contact is active
127
        """
128
        return is_active(self)
129
130
    @security.protected(ModifyPortalContent)
131
    def getUser(self):
132
        """Returns the linked Plone User or None
133
        """
134
        username = self.getUsername()
135
        if not username:
136
            return None
137
        user = api.user.get(userid=username)
138
        return user
139
140
    @security.protected(ModifyPortalContent)
141
    def setUser(self, user_or_username):
142
        """Link the user to the Contact
143
144
        :returns: True if OK, False if the User could not be linked
145
        :rtype: bool
146
        """
147
        user = None
148
        userid = None
149
150
        # Handle User IDs (strings)
151
        if isinstance(user_or_username, types.StringTypes):
152
            userid = user_or_username
153
            user = api.user.get(userid)
154
        # Handle User Objects (MemberData/PloneUser)
155
        if hasattr(user_or_username, "getId"):
156
            userid = user_or_username.getId()
157
            user = user_or_username
158
159
        # Not a valid user
160
        if user is None:
161
            return False
162
163
        # Link the User
164
        return self._linkUser(user)
165
166
    @security.protected(ModifyPortalContent)
167
    def unlinkUser(self, delete=False):
168
        """Unlink the user to the Contact
169
170
        :returns: True if OK, False if no User was unlinked
171
        :rtype: bool
172
        """
173
        userid = self.getUsername()
174
        user = self.getUser()
175
        if user:
176
            logger.debug("Unlinking User '{}' from Contact '{}'".format(
177
                userid, self.Title()))
178
179
            # Unlink the User
180
            if not self._unlinkUser():
181
                return False
182
183
            # Also remove the Plone User (caution)
184
            if delete:
185
                logger.debug("Removing Plone User '{}'".format(userid))
186
                api.user.delete(username=userid)
187
188
            return True
189
        return False
190
191
    @security.protected(ModifyPortalContent)
192
    def hasUser(self):
193
        """Check if Contact has a linked a System User
194
        """
195
        user = self.getUser()
196
        if user is None:
197
            return False
198
        return True
199
200
    def getParentUID(self):
201
        return self.aq_parent.UID()
202
203
    def getParent(self):
204
        return aq_parent(aq_inner(self))
205
206
    def _renameAfterCreation(self, check_auto_id=False):
207
        from senaite.core.idserver import renameAfterCreation
208
        renameAfterCreation(self)
209
210
    @security.private
211
    def _linkUser(self, user):
212
        """Set the UID of the current Contact in the User properties and update
213
        all relevant own properties.
214
        """
215
        KEY = "linked_contact_uid"
216
217
        username = user.getId()
218
        contact = self.getContactByUsername(username)
219
220
        # User is linked to another contact (fix in UI)
221
        if contact and contact.UID() != self.UID():
222
            raise ValueError("User '{}' is already linked to Contact '{}'"
223
                             .format(username, contact.Title()))
224
225
        # User is linked to multiple other contacts (fix in Data)
226
        if isinstance(contact, list):
227
            raise ValueError("User '{}' is linked to multiple Contacts: '{}'"
228
                             .format(username, ",".join(
229
                                 map(lambda x: x.Title(), contact))))
230
231
        # XXX: Does it make sense to "remember" the UID as a User property?
232
        try:
233
            user.getProperty(KEY)
234
        except ValueError:
235
            logger.info("Adding User property {}".format(KEY))
236
            user._tool.manage_addProperty(KEY, "", "string")
237
238
        # Set the UID as a User Property
239
        uid = self.UID()
240
        user.setMemberProperties({KEY: uid})
241
        logger.info("Linked Contact UID {} to User {}".format(
242
            user.getProperty(KEY), username))
243
244
        # Set the Username
245
        self.setUsername(user.getId())
246
247
        # Update the Email address from the user
248
        self.setEmailAddress(user.getProperty("email"))
249
250
        # somehow the `getUsername` index gets out of sync
251
        self.reindexObject()
252
253
        # N.B. Local owner role and client group applies only to client
254
        #      contacts, but not lab contacts.
255
        if IClient.providedBy(self.aq_parent):
256
            # add user to clients group
257
            self.aq_parent.add_user_to_group(username)
258
259
        return True
260
261
    @security.private
262
    def _unlinkUser(self):
263
        """Remove the UID of the current Contact in the User properties and
264
        update all relevant own properties.
265
        """
266
        KEY = "linked_contact_uid"
267
268
        # Nothing to do if no user is linked
269
        if not self.hasUser():
270
            return False
271
272
        user = self.getUser()
273
        username = user.getId()
274
275
        # Unset the UID from the User Property
276
        user.setMemberProperties({KEY: ""})
277
        logger.info("Unlinked Contact UID from User {}"
278
                    .format(user.getProperty(KEY, "")))
279
280
        # Unset the Username
281
        self.setUsername("")
282
283
        # Unset the Email
284
        self.setEmailAddress("")
285
286
        # somehow the `getUsername` index gets out of sync
287
        self.reindexObject()
288
289
        # N.B. Local owner role and client group applies only to client
290
        #      contacts, but not lab contacts.
291
        if IClient.providedBy(self.aq_parent):
292
            # remove user from clients group
293
            self.aq_parent.del_user_from_group(username)
294
295
        return True
296
297
298
atapi.registerType(Contact, PROJECTNAME)
299