Passed
Push — 2.x ( 5ff915...047977 )
by Jordi
06:19
created

bika.lims.content.contact   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 38
eloc 159
dl 0
loc 290
rs 9.36
c 0
b 0
f 0

13 Methods

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