Passed
Push — 2.x ( df5950...91c842 )
by Ramon
07:45
created

Setup.setInvalidationReasonRequired()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nop 2
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-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from bika.lims import api
23
from plone.app.textfield import IRichTextValue
24
from plone.app.textfield.widget import RichTextFieldWidget  # TBD: port to core
25
from plone.autoform import directives
26
from plone.formwidget.namedfile.widget import NamedFileFieldWidget
27
from plone.schema.email import Email
28
from plone.supermodel import model
29
from Products.CMFCore import permissions
30
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
31
from senaite.core.catalog import AUDITLOG_CATALOG
32
from senaite.core.content.base import Container
33
from senaite.core.interfaces import IHideActionsMenu
34
from senaite.core.interfaces import ISetup
35
from senaite.core.schema import RichTextField
36
from senaite.impress import senaiteMessageFactory as _
37
from zope import schema
38
from zope.interface import implementer
39
from zope.interface import provider
40
from zope.schema.interfaces import IContextAwareDefaultFactory
41
42
43
@provider(IContextAwareDefaultFactory)
44
def default_email_body_sample_publication(context):
45
    """Returns the default body text for publication emails
46
    """
47
    view = api.get_view("senaite_view", context=api.get_setup())
48
    if view is None:
49
        # Test fixture
50
        return u""
51
    tpl = ViewPageTemplateFile(
52
        "../browser/setup/templates/email_body_sample_publication.pt")
53
    return tpl(view)
54
55
56
@provider(IContextAwareDefaultFactory)
57
def default_email_from_sample_publication(context):
58
    """Returns the default email 'From' for results reports publish
59
    """
60
    portal_email = api.get_registry_record("plone.email_from_address")
61
    return portal_email
62
63
64
class ISetupSchema(model.Schema):
65
    """Schema and marker interface
66
    """
67
68
    email_from_sample_publication = Email(
69
        title=_(
70
            "title_senaitesetup_email_from_sample_publication",
71
            default="Publication 'From' address"
72
        ),
73
        description=_(
74
            "description_senaitesetup_email_from_sample_publication",
75
            default="E-mail to use as the 'From' address for outgoing e-mails "
76
                    "when publishing results reports. This address overrides "
77
                    "the value set at portal's 'Mail settings'."
78
        ),
79
        defaultFactory=default_email_from_sample_publication,
80
        required=False,
81
    )
82
83
    directives.widget("email_body_sample_publication", RichTextFieldWidget)
84
    email_body_sample_publication = RichTextField(
85
        title=_("title_senaitesetup_publication_email_text",
86
                default=u"Publication Email Text"),
87
        description=_(
88
            "description_senaitesetup_publication_email_text",
89
            default=u"Set the email body text to be used by default "
90
            "when sending out result reports to the selected recipients. "
91
            "You can use reserved keywords: "
92
            "$client_name, $recipients, $lab_name, $lab_address"),
93
        defaultFactory=default_email_body_sample_publication,
94
        required=False,
95
    )
96
97
    always_cc_responsibles_in_report_emails = schema.Bool(
98
        title=_(
99
            "title_senaitesetup_always_cc_responsibles_in_report_emails",
100
            default=u"Always send publication email to responsibles"),
101
        description=_(
102
            "description_senaitesetup_always_cc_responsibles_in_report_emails",
103
            default="When selected, the responsible persons of all involved "
104
            "lab departments will receive publication emails."
105
        ),
106
        default=True,
107
    )
108
109
    enable_global_auditlog = schema.Bool(
110
        title=_(u"Enable global Auditlog"),
111
        description=_(
112
            "The global Auditlog shows all modifications of the system. "
113
            "When enabled, all entities will be indexed in a separate "
114
            "catalog. This will increase the time when objects are "
115
            "created or modified."
116
        ),
117
        default=False,
118
    )
119
120
    # NOTE:
121
    # We use the `NamedFileFieldWidget` instead of `NamedImageFieldWidget`
122
    # by purpose! Using the latter rises this PIL error (appears only in log):
123
    # IOError: cannot identify image file <cStringIO.StringI object at ...>
124
    directives.widget("site_logo", NamedFileFieldWidget)
125
    site_logo = schema.Bytes(
126
        title=_(u"Site Logo"),
127
        description=_(u"This shows a custom logo on your SENAITE site."),
128
        required=False,
129
    )
