Passed
Push — 2.x ( 74703e...8baf66 )
by Jordi
06:04
created

bika.lims.content.client.Client._get_group_id()   A

Complexity

Conditions 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 21
rs 9.75
c 0
b 0
f 0
cc 4
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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import six
22
from AccessControl import ClassSecurityInfo
23
from AccessControl import Unauthorized
24
from bika.lims import _
25
from bika.lims import api
26
from bika.lims.browser.fields import EmailsField
27
from bika.lims.browser.fields import UIDReferenceField
28
from bika.lims.catalog.bikasetup_catalog import SETUP_CATALOG
29
from bika.lims.config import DECIMAL_MARKS
30
from bika.lims.config import PROJECTNAME
31
from bika.lims.content.attachment import Attachment
32
from bika.lims.content.organisation import Organisation
33
from bika.lims.interfaces import IClient
34
from bika.lims.interfaces import IDeactivable
35
from Products.Archetypes.public import BooleanField
36
from Products.Archetypes.public import BooleanWidget
37
from Products.Archetypes.public import Schema
38
from Products.Archetypes.public import SelectionWidget
39
from Products.Archetypes.public import StringField
40
from Products.Archetypes.public import StringWidget
41
from Products.Archetypes.public import registerType
42
from Products.ATContentTypes.content import schemata
43
from Products.CMFCore import permissions
44
from Products.CMFCore.PortalFolder import PortalFolderBase as PortalFolder
45
from Products.CMFCore.utils import _checkPermission
46
from Products.CMFPlone.RegistrationTool import get_member_by_login_name
47
from senaite.core.browser.widgets.referencewidget import ReferenceWidget
48
from zope.interface import implements
49
50
schema = Organisation.schema.copy() + Schema((
51
    StringField(
52
        "ClientID",
53
        required=1,
54
        searchable=True,
55
        validators=("uniquefieldvalidator", "standard_id_validator"),
56
        widget=StringWidget(
57
            label=_("Client ID"),
58
            description=_(
59
                "Short and unique identifier of this client. Besides fast "
60
                "searches by client in Samples listings, the purposes of this "
61
                "field depend on the laboratory needs. For instance, the "
62
                "Client ID can be included as part of the Sample identifier, "
63
                "so the lab can easily know the client a given sample belongs "
64
                "to by just looking to its ID.")
65
        ),
66
    ),
67
68
    BooleanField(
69
        "BulkDiscount",
70
        default=False,
71
        widget=BooleanWidget(
72
            label=_("Bulk discount applies"),
73
        ),
74
    ),
75
76
    BooleanField(
77
        "MemberDiscountApplies",
78
        default=False,
79
        widget=BooleanWidget(
80
            label=_("Member discount applies"),
81
        ),
82
    ),
83
84
    EmailsField(
85
        "CCEmails",
86
        schemata="Preferences",
87
        mode="rw",
88
        widget=StringWidget(
89
            label=_("CC Emails"),
90
            description=_(
91
                "Default Emails to CC all published Samples for this client"),
92
            visible={
93
                "edit": "visible",
94
                "view": "visible",
95
            },
96
        ),
97
    ),
98
99
    UIDReferenceField(
100
        "DefaultCategories",
101
        schemata="Preferences",
102
        required=0,
103
        multiValued=1,
104
        allowed_types=("AnalysisCategory",),
105
        widget=ReferenceWidget(
106
            label=_("Default categories"),
107
            description=_(
108
                "Always expand the selected categories in client views"),
109
            showOn=True,
110
            catalog_name=SETUP_CATALOG,
111
            base_query=dict(
112
                is_active=True,
113
                sort_on="sortable_title",
114
                sort_order="ascending",
115
            ),
116
        ),
117
    ),
118
119
    UIDReferenceField(
120
        "RestrictedCategories",
121
        schemata="Preferences",
122
        required=0,
123
        multiValued=1,
124
        validators=("restrictedcategoriesvalidator",),
125
        allowed_types=("AnalysisCategory",),
126
        widget=ReferenceWidget(
127
            label=_("Restrict categories"),
128
            description=_("Show only selected categories in client views"),
129
            showOn=True,
130
            catalog_name=SETUP_CATALOG,
131
            base_query=dict(
132
                is_active=True,
133
                sort_on="sortable_title",
134
                sort_order="ascending",
135
            ),
136
        ),
137
    ),
138
139
    BooleanField(
140
        "DefaultDecimalMark",
141
        schemata="Preferences",
142
        default=True,
143
        widget=BooleanWidget(
144
            label=_("Default decimal mark"),
145
            description=_(
146
                "The decimal mark selected in Bika Setup will be used."),
147
        )
148
    ),
149
150
    StringField(
151
        "DecimalMark",
152
        schemata="Preferences",
153
        vocabulary=DECIMAL_MARKS,
154
        default=".",
155
        widget=SelectionWidget(
156
            label=_("Custom decimal mark"),
157
            description=_(
158
                "Decimal mark to use in the reports from this Client."),
159
            format="select",
160
        )
161
    ),
162
))
163
164
schema["title"].widget.visible = False
165
schema["description"].widget.visible = False
166
schema["EmailAddress"].schemata = "default"
167
168
schema.moveField("ClientID", after="Name")
169
170
171
class Client(Organisation):
172
    implements(IClient, IDeactivable)
