DashboardView.check_dashboard_cookie()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nop 1
dl 0
loc 24
rs 9.85
c 0
b 0
f 0
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
import collections
22
import datetime
23
import json
24
from calendar import monthrange
25
from operator import itemgetter
26
from time import time
27
28
from bika.lims import bikaMessageFactory as _
29
from bika.lims import logger
30
from bika.lims.api import get_current_client
31
from bika.lims.api import get_tool
32
from bika.lims.api import get_url
33
from bika.lims.api import search
34
from bika.lims.browser import BrowserView
35
from bika.lims.utils import get_strings
36
from bika.lims.utils import get_unicode
37
from DateTime import DateTime
38
from plone import api
39
from plone import protect
40
from plone.api.exc import InvalidParameterError
41
from plone.memoize import ram
42
from plone.memoize import view as viewcache
43
from Products.Archetypes.public import DisplayList
44
from Products.CMFCore.utils import getToolByName
45
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
46
from senaite.core.catalog import ANALYSIS_CATALOG
47
from senaite.core.catalog import SAMPLE_CATALOG
48
from senaite.core.catalog import WORKSHEET_CATALOG
49
50
DASHBOARD_FILTER_COOKIE = 'dashboard_filter_cookie'
51
52
# Supported periodicities for evolution charts
53
PERIODICITY_DAILY = "d"
54
PERIODICITY_WEEKLY = "w"
55
PERIODICITY_MONTHLY = "m"
56
PERIODICITY_QUARTERLY = "q"
57
PERIODICITY_BIANNUAL = "b"
58
PERIODICITY_YEARLY = "y"
59
PERIODICITY_ALL = "a"
60
61
62
def get_dashboard_registry_record():
63
    """
64
    Return the 'senaite.core.dashboard_panels_visibility' values.
65
    :return: A dictionary or None
66
    """
67
    try:
68
        registry = api.portal.get_registry_record(
69
            'senaite.core.dashboard_panels_visibility')
70
        return registry
71
    except InvalidParameterError:
72
        # No entry in the registry for dashboard panels roles.
73
        # Maybe upgradestep 1.1.8 was not run?
74
        logger.warn("Cannot find a record with name "
75
                    "'senaite.core.dashboard_panels_visibility' in "
76
                    "registry_record. Missed upgrade 2.0.0?")
77
    return dict()
78
79
80
def set_dashboard_registry_record(registry_info):
81
    """
82
    Sets the 'senaite.core.dashboard_panels_visibility' values.
83
84
    :param registry_info: A dictionary type object with all its values as
85
    *unicode* objects.
86
    :return: A dictionary or None
87
    """
88
    try:
89
        api.portal.set_registry_record(
90
            'senaite.core.dashboard_panels_visibility', registry_info)
91
    except InvalidParameterError:
92
        # No entry in the registry for dashboard panels roles.
93
        # Maybe upgradestep 1.1.8 was not run?
94
        logger.warn("Cannot find a record with name "
95
                    "'senaite.core.dashboard_panels_visibility' in "
96
                    "registry_record. Missed upgrade 2.0.0?")
97
98
99
def setup_dashboard_panels_visibility_registry(section_name):
100
    """
101
    Initializes the values for panels visibility in registry_records. By
102
    default, only users with LabManager or Manager roles can see the panels.
103
    :param section_name:
104
    :return: An string like: "role1,yes,role2,no,rol3,no"
105
    """
106
    registry_info = get_dashboard_registry_record()
107
    role_permissions_list = []
108
    # Getting roles defined in the system
109
    roles = []
110
    acl_users = get_tool("acl_users")
111
    roles_tree = acl_users.portal_role_manager.listRoleIds()
112
    for role in roles_tree:
113
        roles.append(role)
114
    # Set view permissions to each role as 'yes':
115
    # "role1,yes,role2,no,rol3,no"
116
    for role in roles:
117
        role_permissions_list.append(role)
118
        visible = 'no'
119
        if role in ['LabManager', 'Manager']:
120
            visible = 'yes'
121
        role_permissions_list.append(visible)