130
131
    site_logo_css = schema.ASCII(
132
        title=_(u"Site Logo CSS"),
133
        description=_(
134
            u"Add custom CSS rules for the Logo, "
135
            u"e.g. height:15px; width:150px;"
136
        ),
137
        required=False,
138
    )
139
140
    immediate_results_entry = schema.Bool(
141
        title=_(u"Immediate results entry"),
142
        description=_(
143
            "description_senaitesetup_immediateresultsentry",
144
            default=u"Allow the user to directly enter results after sample "
145
            "creation, e.g. to enter field results immediately, or lab "
146
            "results, when the automatic sample reception is activated."
147
        ),
148
    )
149
150
    categorize_sample_analyses = schema.Bool(
151
        title=_("title_senaitesetup_categorizesampleanalyses",
152
                default=u"Categorize sample analyses"),
153
        description=_(
154
            "description_senaitesetup_categorizesampleanalyses",
155
            default=u"Group analyses by category for samples"
156
        ),
157
        default=False,
158
    )
159
160
    sample_analyses_required = schema.Bool(
161
        title=_("title_senaitesetup_sampleanalysesrequired",
162
                default=u"Require sample analyses"),
163
        description=_(
164
            "description_senaitesetup_sampleanalysesrequired",
165
            default=u"Analyses are required for sample registration"
166
        ),
167
        default=True,
168
    )
169
170
    # Allow Manual Analysis Result Capture Date
171
    allow_manual_result_capture_date = schema.Bool(
172
        title=_("title_senaitesetup_allow_manual_result_capture_date",
173
                default=u"Allow to set the result capture date"),
174
        description=_(
175
            "description_senaitesetup_allow_manual_result_capture_date",
176
            default=u"If this option is activated, the result capture date "
177
                    u"can be entered manually for analyses"),
178
        default=False)
179
180
    max_number_of_samples_add = schema.Int(
181
        title=_(
182
            u"label_senaitesetup_maxnumberofsamplesadd",
183
            default=u"Maximum value for 'Number of samples' field on "
184
                    u"registration"
185
        ),
186
        description=_(
187
            u"description_senaitesetup_maxnumberofsamplesadd",
188
            default=u"Maximum number of samples that can be created in "
189
                    u"accordance with the value set for the field 'Number of "
190
                    u"samples' on the sample registration form"
191
        ),
192
        default=10
193
    )
194
195
    date_sampled_required = schema.Bool(
196
        title=_(
197
            u"title_senaitesetup_date_sampled_required",
198
            default=u"Date sampled required"),
199
        description=_(
200
            u"description_senaitesetup_date_sampled_required",
201
            default=u"Select this to make DateSampled field required on "
202
                    u"sample creation. This functionality only takes effect "
203
                    u"when 'Sampling workflow' is not active"
204
        ),
205
        default=True,
206
    )
207
208
    show_lab_name_in_login = schema.Bool(
209
        title=_(
210
            u"title_senaitesetup_show_lab_name_in_login",
211
            default=u"Display laboratory name in the login page"),
212
        description=_(
213
            u"description_senaitesetup_show_lab_name_in_login",
214
            default=u"When selected, the laboratory name will be displayed"
215
                    u"in the login page, above the access credentials."
216
        ),
217
        default=False,
218
    )
219
220
    invalidation_reason_required = schema.Bool(
221
        title=_(
222
            u"title_senaitesetup_invalidation_reason_required",
223
            default=u"Invalidation reason required"),
224
        description=_(
225
            u"description_senaitesetup_invalidation_reason_required",
226
            default=u"Specify whether providing a reason is mandatory when "
227
                    u"invalidating a sample. If enabled, the '$reason' "
228
                    u"placeholder in the sample invalidation notification "
229
                    u"email body will be replaced with the entered reason."
230
        ),
231
        default=True,
232
    )
233
234
    ###
235
    # Fieldsets
236
    ###
237
    model.fieldset(
238
        "samples",
239
        label=_("label_senaitesetup_fieldset_samples", default=u"Samples"),
240
        fields=[
241
            "max_number_of_samples_add",
242
            "date_sampled_required",
243
            "invalidation_reason_required",
244
        ]
245
    )
246
    model.fieldset(
247
        "analyses",
248
        label=_("label_senaitesetup_fieldset_analyses", default=u"Analyses"),
249
        fields=[
250
            "immediate_results_entry",
251
            "categorize_sample_analyses",
252
            "sample_analyses_required",
253
            "allow_manual_result_capture_date",
254
        ]
255
    )
