Passed
Pull Request — 2.x (#1958)
by Jordi
05:23
created

senaite.core.content.clientcontact   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 266
dl 0
loc 412
rs 4.5599
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A ClientContact.getLastname() 0 3 1
A ClientContact.getCCContacts() 0 7 2
A ClientContact.mutator() 0 8 2
A ClientContact.setMobilePhone() 0 7 3
A ClientContact.getEmail() 0 3 1
A ClientContact.set_string_value() 0 11 3
A ClientContact.Title() 0 3 1
A ClientContact.setCCContacts() 0 4 1
A ClientContact.getJobTitle() 0 3 1
A ClientContact.getBusinessPhone() 0 3 1
A ClientContact.getUsername() 0 3 1
A ClientContact.setHomePhone() 0 7 3
A ClientContact.get_cc_contacts_query() 0 21 2
A IClientContactSchema.validate_email() 0 11 3
A ClientContact.setUsername() 0 3 1
A ClientContact.getFirstname() 0 3 1
A ClientContact.setBusinessPhone() 0 7 3
A ClientContact.getMobilePhone() 0 3 1
A ClientContact.setSalutation() 0 3 1
A IClientContactSchema.validate_business_phone() 0 5 1
A ClientContact.accessor() 0 8 2
A ClientContact.getSalutation() 0 3 1
A ClientContact.setEmail() 0 7 3
A ClientContact.get_string_value() 0 5 1
A ClientContact.setDepartment() 0 3 1
A ClientContact.getClient() 0 8 2
A ClientContact.setFirstname() 0 3 1
A ClientContact.getHomePhone() 0 3 1
A IClientContactSchema.validate_home_phone() 0 5 1
A ClientContact.getAddress() 0 4 1
A IClientContactSchema.validate_mobile_phone() 0 5 1
A ClientContact.setAddress() 0 4 1
A ClientContact.setLastname() 0 3 1
A ClientContact.setJobTitle() 0 3 1
A ClientContact.getDepartment() 0 3 1
A ClientContact.getFullname() 0 6 1

2 Functions

Rating   Name   Duplication   Size   Complexity  
A validate_phone() 0 9 3
A is_valid_phone() 0 10 2

How to fix   Complexity   

Complexity

Complex classes like senaite.core.content.clientcontact often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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-2022 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import re
22
23
from AccessControl import ClassSecurityInfo
24
from bika.lims import api
25
from bika.lims import senaiteMessageFactory as _
26
from bika.lims.api.mail import is_valid_email_address
27
from bika.lims.interfaces import IClient
28
from bika.lims.interfaces import IDeactivable
29
from plone.autoform import directives
30
from plone.dexterity.content import Container
31
from plone.supermodel import model
32
from Products.CMFCore import permissions
33
from senaite.core.interfaces import IClientContact
34
from senaite.core.schema import AddressField
35
from senaite.core.schema import UIDReferenceField
36
from senaite.core.schema.addressfield import PHYSICAL_ADDRESS
37
from senaite.core.schema.addressfield import POSTAL_ADDRESS
38
from senaite.core.z3cform.widgets.uidreference import UIDReferenceWidgetFactory
39
from six import string_types
40
from zope import schema
41
from zope.interface import implementer
42
from zope.interface import Invalid
43
from zope.interface import invariant
44
45
46
def validate_phone(phone):
47
    """Raises an error if phone is not valid
48
    """
49
    if not phone:
50
        return
51
52
    phone = phone.strip()
53
    if not is_valid_phone(phone):
54
        raise Invalid(_("Phone is not valid"))
55
56
57
def is_valid_phone(phone):
58
    """Returns whether the given phone is valid or not
59
    """
60
    # XXX Better validate with phonenumbers library here?
61
    # https://pypi.org/project/phonenumbers/
62
    phone = phone.strip()
63
    match = re.match(r"^[+(]?\d+(?:[- )(]+\d+)+$", phone)
64
    if not match:
65
        return False
66
    return True
67
68
69
class IClientContactSchema(model.Schema):
70
    """Client's Contact Schema interface
71
    """
72
73
    salutation = schema.TextLine(
74
        title=_(u"Salutation"),
75
        description=_(
76
            u"Greeting title eg. Mr, Mrs, Dr"
77
        ),
78
        required=False,
79
    )
80
81
    firstname = schema.TextLine(
82
        title=_(u"Firstname"),
83
        required=True,
84
    )
85
86
    lastname = schema.TextLine(
87
        title=_(u"Lastname"),
88
        required=True,
89
    )
90
91
    directives.omitted("username")
92
    username = schema.TextLine(
93
        title=_(u"Username"),
94
        required=False,
95
    )
96
97
    email = schema.TextLine(
98
        title=_(u"Email"),
99
        required=False,
100
    )
101
102
    business_phone = schema.TextLine(
103
        title=_(u"Phone (business)"),
104
        required=False,
105
    )
106
107
    mobile_phone = schema.TextLine(
108
        title=_(u"Phone (mobile)"),
109
        required=False,
110
    )
111
112
    home_phone = schema.TextLine(
113
        title=_(u"Phone (home)"),
114
        required=False,
115
    )
116
117
    job_title = schema.TextLine(
118
        title=_(u"Job title"),
119
        required=False,
120
    )
121
122
    department = schema.TextLine(
123
        title=_(u"Department"),
124
        required=False,
125
    )
126
127
    address = AddressField(
128
        title=_(u"Address"),
129
        address_types=[
130
            PHYSICAL_ADDRESS,
131
            POSTAL_ADDRESS,
132
        ]
133
    )
134
135
    cc_contacts = UIDReferenceField(
136
        title=_(u"Contacts to CC"),
137
        description=_(
138
            u"Contacts that will receive a copy of the notification emails "
139
            u"sent to this contact. Only contacts from same client are allowed"
140
        ),
141
        allowed_types=("ClientContact", ),
142
        multi_valued=True,
143
        required=False,
144
    )
145
146
    directives.widget(
147
        "cc_contacts",
148
        UIDReferenceWidgetFactory,
149
        catalog="portal_catalog",
150
        query="get_cc_contacts_query",
151
        display_template="<a href='${url}'>${firstname} ${lastname}</a>",
152
        columns=[
153
            {
154
                "name": "title",
155
                "width": "30",
156
                "align": "left",
157
                "label": _(u"Title"),
158
            }, {
159
                "name": "description",
160
                "width": "70",
161
                "align": "left",
162
                "label": _(u"Description"),
163
            },
164
        ],
165
        limit=15,
166
167
    )
168
169
    # Notification preferences fieldset
170
    model.fieldset(
171
        "notification_preferences",
172
        label=_(u"Notification preferences"),
173
        fields=["cc_contacts",]
174
    )
175
176
    @invariant
177
    def validate_email(data):
178
        """Checks if the email is correct
179
        """
180
        email = data.email
181
        if not email:
182
            return
183
184
        email = email.strip()
185
        if not is_valid_email_address(email):
186
            raise Invalid(_("Email is not valid"))
187
188
    @invariant
189
    def validate_business_phone(data):
190
        """Checks if the business phone is correct
191
        """
192
        validate_phone(data.business_phone)
193
194
    @invariant
195
    def validate_home_phone(data):
196
        """Checks if the home phone is correct
197
        """
198
        validate_phone(data.home_phone)
199
200
    @invariant
201
    def validate_mobile_phone(data):
202
        """Checks if the mobile phone is correct
203
        """
204
        validate_phone(data.mobile_phone)
205
206
207
@implementer(IClientContact, IClientContactSchema, IDeactivable)
208
class ClientContact(Container):
209
    """Client Contact type
210
    """
211
    # Catalogs where this type will be catalogued
212
    _catalogs = ["portal_catalog"]
213
214
    security = ClassSecurityInfo()
215
216
    @security.private
217
    def accessor(self, fieldname):
218
        """Return the field accessor for the fieldname
219
        """
220
        schema = api.get_schema(self)
221
        if fieldname not in schema:
222
            return None
223
        return schema[fieldname].get
224
225
    @security.private
226
    def mutator(self, fieldname):
227
        """Return the field mutator for the fieldname
228
        """
229
        schema = api.get_schema(self)
230
        if fieldname not in schema:
231
            return None
232
        return schema[fieldname].set
233
234
    @security.protected(permissions.View)
235
    def Title(self):
236
        return self.getFullname()
237
238
    @security.private
239
    def set_string_value(self, field_name, value, validator=None):
240
        if not isinstance(value, string_types):
241
            value = u""
242
243
        value = value.strip()
244
        if validator:
245
            validator(value)
246
247
        mutator = self.mutator(field_name)
248
        mutator(self, api.safe_unicode(value))
249
250
    @security.private
251
    def get_string_value(self, field_name, default=""):
252
        accessor = self.accessor(field_name)
253
        value = accessor(self) or default
254
        return value.encode("utf-8")
255
256
    @security.protected(permissions.ModifyPortalContent)
257
    def setSalutation(self, value):
258
        self.set_string_value("salutation", value)
259
260
    @security.protected(permissions.View)
261
    def getSalutation(self):
262
        return self.get_string_value("salutation")
263
264
    @security.protected(permissions.ModifyPortalContent)
265
    def setFirstname(self, value):
266
        self.set_string_value("firstname", value)
267
268
    @security.protected(permissions.View)
269
    def getFirstname(self):
270
        return self.get_string_value("firstname")
271
272
    @security.protected(permissions.ModifyPortalContent)
273
    def setLastname(self, value):
274
        self.set_string_value("lastname", value)
275
276
    @security.protected(permissions.View)
277
    def getLastname(self):
278
        return self.get_string_value("lastname")
279
280
    @security.protected(permissions.ModifyPortalContent)
281
    def setUsername(self, value):
282
        self.set_string_value("username", value)
283
284
    @security.protected(permissions.View)
285
    def getUsername(self):
286
        return self.get_string_value("username")
287
288
    @security.protected(permissions.ModifyPortalContent)
289
    def setEmail(self, value):
290
        def validate_email(email):
291
            if email and not is_valid_email_address(email):
292
                raise ValueError("Email is not valid")
293
294
        self.set_string_value("email", value, validator=validate_email)
295
296
    @security.protected(permissions.View)
297
    def getEmail(self):
298
        return self.get_string_value("email")
299
300
    @security.protected(permissions.ModifyPortalContent)
301
    def setBusinessPhone(self, value):
302
        def validate_phone(phone):
303
            if phone and not is_valid_phone(phone):
304
                raise ValueError("Phone is not valid")
305
306
        self.set_string_value("business_phone", value, validator=validate_phone)
307
308
    @security.protected(permissions.View)
309
    def getBusinessPhone(self):
310
        return self.get_string_value("business_phone")
311
312
    @security.protected(permissions.ModifyPortalContent)
313
    def setMobilePhone(self, value):
314
        def validate_phone(phone):
315
            if phone and not is_valid_phone(phone):
316
                raise ValueError("Phone is not valid")
317
318
        self.set_string_value("mobile_phone", value, validator=validate_phone)
319
320
    @security.protected(permissions.View)
321
    def getMobilePhone(self):
322
        return self.get_string_value("mobile_phone")
323
324
    @security.protected(permissions.ModifyPortalContent)
325
    def setHomePhone(self, value):
326
        def validate_phone(phone):
327
            if phone and not is_valid_phone(phone):
328
                raise ValueError("Phone is not valid")
329
330
        self.set_string_value("home_phone", value, validator=validate_phone)
331
332
    @security.protected(permissions.View)
333
    def getHomePhone(self):
334
        return self.get_string_value("home_phone")
335
336
    @security.protected(permissions.ModifyPortalContent)
337
    def setJobTitle(self, value):
338
        self.set_string_value("job_title", value)
339
340
    @security.protected(permissions.View)
341
    def getJobTitle(self):
342
        return self.get_string_value("job_title")
343
344
    @security.protected(permissions.ModifyPortalContent)
345
    def setDepartment(self, value):
346
        self.set_string_value("department", value)
347
348
    @security.protected(permissions.View)
349
    def getDepartment(self):
350
        return self.get_string_value("department")
351
352
    @security.protected(permissions.ModifyPortalContent)
353
    def setAddress(self, value):
354
        mutator = self.mutator("address")
355
        mutator(self, value)
356
357
    @security.protected(permissions.View)
358
    def getAddress(self):
359
        accessor = self.accessor("address")
360
        return accessor(self)
361
362
    @security.protected(permissions.ModifyPortalContent)
363
    def setCCContacts(self, value):
364
        mutator = self.mutator("cc_contacts")
365
        mutator(self, value)
366
367
    @security.protected(permissions.View)
368
    def getCCContacts(self, as_objects=False):
369
        accessor = self.accessor("cc_contacts")
370
        uids = accessor(self) or []
371
        if as_objects:
372
            return [api.get_object(uid) for uid in uids]
373
        return [uid.encode("utf.8") for uid in uids]
374
375
    @security.protected(permissions.View)
376
    def getFullname(self):
377
        """Returns the fullname of this Client Contact
378
        """
379
        full = filter(None, [self.getFirstname(), self.getLastname()])
380
        return " ".join(full)
381
382
    @security.protected(permissions.View)
383
    def getClient(self):
384
        """Returns the client this Client Contact belongs to
385
        """
386
        obj = api.get_parent(self)
387
        if IClient.providedBy(obj):
388
            return obj
389
        return None
390
391
    @security.private
392
    def get_cc_contacts_query(self):
393
        """Returns the default query for the cc_contacts field. Only contacts
394
        from same client as the current one are displayed, if client set
395
        """
396
        query = {
397
            "portal_type": "ClientContact",
398
            "is_active": True,
399
            "sort_on": "title",
400
            "sort_order": "ascending",
401
        }
402
        client = self.getClient()
403
        if client:
404
            query.update({
405
                "path": {
406
                    "query": api.get_path(client),
407
                    "level": 0
408
                }
409
            })
410
411
        return query
412