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

senaite.core.content.clientcontact   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 55
eloc 258
dl 0
loc 399
rs 6
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A ClientContact.getLastname() 0 3 1
A ClientContact.getCCContacts() 0 5 1
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.getBusinessPhone() 0 3 1
A ClientContact.getJobTitle() 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.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 IClientContactSchema.validate_phone() 0 7 3
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.getFullname() 0 6 1
A ClientContact.getDepartment() 0 3 1

1 Function

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