256
257
    model.fieldset(
258
        "notifications",
259
        label=_(u"Notifications"),
260
        fields=[
261
            "email_from_sample_publication",
262
            "email_body_sample_publication",
263
            "always_cc_responsibles_in_report_emails",
264
        ]
265
    )
266
267
    model.fieldset(
268
        "appearance",
269
        label=_(u"Appearance"),
270
        fields=[
271
            "site_logo",
272
            "site_logo_css",
273
            "show_lab_name_in_login",
274
        ]
275
    )
276
277
278
@implementer(ISetup, ISetupSchema, IHideActionsMenu)
279
class Setup(Container):
280
    """SENAITE Setup Folder
281
    """
282
    security = ClassSecurityInfo()
283
284
    @security.protected(permissions.View)
285
    def getEmailFromSamplePublication(self):
286
        """Returns the 'From' address for publication emails
287
        """
288
        accessor = self.accessor("email_from_sample_publication")
289
        email = accessor(self)
290
        if not email:
291
            email = default_email_from_sample_publication(self)
292
        return email
293
294
    @security.protected(permissions.ModifyPortalContent)
295
    def setEmailFromSamplePublication(self, value):
296
        """Set the 'From' address for publication emails
297
        """
298
        mutator = self.mutator("email_from_sample_publication")
299
        return mutator(self, value)
300
301
    @security.protected(permissions.View)
302
    def getEmailBodySamplePublication(self):
303
        """Returns the transformed email body text for publication emails
304
        """
305
        accessor = self.accessor("email_body_sample_publication")
306
        value = accessor(self)
307
        if IRichTextValue.providedBy(value):
308
            # Transforms the raw value to the output mimetype
309
            value = value.output_relative_to(self)
310
        if not value:
311
            # Always fallback to default value
312
            value = default_email_body_sample_publication(self)
313
        return value
314
315
    @security.protected(permissions.ModifyPortalContent)
316
    def setEmailBodySamplePublication(self, value):
317
        """Set email body text for publication emails
318
        """
319
        mutator = self.mutator("email_body_sample_publication")
320
        return mutator(self, value)
321
322
    @security.protected(permissions.View)
323
    def getAlwaysCCResponsiblesInReportEmail(self):
324
        """Returns if responsibles should always receive publication emails
325
        """
326
        accessor = self.accessor("always_cc_responsibles_in_report_emails")
327
        return accessor(self)
328
329
    @security.protected(permissions.View)
330
    def setAlwaysCCResponsiblesInReportEmail(self, value):
331
        """Set if responsibles should always receive publication emails
332
        """
333
        mutator = self.mutator("always_cc_responsibles_in_report_emails")
334
        return mutator(self, value)
335
336
    @security.protected(permissions.View)
337
    def getEnableGlobalAuditlog(self):
338
        """Returns if the global Auditlog is enabled
339
        """
340
        accessor = self.accessor("enable_global_auditlog")
341
        return accessor(self)
342
343
    @security.protected(permissions.ModifyPortalContent)
344
    def setEnableGlobalAuditlog(self, value):
345
        """Enable/Disable global Auditlogging
346
        """
347
        if value is False:
348
            # clear the auditlog catalog
349
            catalog = api.get_tool(AUDITLOG_CATALOG)
350
            catalog.manage_catalogClear()
351
        mutator = self.mutator("enable_global_auditlog")
352
        return mutator(self, value)
353
354
    @security.protected(permissions.View)
355
    def getSiteLogo(self):
356
        """Returns the global site logo
357
        """
358
        accessor = self.accessor("site_logo")
359
        return accessor(self)
360
361
    @security.protected(permissions.ModifyPortalContent)
362
    def setSiteLogo(self, value):
363
        """Set the site logo
364
        """
365
        mutator = self.mutator("site_logo")
366
        return mutator(self, value)
367
368
    @security.protected(permissions.View)
369
    def getSiteLogoCSS(self):
370
        """Returns the global site logo
371
        """
372
        accessor = self.accessor("site_logo_css")
373
        return accessor(self)
374
375
    @security.protected(permissions.ModifyPortalContent)
376
    def setSiteLogoCSS(self, value):
377
        """Set the site logo
378
        """
379
        mutator = self.mutator("site_logo_css")
380
        return mutator(self, value)
381
382
    @security.protected(permissions.View)
383
    def getImmediateResultsEntry(self):
384
        """Returns if immediate results entry is enabled or not
385
        """