122
    role_permissions = ','.join(role_permissions_list)
123
124
    # Set permissions string into dict
125
    registry_info[get_unicode(section_name)] = get_unicode(role_permissions)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable get_unicode does not seem to be defined.
Loading history...
126
    # Set new values to registry record
127
    set_dashboard_registry_record(registry_info)
128
    return registry_info
129
130
131
def get_dashboard_panels_visibility_by_section(section_name):
132
    """
133
    Return a list of pairs as values that represents the role-permission
134
    view relation for the panel section passed in.
135
    :param section_name: the panels section id.
136
    :return: a list of tuples.
137
    """
138
    registry_info = get_dashboard_registry_record()
139
    if section_name not in registry_info:
140
        # Registry hasn't been set, do it at least for this section
141
        registry_info = \
142
            setup_dashboard_panels_visibility_registry(section_name)
143
144
    pairs = registry_info.get(section_name)
145
    pairs = get_strings(pairs)
146
    if pairs is None:
147
        # In the registry, but with None value?
148
        setup_dashboard_panels_visibility_registry(section_name)
149
        return get_dashboard_panels_visibility_by_section(section_name)
150
151
    pairs = pairs.split(',')
152
    if len(pairs) == 0 or len(pairs) % 2 != 0:
153
        # Non-valid or malformed value
154
        setup_dashboard_panels_visibility_registry(section_name)
155
        return get_dashboard_panels_visibility_by_section(section_name)
156
157
    result = [
158
        (pairs[i], pairs[i + 1]) for i in range(len(pairs)) if i % 2 == 0]
159
    return result
160
161
162
def is_panel_visible_for_user(panel, user):
163
    """
164
    Checks if the user is allowed to see the panel
165
    :param panel: panel ID as string
166
    :param user: a MemberData object
167
    :return: Boolean
168
    """
169
    roles = user.getRoles()
170
    visibility = get_dashboard_panels_visibility_by_section(panel)
171
    for pair in visibility:
172
        if pair[0] in roles and pair[1] == 'yes':
173
            return True
174
    return False
175
176
177
class DashboardView(BrowserView):
178
    template = ViewPageTemplateFile("templates/dashboard.pt")
179
180
    def __init__(self, context, request):
181
        BrowserView.__init__(self, context, request)
182
        self.dashboard_cookie = None
183
        self.member = None
184
185
    def __call__(self):
186
        # If a client contact, redirect to the client's page
187
        client = get_current_client()
188
        if client:
189
            url = get_url(client)
190
            return self.request.response.redirect(url)
191
192
        frontpage_url = self.portal_url + "/senaite-frontpage"
193
        if not self.context.bika_setup.getDashboardByDefault():
194
            # Do not render dashboard, render frontpage instead
195
            self.request.response.redirect(frontpage_url)
196
            return
197
198
        mtool = getToolByName(self.context, 'portal_membership')
199
        if mtool.isAnonymousUser():
200
            # Anonymous user, redirect to frontpage
201
            self.request.response.redirect(frontpage_url)
202
            return
203
204
        self.member = mtool.getAuthenticatedMember()
205
        self.periodicity = self.request.get('p', PERIODICITY_WEEKLY)
206
        self.dashboard_cookie = self.check_dashboard_cookie()
207
        date_range = self.get_date_range(self.periodicity)
208
        self.date_from = date_range[0]
209
        self.date_to = date_range[1]
210
211
        return self.template()
212
213
    def check_dashboard_cookie(self):
214
        """
215
        Check if the dashboard cookie should exist through bikasetup
216
        configuration.
217
218
        If it should exist but doesn't exist yet, the function creates it
219
        with all values as default.
220
        If it should exist and already exists, it returns the value.
221
        Otherwise, the function returns None.
222
223
        :return: a dictionary of strings
224
        """
225
        # Getting cookie
226
        cookie_raw = self.request.get(DASHBOARD_FILTER_COOKIE, None)
227
        # If it doesn't exist, create it with default values
228
        if cookie_raw is None:
229
            cookie_raw = self._create_raw_data()
