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

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

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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