173
174
    security = ClassSecurityInfo()
175
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
176
    GROUP_KEY = "_client_group_id"
177
178
    def _renameAfterCreation(self, check_auto_id=False):
179
        from bika.lims.idserver import renameAfterCreation
180
        renameAfterCreation(self)
181
182
    @property
183
    def group_id(self):
184
        """Client group ID
185
        """
186
        if not hasattr(self, self.GROUP_KEY):
187
            setattr(self, self.GROUP_KEY, self._get_group_id())
188
        return getattr(self, self.GROUP_KEY)
189
190
    def _get_group_id(self):
191
        """Get a valid group ID
192
        """
193
        # Try first the client ID
194
        client_id = self.getClientID()
195
        if self.is_valid_group_id(client_id):
196
            return client_id
197
198
        # Use the context ID
199
        context_id = self.getId()
200
        if self.is_valid_group_id(context_id):
201
            return context_id
202
203
        # Use the ID + prefix
204
        prefix_id = "group_%s" % client_id or context_id
205
        count = 0
206
        while not self.is_valid_group_id(prefix_id):
207
            count += 1
208
            prefix_id = "group_%s_%s" % (count, client_id or context_id)
209
210
        return prefix_id
211
212
    def is_valid_group_id(self, group_id):
213
        """Check if the group ID is valid
214
215
        :param group_id: The group ID to validate
216
        """
217
        # Check for string
218
        if not api.is_string(group_id):
219
            return False
220
221
        # Check for empty string
222
        if not len(group_id) > 0:
223
            return False
224
225
        portal = api.get_portal()
226
        # Check if the ID is already used by a user login
227
        if get_member_by_login_name(portal, group_id, False):
228
            return False
229
230
        return True
231
232
    @security.public
233
    def get_group(self):
234
        """Returns our client group
235
236
        :returns: Client group object
237
        """
238
        portal_groups = api.get_tool("portal_groups")
239
        return portal_groups.getGroupById(self.group_id)
240
241
    @security.private
242
    def create_group(self):
243
        """Create a new client group
244
245
        :returns: Client group object
246
        """
247
        group = self.get_group()
248
249
        # return the existing Group immediately
250
        if group:
251
            return group
252
253
        portal_groups = api.get_tool("portal_groups")
254
        group_name = self.getName()
255
256
        # Create a new Client Group
257
        # NOTE: The global "Client" role is necessary for the client contacts
258
        portal_groups.addGroup(self.group_id,
259
                               roles=["Client"],
260
                               title=group_name)
261
262
        # Grant the group the "Owner" role on ourself
263
        # NOTE: This will grant each member of this group later immediate
264
        #       access to all exisiting objects with the same role.
265
        self.manage_setLocalRoles(self.group_id, ["Owner"])
266
267
        return self.get_group()
268
269
    @security.private
270
    def remove_group(self):
271
        """Remove the own client group
272
273
        :returns: True if the group was removed, otherwise False
274
        """
275
        group = self.get_group()
276
        if not group:
277
            return False
278
        portal_groups = api.get_tool("portal_groups")
279
        # Use the client ID for the group ID
280
        portal_groups.removeGroup(self.group_id)
281
        # also remove the group attribute