230
            self.request.response.setCookie(
231
                DASHBOARD_FILTER_COOKIE,
232
                json.dumps(cookie_raw),
233
                quoted=False,
234
                path='/')
235
            return cookie_raw
236
        return get_strings(json.loads(cookie_raw))
237
238
    def is_filter_selected(self, selection_id, value):
239
        """
240
        Compares whether the 'selection_id' parameter value saved in the
241
        cookie is the same value as the "value" parameter.
242
243
        :param selection_id: a string as a dashboard_cookie key.
244
        :param value: The value to compare against the value from
245
        dashboard_cookie key.
246
        :return: Boolean.
247
        """
248
        selected = self.dashboard_cookie.get(selection_id)
249
        return selected == value
250
251
    def is_admin_user(self):
252
        """
253
        Checks if the user is the admin or a SiteAdmin user.
254
        :return: Boolean
255
        """
256
        user = api.user.get_current()
257
        roles = user.getRoles()
258
        return "LabManager" in roles or "Manager" in roles
259
260
    def _create_raw_data(self):
261
        """
262
        Gathers the different sections ids and creates a string as first
263
        cookie data.
264
265
        :return: A dictionary like:
266
            {'analyses':'all','analysisrequest':'all','worksheets':'all'}
267
        """
268
        result = {}
269
        for section in self.get_sections():
270
            result[section.get('id')] = 'all'
271
        return result
272
273
    def get_date_range(self, periodicity=PERIODICITY_WEEKLY):
274
        """Returns a date range (date from, date to) that suits with the passed
275
        in periodicity.
276
277
        :param periodicity: string that represents the periodicity
278
        :type periodicity: str
279
        :return: A date range
280
        :rtype: [(DateTime, DateTime)]
281
        """
282
        today = datetime.date.today()
283
        if periodicity == PERIODICITY_DAILY:
284
            # Daily, load last 30 days
285
            date_from = DateTime() - 30
286
            date_to = DateTime() + 1
287
            return date_from, date_to
288
289
        if periodicity == PERIODICITY_MONTHLY:
290
            # Monthly, load last 2 years
291
            min_year = today.year - 1 if today.month == 12 else today.year - 2
292
            min_month = 1 if today.month == 12 else today.month
293
            date_from = DateTime(min_year, min_month, 1)
294
            date_to = DateTime(today.year, today.month,
295
                               monthrange(today.year, today.month)[1],
296
                               23, 59, 59)
297
            return date_from, date_to
298
299
        if periodicity == PERIODICITY_QUARTERLY:
300
            # Quarterly, load last 4 years
301
            m = (((today.month - 1) / 3) * 3) + 1
302
            min_year = today.year - 4 if today.month == 12 else today.year - 5
303
            date_from = DateTime(min_year, m, 1)
304
            date_to = DateTime(today.year, m + 2,
305
                               monthrange(today.year, m + 2)[1], 23, 59,
306
                               59)
307
            return date_from, date_to
308
        if periodicity == PERIODICITY_BIANNUAL:
309
            # Biannual, load last 10 years
310
            m = (((today.month - 1) / 6) * 6) + 1
311
            min_year = today.year - 10 if today.month == 12 else today.year - 11
312
            date_from = DateTime(min_year, m, 1)
313
            date_to = DateTime(today.year, m + 5,
314
                               monthrange(today.year, m + 5)[1], 23, 59,
315
                               59)
316
            return date_from, date_to
317
318
        if periodicity in [PERIODICITY_YEARLY, PERIODICITY_ALL]:
319
            # Yearly or All time, load last 15 years
320
            min_year = today.year - 15 if today.month == 12 else today.year - 16
321
            date_from = DateTime(min_year, 1, 1)
322
            date_to = DateTime(today.year, 12, 31, 23, 59, 59)
323
            return date_from, date_to
324
325
        # Default Weekly, load last six months
326
        year, weeknum, dow = today.isocalendar()
327
        min_year = today.year if today.month > 6 else today.year - 1
328
        min_month = today.month - 6 if today.month > 6 \
329
            else (today.month - 6) + 12
330
        date_from = DateTime(min_year, min_month, 1)