386
        accessor = self.accessor("immediate_results_entry")
387
        return accessor(self)
388
389
    @security.protected(permissions.ModifyPortalContent)
390
    def setImmediateResultsEntry(self, value):
391
        """Enable/Disable global Auditlogging
392
        """
393
        mutator = self.mutator("immediate_results_entry")
394
        return mutator(self, value)
395
396
    @security.protected(permissions.View)
397
    def getCategorizeSampleAnalyses(self):
398
        """Returns if analyses should be grouped by category for samples
399
        """
400
        accessor = self.accessor("categorize_sample_analyses")
401
        return accessor(self)
402
403
    @security.protected(permissions.ModifyPortalContent)
404
    def setCategorizeSampleAnalyses(self, value):
405
        """Enable/Disable grouping of analyses by category for samples
406
        """
407
        mutator = self.mutator("categorize_sample_analyses")
408
        return mutator(self, value)
409
410
    @security.protected(permissions.View)
411
    def getSampleAnalysesRequired(self):
412
        """Returns if analyses are required in sample add form
413
        """
414
        accessor = self.accessor("sample_analyses_required")
415
        return accessor(self)
416
417
    @security.protected(permissions.ModifyPortalContent)
418
    def setSampleAnalysesRequired(self, value):
419
        """Allow/Disallow to create samples without analyses
420
        """
421
        mutator = self.mutator("sample_analyses_required")
422
        return mutator(self, value)
423
424
    @security.protected(permissions.View)
425
    def getAllowManualResultCaptureDate(self):
426
        """Returns if analyses are required in sample add form
427
        """
428
        accessor = self.accessor("allow_manual_result_capture_date")
429
        return accessor(self)
430
431
    @security.protected(permissions.ModifyPortalContent)
432
    def setAllowManualResultCaptureDate(self, value):
433
        """Allow/Disallow to create samples without analyses
434
        """
435
        mutator = self.mutator("allow_manual_result_capture_date")
436
        return mutator(self, value)
437
438
    @security.protected(permissions.View)
439
    def getMaxNumberOfSamplesAdd(self):
440
        """Returns the maximum number of samples that can be created for each
441
        column in sample add form in accordance with the value set for the
442
        field 'Number of samples'
443
        """
444
        accessor = self.accessor("max_number_of_samples_add")
445
        return api.to_int(accessor(self))
446
447
    @security.protected(permissions.ModifyPortalContent)
448
    def setMaxNumberOfSamplesAdd(self, value):
449
        """Sets the maximum number of samples that can be created for each
450
        column in sample add form in accordance with the value set for the
451
        field 'Number of samples'
452
        """
453
        mutator = self.mutator("max_number_of_samples_add")
454
        return mutator(self, value)
455
456
    @security.protected(permissions.View)
457
    def getDateSampledRequired(self):
458
        """Returns whether the DateSampled field is required on sample creation
459
        when the sampling workflow is not active
460
        """
461
        accessor = self.accessor("date_sampled_required")
462
        return accessor(self)
463
464
    @security.protected(permissions.ModifyPortalContent)
465
    def setDateSampledRequired(self, value):
466
        """Sets whether the entry of a value for DateSampled field on sample
467
        creation is required when the sampling workflow is not active
468
        """
469
        mutator = self.mutator("date_sampled_required")
470
        return mutator(self, value)
471
472
    @security.protected(permissions.View)
473
    def getShowLabNameInLogin(self):
474
        """Returns if the laboratory name has to be displayed in login page
475
        """
476
        accessor = self.accessor("show_lab_name_in_login")
477
        return accessor(self)
478
479
    @security.protected(permissions.ModifyPortalContent)
480
    def setShowLabNameInLogin(self, value):
481
        """Show/hide the laboratory name in the login page
482
        """
483
        mutator = self.mutator("show_lab_name_in_login")
484
        return mutator(self, value)
485
486
    @security.protected(permissions.View)
487
    def getInvalidationReasonRequired(self):
488
        """Returns whether the introduction of a reason is required when
489
        invalidating a sample
490
        """
491
        accessor = self.accessor("invalidation_reason_required")
492
        return accessor(self)
493
494
    @security.protected(permissions.ModifyPortalContent)
495
    def setInvalidationReasonRequired(self, value):
496
        """Set whether the introduction of a reason is required when
497
        invalidating a sample
498
        """
499
        mutator = self.mutator("invalidation_reason_required")
500
        return mutator(self, value)
501