282
        delattr(self, self.GROUP_KEY)
283
        return True
284
285
    @security.private
286
    def add_user_to_group(self, user):
287
        """Add a user to the client group
288
289
        :param user: user/group object or user/group ID
290
        :returns: list of new group IDs
291
        """
292
        group = self.get_group()
293
        if not group:
294
            group = self.create_group()
295
        return api.user.add_group(group, user)
296
297
    @security.private
298
    def del_user_from_group(self, user):
299
        """Add a user to the client group
300
301
        :param user: user/group object or user/group ID
302
        :returns: list of new group IDs
303
        """
304
        group = self.get_group()
305
        if not group:
306
            group = self.create_group()
307
        return api.user.del_group(group, user)
308
309
    @security.public
310
    def getContactFromUsername(self, username):
311
        for contact in self.objectValues("Contact"):
312
            if contact.getUsername() == username:
313
                return contact.UID()
314
315
    @security.public
316
    def getContacts(self, active=True):
317
        """Return an array containing the contacts from this Client
318
        """
319
        path = api.get_path(self)
320
        query = {
321
            "portal_type": "Contact",
322
            "path": {"query": path},
323
            "is_active": active,
324
        }
325
        brains = api.search(query)
326
        contacts = map(api.get_object, brains)
327
        return list(contacts)
328
329
    @security.public
330
    def getDecimalMark(self):
331
        """Return the decimal mark to be used on reports for this client
332
333
        If the client has DefaultDecimalMark selected, the Default value from
334
        the LIMS Setup will be returned.
335
336
        Otherwise, will return the value of DecimalMark.
337
        """
338
        if self.getDefaultDecimalMark() is False:
339
            return self.Schema()["DecimalMark"].get(self)
340
        return self.bika_setup.getDecimalMark()
341
342
    @security.public
343
    def getCountry(self, default=None):
344
        """Return the Country from the Physical or Postal Address
345
        """
346
        physical_address = self.getPhysicalAddress().get("country", default)
347
        postal_address = self.getPostalAddress().get("country", default)
348
        return physical_address or postal_address
349
350
    @security.public
351
    def getProvince(self, default=None):
352
        """Return the Province from the Physical or Postal Address
353
        """
354
        physical_address = self.getPhysicalAddress().get("state", default)
355
        postal_address = self.getPostalAddress().get("state", default)
356
        return physical_address or postal_address
357
358
    @security.public
359
    def getDistrict(self, default=None):
360
        """Return the Province from the Physical or Postal Address
361
        """
362
        physical_address = self.getPhysicalAddress().get("district", default)
363
        postal_address = self.getPostalAddress().get("district", default)
364
        return physical_address or postal_address
365
366
    # TODO Security Make Attachments live inside ARs (instead of Client)
367
    # Since the Attachments live inside Client, we are forced here to overcome
368
    # the DeleteObjects permission when objects to delete are from Attachment
369
    # type. And we want to keep the DeleteObjects permission at Client level
370
    # because is the main container for Samples!
371
    # For some statuses of the AnalysisRequest type (e.g. received), the
372
    # permission "DeleteObjects" is granted, allowing the user to remove e.g.
373
    # analyses. Attachments are closely bound to Analysis and Samples, so they
374
    # should live inside Analysis Request.
375
    # Then, we will be able to remove this function from here
376
    def manage_delObjects(self, ids=None, REQUEST=None):
377
        """Overrides parent function. If the ids passed in are from Attachment
378
        types, the function ignores the DeleteObjects permission. For the rest
379
        of types, it works as usual (checks the permission)
380
        """
381
        if ids is None:
382
            ids = []
383
        if isinstance(ids, six.string_types):
384
            ids = [ids]
385
386
        for id in ids:
387
            item = self._getOb(id)
388
            if isinstance(item, Attachment):
389
                # Ignore DeleteObjects permission check
390
                continue
391
            if not _checkPermission(permissions.DeleteObjects, item):
392
                msg = "Do not have permissions to remove this object"
393
                raise Unauthorized(msg)
394
395
        return PortalFolder.manage_delObjects(self, ids, REQUEST=REQUEST)
396
397
398
schemata.finalizeATCTSchema(schema, folderish=True, moveDiscussion=False)
399
400
registerType(Client, PROJECTNAME)
401