331
        date_to = DateTime() - dow + 7
332
        return date_from, date_to
333
334
    def get_sections(self):
335
        """ Returns an array with the sections to be displayed.
336
            Every section is a dictionary with the following structure:
337
                {'id': <section_identifier>,
338
                 'title': <section_title>,
339
                'panels': <array of panels>}
340
        """
341
        sections = []
342
        user = api.user.get_current()
343
        if is_panel_visible_for_user('analyses', user):
344
            sections.append(self.get_analyses_section())
345
        if is_panel_visible_for_user('analysisrequests', user):
346
            sections.append(self.get_analysisrequests_section())
347
        if is_panel_visible_for_user('worksheets', user):
348
            sections.append(self.get_worksheets_section())
349
        return sections
350
351
    def get_filter_options(self):
352
        """
353
        Returns dasboard filter options.
354
        :return: Boolean
355
        """
356
        dash_opt = DisplayList((
357
            ('all', _('All')),
358
            ('mine', _('Mine')),
359
        ))
360
        return dash_opt
361
362
    def _getStatistics(self, name, description, url, catalog, criterias, total):
363
        out = {'type':        'simple-panel',
364
               'name':        name,
365
               'class':       'informative',
366
               'description': description,
367
               'total':       total,
368
               'link':        self.portal_url + '/' + url}
369
370
        results = 0
371
        ratio = 0
372
        if total > 0:
373
            results = self.search_count(criterias, catalog.id)
374
            results = results if total >= results else total
375
            ratio = (float(results)/float(total))*100 if results > 0 else 0
376
        ratio = str("%%.%sf" % 1) % ratio
377
        out['legend'] = _('of') + " " + str(total) + ' (' + ratio + '%)'
378
        out['number'] = results
379
        out['percentage'] = float(ratio)
380
        return out
381
382
    def get_analysisrequests_section(self):
383
        """ Returns the section dictionary related with Analysis
384
            Requests, that contains some informative panels (like
385
            ARs to be verified, ARs to be published, etc.)
386
        """
387
        out = []
388
        catalog = getToolByName(self.context, SAMPLE_CATALOG)
389
        query = {'portal_type': "AnalysisRequest",
390
                 'is_active': True}
391
392
        # Check if dashboard_cookie contains any values to query
393
        # elements by
394
        query = self._update_criteria_with_filters(query, 'analysisrequests')
395
396
        # Active Samples (All)
397
        total = self.search_count(query, catalog.id)
398
399
        # Sampling workflow enabled?
400
        if self.context.bika_setup.getSamplingWorkflowEnabled():
401
            # Samples awaiting to be sampled or scheduled
402
            name = _('Samples to be sampled')
403
            desc = _("To be sampled")
404
            purl = 'samples?samples_review_state=to_be_sampled'
405
            query['review_state'] = ['to_be_sampled', ]
406
            out.append(self._getStatistics(name, desc, purl, catalog, query, total))
407
408
            # Samples awaiting to be preserved
409
            name = _('Samples to be preserved')
410
            desc = _("To be preserved")
411
            purl = 'samples?samples_review_state=to_be_preserved'
412
            query['review_state'] = ['to_be_preserved', ]
413
            out.append(self._getStatistics(name, desc, purl, catalog, query, total))
414
415
            # Samples scheduled for Sampling
416
            name = _('Samples scheduled for sampling')
417
            desc = _("Sampling scheduled")
418
            purl = 'samples?samples_review_state=scheduled_sampling'
419
            query['review_state'] = ['scheduled_sampling', ]
420
            out.append(self._getStatistics(name, desc, purl, catalog, query, total))
421
422
        # Samples awaiting for reception
423
        name = _('Samples to be received')
424
        desc = _("Reception pending")
425
        purl = 'samples?samples_review_state=sample_due'
426
        query['review_state'] = ['sample_due', ]
427
        out.append(self._getStatistics(name, desc, purl, catalog, query, total))
428
429
        # Samples under way
430
        name = _('Samples with results pending')
431
        desc = _("Results pending")
432
        purl = 'samples?samples_review_state=sample_received'
