Passed
Pull Request — 2.x (#1955)
by Jordi
06:45
created

senaite.core.content.clientcontact   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 54
eloc 238
dl 0
loc 354
rs 6.4799
c 0
b 0
f 0

33 Methods

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