433
        query['review_state'] = ['sample_received', ]
434
        out.append(self._getStatistics(name, desc, purl, catalog, query, total))
435
436
        # Samples to be verified
437
        name = _('Samples to be verified')
438
        desc = _("To be verified")
439
        purl = 'samples?samples_review_state=to_be_verified'
440
        query['review_state'] = ['to_be_verified', ]
441
        out.append(self._getStatistics(name, desc, purl, catalog, query, total))
442
443
        # Samples verified (to be published)
444
        name = _('Samples verified')
445
        desc = _("Verified")
446
        purl = 'samples?samples_review_state=verified'
447
        query['review_state'] = ['verified', ]
448
        out.append(self._getStatistics(name, desc, purl, catalog, query, total))
449
450
        # Samples published
451
        name = _('Samples published')
452
        desc = _("Published")
453
        purl = 'samples?samples_review_state=published'
454
        query['review_state'] = ['published', ]
455
        out.append(self._getStatistics(name, desc, purl, catalog, query, total))
456
457
        # Samples to be printed
458
        if self.context.bika_setup.getPrintingWorkflowEnabled():
459
            name = _('Samples to be printed')
460
            desc = _("To be printed")
461
            purl = 'samples?samples_getPrinted=0'
462
            query['getPrinted'] = '0'
463
            query['review_state'] = ['published', ]
464
            out.append(
465
                self._getStatistics(name, desc, purl, catalog, query, total))
466
467
        # Chart with the evolution of ARs over a period, grouped by
468
        # periodicity
469
        outevo = self.fill_dates_evo(catalog, query)
470
        out.append({'type':         'bar-chart-panel',
471
                    'name':         _('Evolution of Samples'),
472
                    'class':        'informative',
473
                    'description':  _('Evolution of Samples'),
474
                    'data':         json.dumps(outevo),
475
                    'datacolors':   json.dumps(self.get_colors_palette())})
476
477
        return {'id': 'analysisrequests',
478
                'title': _('Samples'),
479
                'panels': out}
480
481
    def get_worksheets_section(self):
482
        """ Returns the section dictionary related with Worksheets,
483
            that contains some informative panels (like
484
            WS to be verified, WS with results pending, etc.)
485
        """
486
        out = []
487
        bc = getToolByName(self.context, WORKSHEET_CATALOG)
488
        query = {'portal_type': "Worksheet", }
489
490
        # Check if dashboard_cookie contains any values to query
491
        # elements by
492
        query = self._update_criteria_with_filters(query, 'worksheets')
493
494
        # Active Worksheets (all)
495
        total = self.search_count(query, bc.id)
496
497
        # Open worksheets
498
        name = _('Results pending')
499
        desc = _('Results pending')
500
        purl = 'worksheets?list_review_state=open'
501
        query['review_state'] = ['open']
502
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
503
504
        # Worksheets to be verified
505
        name = _('To be verified')
506
        desc = _('To be verified')
507
        purl = 'worksheets?list_review_state=to_be_verified'
508
        query['review_state'] = ['to_be_verified', ]
509
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
510
511
        # Worksheets verified
512
        name = _('Verified')
513
        desc = _('Verified')
514
        purl = 'worksheets?list_review_state=verified'
515
        query['review_state'] = ['verified', ]
516
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
517
518
        # Chart with the evolution of WSs over a period, grouped by
519
        # periodicity
520
        outevo = self.fill_dates_evo(bc, query)
521
        out.append({'type':         'bar-chart-panel',
522
                    'name':         _('Evolution of Worksheets'),
523
                    'class':        'informative',
524
                    'description':  _('Evolution of Worksheets'),
525
                    'data':         json.dumps(outevo),
526
                    'datacolors':   json.dumps(self.get_colors_palette())})
527
528
        return {'id': 'worksheets',
529
                'title': _('Worksheets'),
530
                'panels': out}
531
532
    def get_analyses_section(self):
533
        """ Returns the section dictionary related with Analyses,
534
            that contains some informative panels (analyses pending
535
            analyses assigned, etc.)
536
        """
537
        out = []
538
        bc = getToolByName(self.context, ANALYSIS_CATALOG)
539
        query = {'portal_type': "Analysis", 'is_active': True}
540
541
        # Check if dashboard_cookie contains any values to query elements by
542
        query = self._update_criteria_with_filters(query, 'analyses')
543
544
        # Active Analyses (All)
545
        total = self.search_count(query, bc.id)
546
547
        # Analyses to be assigned
548
        name = _('Assignment pending')
549
        desc = _('Assignment pending')
550
        purl = '#'
551
        query['review_state'] = ['unassigned']
552
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
553
554
        # Analyses pending
555
        name = _('Results pending')
556
        desc = _('Results pending')
557
        purl = '#'
558
        query['review_state'] = ['unassigned', 'assigned', ]
559
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
560
561
        # Analyses to be verified
562
        name = _('To be verified')
563
        desc = _('To be verified')
564
        purl = '#'
565
        query['review_state'] = ['to_be_verified', ]
566
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
567
568
        # Analyses verified
569
        name = _('Verified')
570
        desc = _('Verified')
571
        purl = '#'
572
        query['review_state'] = ['verified', ]
573
        out.append(self._getStatistics(name, desc, purl, bc, query, total))
574
575
        # Chart with the evolution of Analyses over a period, grouped by
576
        # periodicity
577
        outevo = self.fill_dates_evo(bc, query)
578
        out.append({'type':         'bar-chart-panel',
579
                    'name':         _('Evolution of Analyses'),
580
                    'class':        'informative',
581
                    'description':  _('Evolution of Analyses'),
582
                    'data':         json.dumps(outevo),
583
                    'datacolors':   json.dumps(self.get_colors_palette())})
584
        return {'id': 'analyses',
585
                'title': _('Analyses'),
586
                'panels': out}
587
588
    def get_states_map(self, portal_type):
589
        if portal_type == 'Analysis':
590
            return {'unassigned':      _('Assignment pending'),
591
                    'assigned':        _('Results pending'),
592
                    'to_be_verified':  _('To be verified'),
593
                    'rejected':        _('Rejected'),
594
                    'retracted':       _('Retracted'),
595
                    'verified':        _('Verified'),
596
                    'published':       _('Published')}
597
        elif portal_type == 'AnalysisRequest':
598
            return {'to_be_sampled':       _('To be sampled'),
599
                    'to_be_preserved':     _('To be preserved'),
600
                    'scheduled_sampling':  _('Sampling scheduled'),
601
                    'sample_due':          _('Reception pending'),
602
                    'rejected':            _('Rejected'),
603
                    'invalid':             _('Invalid'),
604
                    'sample_received':     _('Results pending'),
605
                    'assigned':            _('Results pending'),
606
                    'to_be_verified':      _('To be verified'),
607
                    'verified':            _('Verified'),
608
                    'published':           _('Published')}
609
        elif portal_type == 'Worksheet':
610
            return {'open':            _('Results pending'),
611
                    'to_be_verified':  _('To be verified'),
612
                    'verified':        _('Verified')}
613
614
    def get_colors_palette(self):
615
        return {
616
            'to_be_sampled':                '#917A4C',
617
            _('To be sampled'):             '#917A4C',
618
619
            'to_be_preserved':              '#C2803E',
620
            _('To be preserved'):           '#C2803E',
621
622
            'scheduled_sampling':           '#F38630',
623
            _('Sampling scheduled'):        '#F38630',
624
625
            'sample_due':                   '#ffff8d',
626
            _('Reception pending'):         '#ffff8d',
627
628
            'sample_received':              '#a1887f',
629
            _('Assignment pending'):        '#a1887f',
630
            _('Sample received'):           '#a1887f',
631
632
            'assigned':                     '#ddd',
633
            'open':                         '#ddd',
634
            _('Results pending'):           '#ddd',
635
636
            'rejected':                     '#abc',
637
            'retracted':                    '#abc',
638
            _('Rejected'):                  '#abc',
639
            _('Retracted'):                 '#abc',
640
641
            'invalid':                      '#e65100',
642
            _('Invalid'):                   '#e65100',
643
644
            'to_be_verified':               '#18ffff',
645
            _('To be verified'):            '#18ffff',
646
647
            'verified':                     '#0091ea',
648
            _('Verified'):                  '#0091ea',
649
650
            'published':                    '#00c853',
651
            _('Published'):                 '#00c853',
652
        }
653
654
    def _getDateStr(self, period, created):
655
        if period == PERIODICITY_YEARLY:
656
            created = created.year()
657
        elif period == PERIODICITY_BIANNUAL:
658
            m = (((created.month()-1)/6)*6)+1
659
            created = '%s-%s' % (str(created.year())[2:], str(m).zfill(2))
660
        elif period == PERIODICITY_QUARTERLY:
661
            m = (((created.month()-1)/3)*3)+1
662
            created = '%s-%s' % (str(created.year())[2:], str(m).zfill(2))
663
        elif period == PERIODICITY_MONTHLY:
664
            created = '%s-%s' % (str(created.year())[2:], str(created.month()).zfill(2))
665
        elif period == PERIODICITY_WEEKLY:
666
            year, weeknum, dow = created.asdatetime().isocalendar()
667
            created = created - dow
668
            created = '%s-%s-%s' % (str(created.year())[2:], str(created.month()).zfill(2), str(created.day()).zfill(2))
669
        elif period == PERIODICITY_ALL:
670
            # All time, but evolution chart grouped by year
671
            created = created.year()
672
        else:
673
            created = '%s-%s-%s' % (str(created.year())[2:], str(created.month()).zfill(2), str(created.day()).zfill(2))
674
        return created
675
676
    def fill_dates_evo(self, catalog, query):
677
        sorted_query = collections.OrderedDict(sorted(query.items()))
678
        query_json = json.dumps(sorted_query)
679
        return self._fill_dates_evo(query_json, catalog.id, self.periodicity)
680
681
    def _fill_dates_evo_cachekey(method, self, query_json, catalog_name,
682
                                 periodicity):
683
        hour = time() // (60 * 60 * 2)
684
        return hour, catalog_name, query_json, periodicity
685
686
    @ram.cache(_fill_dates_evo_cachekey)
687
    def _fill_dates_evo(self, query_json, catalog_name, periodicity):
688
        """Returns an array of dictionaries, where each dictionary contains the
689
        amount of items created at a given date and grouped by review_state,
690
        based on the passed in periodicity.
691
692
        This is an expensive function that will not be called more than once
693
        every 2 hours (note cache decorator with `time() // (60 * 60 * 2)
694
        """
695
        outevoidx = {}
696
        outevo = []
697
        days = 1
698
        if periodicity == PERIODICITY_YEARLY:
699
            days = 336
700
        elif periodicity == PERIODICITY_BIANNUAL:
701
            days = 168
702
        elif periodicity == PERIODICITY_QUARTERLY:
703
            days = 84
704
        elif periodicity == PERIODICITY_MONTHLY:
705
            days = 28
706
        elif periodicity == PERIODICITY_WEEKLY:
707
            days = 7
708
        elif periodicity == PERIODICITY_ALL:
709
            days = 336
710
711
        # Get the date range
712
        date_from, date_to = self.get_date_range(periodicity)
713
        query = json.loads(query_json)
714
        if 'review_state' in query:
715
            del query['review_state']
716
        query['sort_on'] = 'created'
717
        query['created'] = {'query': (date_from, date_to),
718
                            'range': 'min:max'}
719
720
        otherstate = _('Other status')
721
        statesmap = self.get_states_map(query['portal_type'])
722
        stats = statesmap.values()
723
        stats.sort()
724
        stats.append(otherstate)
725
        statscount = {s: 0 for s in stats}
726
        # Add first all periods, cause we want all segments to be displayed
727
        curr = date_from.asdatetime()
728
        end = date_to.asdatetime()
729
        while curr < end:
730
            currstr = self._getDateStr(periodicity, DateTime(curr))
731
            if currstr not in outevoidx:
732
                outdict = {'date': currstr}
733
                for k in stats:
734
                    outdict[k] = 0
735
                outevo.append(outdict)
736
                outevoidx[currstr] = len(outevo)-1
737
            curr = curr + datetime.timedelta(days=days)
738
739
        brains = search(query, catalog_name)
740
        for brain in brains:
741
            created = brain.created
742
            state = brain.review_state
743
            if state not in statesmap:
744
                logger.warn("'%s' State for '%s' not available" % (state, query['portal_type']))
745
            state = statesmap[state] if state in statesmap else otherstate
746
            created = self._getDateStr(periodicity, created)
747
            statscount[state] += 1
748
            if created in outevoidx:
749
                oidx = outevoidx[created]
750
                if state in outevo[oidx]:
751
                    outevo[oidx][state] += 1
752
                else:
753
                    outevo[oidx][state] = 1
754
            else:
755
                # Create new row
756
                currow = {'date': created,
757
                          state: 1}
758
                outevo.append(currow)
759
760
        # Remove all those states for which there is no data
761
        rstates = [k for k, v in statscount.items() if v == 0]
762
        for o in outevo:
763
            for r in rstates:
764
                if r in o:
765
                    del o[r]
766
767
        # Sort available status by number of occurences descending
768
        sorted_states = sorted(statscount.items(), key=itemgetter(1))
769
        sorted_states = map(lambda item: item[0], sorted_states)
770
        sorted_states.reverse()
771
        return {'data': outevo, 'states': sorted_states}
772
773
    def search_count(self, query, catalog_name):
774
        sorted_query = collections.OrderedDict(sorted(query.items()))
775
        query_json = json.dumps(sorted_query)
776
        return self._search_count(query_json, catalog_name)
777
778
    @viewcache.memoize
779
    def _search_count(self, query_json, catalog_name):
780
        query = json.loads(query_json)
781
        brains = search(query, catalog_name)
782
        return len(brains)
783
784
    def _update_criteria_with_filters(self, query, section_name):
785
        """
786
        This method updates the 'query' dictionary with the criteria stored in
787
        dashboard cookie.
788
789
        :param query: A dictionary with search criteria.
790
        :param section_name: The dashboard section name
791
        :return: The 'query' dictionary
792
        """
793
        if self.dashboard_cookie is None:
794
            return query
795
        cookie_criteria = self.dashboard_cookie.get(section_name)
796
        if cookie_criteria == 'mine':
797
            query['Creator'] = self.member.getId()
798
        return query
799
800
    def get_dashboard_panels_visibility(self, section_name):
801
        """
802
        Return a list of pairs as values that represents the role-permission
803
        view relation for the panel section.
804
        :param section_name: the panels section id.
805
        :return: a list of tuples.
806
        """
807
        return get_dashboard_panels_visibility_by_section(section_name)
808
809
810
class DashboardViewPermissionUpdate(BrowserView):
811
    """
812
    Updates the values in 'senaite.core.dashboard_panels_visibility' registry.
813
    """
814
815
    def __call__(self):
816
        protect.CheckAuthenticator(self.request)
817
        # Getting values from post
818
        section_name = self.request.get('section_name', None)
819
        if section_name is None:
820
            return None
821
        role_id = self.request.get('role_id', None)
822
        if role_id is None:
823
            return None
824
        check_state = self.request.get('check_state', None)
825
        if check_state is None:
826
            return None
827
        elif check_state == 'false':
828
            check_state = 'no'
829
        else:
830
            check_state = 'yes'
831
        # Update registry
832
        registry_info = get_dashboard_registry_record()
833
        pairs = get_dashboard_panels_visibility_by_section(section_name)
834
        role_permissions = list()
835
        for pair in pairs:
836
            visibility = pair[1]
837
            if pair[0] == role_id:
838
                visibility = check_state
839
            value = '{0},{1}'.format(pair[0], visibility)
840
            role_permissions.append(value)
841
        role_permissions = ','.join(role_permissions)
842
        # Set permissions string into dict
843
        registry_info[section_name] = get_unicode(role_permissions)
844
        set_dashboard_registry_record(registry_info)
845
        return True
846