Passed
Push — master ( 5f146e...28e0c7 )
by Jordi
04:40
created

  A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
# -*- coding: utf-8 -*-
2
3
import collections
4
import copy
5
import json
6
import re
7
import time
8
9
import DateTime
10
import Missing
11
from AccessControl import getSecurityManager
12
from ajax import AjaxListingView
13
from bika.lims import api
14
from bika.lims import bikaMessageFactory as _
15
from bika.lims import deprecated
16
from bika.lims import logger
17
from bika.lims.interfaces import IFieldIcons
18
from bika.lims.utils import getFromString
19
from bika.lims.utils import t
20
from bika.lims.utils import to_utf8
21
from plone.memoize import view
22
from Products.CMFCore.utils import getToolByName
23
from Products.CMFPlone.utils import safe_unicode
24
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
25
from zope.component import getAdapters
26
from zope.component import getMultiAdapter
27
28
29
class ListingView(AjaxListingView):
30
    """Base Listing View
31
    """
32
    template = ViewPageTemplateFile("templates/listing.pt")
33
34
    # The title of the outer listing view
35
    # see: templates/listing.pt
36
    title = ""
37
38
    # The description of the outer listing view
39
    # see: templates/listing.pt
40
    description = ""
41
42
    # TODO: Refactor to viewlet
43
    # Context actions rendered next to the title
44
    # see: templates/listing.pt
45
    context_actions = {}
46
47
    # Default search catalog to be used for the content filter query
48
    catalog = "portal_catalog"
49
50
    # Catalog query used for the listing. It can be extended by the
51
    # review_state filter
52
    contentFilter = {}
53
54
    # A mapping of column_key -> colum configuration
55
    columns = collections.OrderedDict((
56
        ("Title", {
57
            "title": _("Title"),
58
            "index": "sortable_title"}),
59
        ("Description", {
60
            "title": _("Description"),
61
            "index": "Description"}),
62
    ))
63
64
    # A list of dictionaries, specifying parameters for listing filter buttons.
65
    #
66
    # - If review_state[x]["transitions"] is defined, e.g.:
67
    #     "transitions": [{"id": "x"}]
68
    # The possible transitions will be restricted by those defined
69
    #
70
    # - If review_state[x]["custom_transitions"] is defined, e.g.:
71
    #     "custom_transitions": [{"id": "x"}]
72
    # The possible transitions will be extended by those defined.
73
    review_states = [
74
        {
75
            "id": "default",
76
            "title": _("All"),
77
            "contentFilter": {},
78
            "transitions": [],
79
            "custom_transitions": [],
80
            "columns": ["Title", "Descritpion"],
81
        }
82
    ]
83
84
    # The initial/default review_state
85
    default_review_state = "default"
86
87
    # When rendering multiple listing tables, e.g. AR lab/field/qc tables, the
88
    # form_id must be unique for each listing table.
89
    form_id = "list"
90
91
    # This is an override and a switch, but it does not guarantee allow_edit.
92
    # This can be used to turn it off, regardless of settings in place by
93
    # individual items/fields, but if it is turned on, ultimate control
94
    # is still given to the individual items/fields.
95
    allow_edit = True
96
97
    # Defines the input name of the select checkbox.
98
    # This will be probaly removed in later versions, because most of the form
99
    # handlers expect the selected UIDs inside the "uids" request parameter
100
    select_checkbox_name = "uids"
101
102
    # Display a checkbox to select all visible rows
103
    show_select_all_checkbox = True
104
105
    # Display the left-most column for selecting all/individual items.
106
    # Also see the "fetch_transitions_on_select" option.
107
    show_select_column = False
108
109
    # Automatically fetch all possible transitions for selected items.
110
    fetch_transitions_on_select = True
111
112
    # Allow to show/hide columns by right-clicking on the column header.
113
    show_column_toggles = True
114
115
    # Render items in expandable categories
116
    show_categories = False
117
118
    # These are the possible categories. If self.show_categories is True, only
119
    # these categories which will be rendered.
120
    categories = []
121
122
    # Expand all categories on load. If set to False, only categories
123
    # containing selected items are expanded.
124
    expand_all_categories = False
125
126
    # Number of rows initially displayed. If more items are returned from the
127
    # database, a paging control is displayed in the lower right corner
128
    pagesize = 50
129
130
    # Override pagesize and show all items on one page
131
    # XXX: Currently only used in classic folderitems method.
132
    #      -> Consider if it is worth to keep that funcitonality
133
    show_all = False
134
135
    # Manually sort catalog results on this column
136
    # XXX: Currently the listing table sorts only if the catalog index exists.
137
    #      -> Consider if it is worth to keep that functionality
138
    manual_sort_on = None
139
140
    # Render the search box in the upper right corner
141
    show_search = True
142
143
    # Omit the outer form wrapper of the contents table, e.g. when the listing
144
    # is used as an embedded widget in an edit form.
145
    omit_form = False
146
147
    # Toggle transition button rendering of the table footer
148
    show_workflow_action_buttons = True
149
150
    # Toggle the whole table footer rendering. This includes the pagination and
151
    # transition buttons.
152
    show_table_footer = True
153
154
    def __init__(self, context, request):
155
        super(ListingView, self).__init__(context, request)
156
        self.context = context
157
        self.request = request
158
159
        # N.B. We set that here so that it can be overridden by subclasses,
160
        #      e.g. by the worksheet add view
161
        if "path" not in self.contentFilter:
162
            self.contentFilter.update(self.get_path_query())
163
164
        self.total = 0
165
        self.limit_from = 0
166
        self.show_more = False
167
        self.sort_on = "sortable_title"
168
        self.sort_order = "ascending"
169
170
        # Internal cache for translated state titles
171
        self.state_titles = {}
172
173
        # TODO: Refactor to a view memoized property
174
        # Internal cache for alert icons
175
        self.field_icons = {}
176
177
    def __call__(self):
178
        """Handle request parameters and render the form
179
        """
180
        logger.info(u"ListingView::__call__")
181
182
        self.portal = api.get_portal()
183
        self.mtool = api.get_tool("portal_membership")
184
        self.workflow = api.get_tool("portal_workflow")
185
        self.member = api.get_current_user()
186
        self.translate = self.context.translate
187
188
        # Call update hook
189
        self.update()
190
191
        # handle subpath calls
192
        if len(self.traverse_subpath) > 0:
193
            return self.handle_subpath()
194
195
        # Call before render hook
196
        self.before_render()
197
198
        return self.template(self.context)
199
200
    def update(self):
201
        """Update the view state
202
        """
203
        logger.info(u"ListingView::update")
204
        self.limit_from = self.get_limit_from()
205
        self.pagesize = self.get_pagesize()
206
207
    def before_render(self):
208
        """Before render hook
209
        """
210
        logger.info(u"ListingView::before_render")
211
212
    def contents_table(self, *args, **kwargs):
213
        """Render the ReactJS enabled contents table template
214
        """
215
        return self.contents_table_template()
216
217
    @view.memoize
218
    def is_permission_granted_for(self, permission, context=None):
219
        """Checks if the the given permission is granted
220
221
        :param permission: Permission to check
222
        :param brain_or_object: Object to check the permission
223
        """
224
        sm = getSecurityManager()
225
        if context is None:
226
            context = self.context
227
        context = api.get_object(context)
228
        return sm.checkPermission(permission, context)
229
230
    @property
231
    def review_state(self):
232
        """Get workflow state of object in wf_id.
233
234
        First try request: <form_id>_review_state
235
        Then try 'default': self.default_review_state
236
237
        :return: item from self.review_states
238
        """
239
        if not self.review_states:
240
            logger.error("%s.review_states is undefined." % self)
241
            return None
242
        # get state_id from (request or default_review_states)
243
        key = "%s_review_state" % self.form_id
244
        state_id = self.request.form.get(key, self.default_review_state)
245
        if not state_id:
246
            state_id = self.default_review_state
247
        states = [r for r in self.review_states if r["id"] == state_id]
248
        if not states:
249
            logger.error("%s.review_states does not contain id='%s'." %
250
                         (self, state_id))
251
            return None
252
        review_state = states[0] if states else self.review_states[0]
253
        # set selected state into the request
254
        self.request["%s_review_state" % self.form_id] = review_state["id"]
255
        return review_state
256
257
    def remove_column(self, column):
258
        """Removes the column passed-in, if exists
259
260
        :param column: Column key
261
        :returns: True if the column was removed
262
        """
263
        if column not in self.columns:
264
            return False
265
266
        del self.columns[column]
267
        for item in self.review_states:
268
            if column in item.get("columns", []):
269
                item["columns"].remove(column)
270
        return True
271
272
    def getPOSTAction(self):
273
        """This function returns a string as the value for the action attribute
274
        of the form element in the template.
275
276
        This method is used in bika_listing_table.pt
277
        """
278
        return "workflow_action"
279
280
    def get_form_id(self):
281
        """Return the form id
282
283
        Note: The form_id must be unique when rendering multiple listing tables
284
        """
285
        return self.form_id
286
287
    def get_catalog(self, default="portal_catalog"):
288
        """Get the catalog tool to be used in the listing
289
290
        :returns: ZCatalog tool
291
        """
292
        try:
293
            return api.get_tool(self.catalog)
294
        except api.BikaLIMSError:
295
            return api.get_tool(default)
296
297
    @view.memoize
298
    def get_catalog_indexes(self):
299
        """Return a list of registered catalog indexes
300
        """
301
        return self.get_catalog().indexes()
302
303
    @view.memoize
304
    def get_columns_indexes(self):
305
        """Returns a list of allowed sorting indexeds
306
        """
307
        columns = self.columns
308
        indexes = [v["index"] for k, v in columns.items() if "index" in v]
309
        return indexes
310
311
    @view.memoize
312
    def get_metadata_columns(self):
313
        """Get a list of all metadata column names
314
315
        :returns: List of catalog metadata column names
316
        """
317
        catalog = self.get_catalog()
318
        return catalog.schema()
319
320
    @view.memoize
321
    def translate_review_state(self, state, portal_type):
322
        """Translates the review state to the current set language
323
324
        :param state: Review state title
325
        :type state: basestring
326
        :returns: Translated review state title
327
        """
328
        ts = api.get_tool("translation_service")
329
        wf = api.get_tool("portal_workflow")
330
        state_title = wf.getTitleForStateOnType(state, portal_type)
331
        translated_state = ts.translate(
332
            _(state_title or state), context=self.request)
333
        logger.info(u"ListingView:translate_review_state: {} -> {} -> {}"
334
                    .format(state, state_title, translated_state))
335
        return translated_state
336
337
    def metadata_to_searchable_text(self, brain, key, value):
338
        """Parse the given metadata to text
339
340
        :param brain: ZCatalog Brain
341
        :param key: The name of the metadata column
342
        :param value: The raw value of the metadata column
343
        :returns: Searchable and translated unicode value or None
344
        """
345
        if not value:
346
            return u""
347
        if value is Missing.Value:
348
            return u""
349
        if api.is_uid(value):
350
            return u""
351
        if isinstance(value, (bool)):
352
            return u""
353
        if isinstance(value, (list, tuple)):
354
            for v in value:
355
                return self.metadata_to_searchable_text(brain, key, v)
356
        if isinstance(value, (dict)):
357
            for k, v in value.items():
358
                return self.metadata_to_searchable_text(brain, k, v)
359
        if self.is_date(value):
360
            return self.to_str_date(value)
361
        if "state" in key.lower():
362
            return self.translate_review_state(
363
                value, api.get_portal_type(brain))
364
        if not isinstance(value, basestring):
365
            value = str(value)
366
        return safe_unicode(value)
367
368
    def get_sort_order(self):
369
        """Get the sort_order criteria from the request or view
370
        """
371
        form_id = self.get_form_id()
372
        allowed = ["ascending", "descending"]
373
        sort_order = [self.request.form.get("{}_sort_order"
374
                                            .format(form_id), None),
375
                      self.contentFilter.get("sort_order", None)]
376
        sort_order = filter(lambda order: order in allowed, sort_order)
377
        return sort_order and sort_order[0] or "descending"
378
379
    def get_sort_on(self, default="created"):
380
        """Get the sort_on criteria to be used
381
382
        :param default: The default sort_on index to be used
383
        :returns: valid sort_on index or None
384
        """
385
        form_id = self.get_form_id()
386
        key = "{}_sort_on".format(form_id)
387
388
        # List of known catalog columns
389
        catalog_columns = self.get_metadata_columns()
390
391
        # The sort_on parameter from the request
392
        sort_on = self.request.form.get(key, None)
393
        # Use the index specified in the columns config
394
        if sort_on in self.columns:
395
            sort_on = self.columns[sort_on].get("index", sort_on)
396
397
        # Return immediately if the request sort_on parameter is found in the
398
        # catalog indexes
399
        if self.is_valid_sort_index(sort_on):
400
            return sort_on
401
402
        # Flag manual sorting if the request sort_on parameter is found in the
403
        # catalog metadata columns
404
        if sort_on in catalog_columns:
405
            self.manual_sort_on = sort_on
406
407
        # The sort_on parameter from the catalog query
408
        content_filter_sort_on = self.contentFilter.get("sort_on", None)
409
        if self.is_valid_sort_index(content_filter_sort_on):
410
            return content_filter_sort_on
411
412
        # The sort_on attribute from the instance
413
        instance_sort_on = self.sort_on
414
        if self.is_valid_sort_index(instance_sort_on):
415
            return instance_sort_on
416
417
        # The default sort_on
418
        if self.is_valid_sort_index(default):
419
            return default
420
421
        return None
422
423
    def is_valid_sort_index(self, sort_on):
424
        """Checks if the sort_on index is capable for a sort_
425
426
        :param sort_on: The name of the sort index
427
        :returns: True if the sort index is capable for sorting
428
        """
429
        # List of known catalog indexes
430
        catalog_indexes = self.get_catalog_indexes()
431
        if sort_on not in catalog_indexes:
432
            return False
433
        catalog = self.get_catalog()
434
        sort_index = catalog.Indexes.get(sort_on)
435
        if not hasattr(sort_index, "documentToKeyMap"):
436
            return False
437
        return True
438
439
    def is_date(self, thing):
440
        """checks if the passed in value is a date
441
442
        :param thing: an arbitrary object
443
        :returns: True if it can be converted to a date time object
444
        """
445
        if isinstance(thing, DateTime.DateTime):
446
            return True
447
        return False
448
449
    def to_str_date(self, date):
450
        """Converts the date to a string
451
452
        :param date: DateTime object or ISO date string
453
        :returns: locale date string
454
        """
455
        date = DateTime.DateTime(date)
456
        try:
457
            return date.strftime(self.date_format_long)
458
        except ValueError:
459
            return str(date)
460
461
    def get_pagesize(self):
462
        """Return the pagesize request parameter
463
        """
464
        form_id = self.get_form_id()
465
        pagesize = self.request.form.get(form_id + '_pagesize', self.pagesize)
466
        try:
467
            return int(pagesize)
468
        except (ValueError, TypeError):
469
            return self.pagesize
470
471
    def get_limit_from(self):
472
        """Return the limit_from request parameter
473
        """
474
        form_id = self.get_form_id()
475
        limit = self.request.form.get(form_id + '_limit_from', 0)
476
        try:
477
            return int(limit)
478
        except (ValueError, TypeError):
479
            return 0
480
481
    def get_path_query(self, context=None, level=0):
482
        """Return a path query
483
484
        :param context: The context to get the physical path from
485
        :param level: The depth level of the search
486
        :returns: Catalog path query
487
        """
488
        if context is None:
489
            context = self.context
490
        path = api.get_path(context)
491
        return {
492
            "path": {
493
                "query": path,
494
                "level": level,
495
            }
496
        }
497
498
    def get_item_info(self, brain_or_object):
499
        """Return the data of this brain or object
500
        """
501
        return {
502
            "obj": brain_or_object,
503
            "uid": api.get_uid(brain_or_object),
504
            "url": api.get_url(brain_or_object),
505
            "id": api.get_id(brain_or_object),
506
            "title": api.get_title(brain_or_object),
507
            "portal_type": api.get_portal_type(brain_or_object),
508
            "review_state": api.get_workflow_status_of(brain_or_object),
509
        }
510
511
    def get_catalog_query(self, searchterm=None):
512
        """Return the catalog query
513
514
        :param searchterm: Additional filter value to be added to the query
515
        :returns: Catalog query dictionary
516
        """
517
518
        # avoid to change the original content filter
519
        query = copy.deepcopy(self.contentFilter)
520
521
        # contentFilter is allowed in every self.review_state.
522
        for k, v in self.review_state.get("contentFilter", {}).items():
523
            query[k] = v
524
525
        # set the sort_on criteria
526
        sort_on = self.get_sort_on()
527
        if sort_on is not None:
528
            query["sort_on"] = sort_on
529
530
        # set the sort_order criteria
531
        query["sort_order"] = self.get_sort_order()
532
533
        # # Pass the searchterm as well to the Searchable Text index
534
        # if searchterm and isinstance(searchterm, basestring):
535
        #     query.update({"SearchableText": searchterm + "*"})
536
537
        logger.info(u"ListingView::get_catalog_query: query={}".format(query))
538
        return query
539
540
    def make_regex_for(self, searchterm, ignorecase=True):
541
        """Make a regular expression for the given searchterm
542
543
        :param searchterm: The searchterm for the regular expression
544
        :param ignorecase: Flag to compile with re.IGNORECASE
545
        :returns: Compiled regular expression
546
        """
547
        # searchterm comes in as a 8-bit string, e.g. 'D\xc3\xa4'
548
        # but must be a unicode u'D\xe4' to match the metadata
549
        searchterm = safe_unicode(searchterm)
550
        if ignorecase:
551
            return re.compile(searchterm, re.IGNORECASE)
552
        return re.compile(searchterm)
553
554
    def sort_brains(self, brains, sort_on=None):
555
        """Sort the brains
556
557
        :param brains: List of catalog brains
558
        :param sort_on: The metadata column name to sort on
559
        :returns: Manually sorted list of brains
560
        """
561
        if sort_on not in self.get_metadata_columns():
562
            logger.warn(
563
                "ListingView::sort_brains: '{}' not in metadata columns."
564
                .format(sort_on))
565
            return brains
566
567
        logger.warn(
568
            "ListingView::sort_brains: Manual sorting on metadata column '{}'."
569
            "Consider to add an explicit catalog index to speed up filtering."
570
            .format(self.manual_sort_on))
571
572
        # calculate the sort_order
573
        reverse = self.get_sort_order() == "descending"
574
575
        def metadata_sort(a, b):
576
            a = getattr(a, self.manual_sort_on, "")
577
            b = getattr(b, self.manual_sort_on, "")
578
            return cmp(safe_unicode(a), safe_unicode(b))
579
580
        return sorted(brains, cmp=metadata_sort, reverse=reverse)
581
582
    def get_searchterm(self):
583
        """Get the user entered search value from the request
584
585
        :returns: Current search box value from the request
586
        """
587
        form_id = self.get_form_id()
588
        key = "{}_filter".format(form_id)
589
        # we need to ensure unicode here
590
        return safe_unicode(self.request.form.get(key, ""))
591
592
    def metadata_search(self, catalog, query, searchterm, ignorecase=True):
593
        """ Retrieves all the brains from given catalog and returns the ones
594
        with at least one metadata containing the search term
595
        :param catalog: catalog to search
596
        :param query:
597
        :param searchterm:
598
        :param ignorecase:
599
        :return: brains matching search result
600
        """
601
        # create a catalog query
602
        logger.info(u"ListingView::search: Prepare metadata query for '{}'"
603
                    .format(self.catalog))
604
605
        brains = catalog(query)
606
607
        # Build a regular expression for the given searchterm
608
        regex = self.make_regex_for(searchterm, ignorecase=ignorecase)
609
610
        # Get the catalog metadata columns
611
        columns = self.get_metadata_columns()
612
613
        # Filter predicate to match each metadata value against the searchterm
614
        def match(brain):
615
            for column in columns:
616
                value = getattr(brain, column, None)
617
                parsed = self.metadata_to_searchable_text(brain, column, value)
618
                if regex.search(parsed):
619
                    return True
620
            return False
621
622
        # Filtered brains by searchterm -> metadata match
623
        return filter(match, brains)
624
625
    def ng3_index_search(self, catalog, query, searchterm):
626
        """Searches given catalog by query and also looks for a keyword in the
627
        specific index called "listing_searchable_text"
628
629
        #REMEMBER TextIndexNG indexes are the only indexes that wildcards can
630
        be used in the beginning of the string.
631
        http://zope.readthedocs.io/en/latest/zope2book/SearchingZCatalog.html#textindexng
632
633
        :param catalog: catalog to search
634
        :param query:
635
        :param searchterm: a keyword to look for in "listing_searchable_text"
636
        :return: brains matching the search result
637
        """
638
        logger.info(u"ListingView::search: Prepare NG3 index query for '{}'"
639
                    .format(self.catalog))
640
        # Remove quotation mark
641
        searchterm = searchterm.replace('"', '')
642
        # If the keyword is not encoded in searches, TextIndexNG3 encodes by
643
        # default encoding which we cannot always trust
644
        searchterm = searchterm.encode("utf-8")
645
        query["listing_searchable_text"] = "*" + searchterm + "*"
646
        return catalog(query)
647
648
    def _fetch_brains(self, idxfrom=0):
649
        """Fetch the catalog results for the current listing table state
650
        """
651
652
        searchterm = self.get_searchterm()
653
        brains = self.search(searchterm=searchterm)
654
        self.total = len(brains)
655
656
        # Return a subset of results, if necessary
657
        if idxfrom and len(brains) > idxfrom:
658
            return brains[idxfrom:self.pagesize + idxfrom]
659
        return brains[:self.pagesize]
660
661
    def search(self, searchterm="", ignorecase=True):
662
        """Search the catalog tool
663
664
        :param searchterm: The searchterm for the regular expression
665
        :param ignorecase: Flag to compile with re.IGNORECASE
666
        :returns: List of catalog brains
667
        """
668
669
        # TODO Append start and pagesize to return just that slice of results
670
671
        # start the timer for performance checks
672
        start = time.time()
673
674
        # strip whitespaces off the searchterm
675
        searchterm = searchterm.strip()
676
        # Strip illegal characters of the searchterm
677
        searchterm = searchterm.strip(u"*.!$%&/()=-+:'`´^")
678
        logger.info(u"ListingView::search:searchterm='{}'".format(searchterm))
679
680
        # create a catalog query
681
        logger.info(u"ListingView::search: Prepare catalog query for '{}'"
682
                    .format(self.catalog))
683
        query = self.get_catalog_query(searchterm=searchterm)
684
685
        # search the catalog
686
        catalog = api.get_tool(self.catalog)
687
688
        # return the unfiltered catalog results if no searchterm
689
        if not searchterm:
690
            brains = catalog(query)
691
692
        # check if there is ng3 index in the catalog to query by wildcards
693
        elif "listing_searchable_text" in catalog.indexes():
694
            # Always expand all categories if we have a searchterm
695
            self.expand_all_categories = True
696
            brains = self.ng3_index_search(catalog, query, searchterm)
697
698
        else:
699
            self.expand_all_categories = True
700
            brains = self.metadata_search(
701
                catalog, query, searchterm, ignorecase)
702
703
        # Sort manually?
704
        if self.manual_sort_on is not None:
705
            brains = self.sort_brains(brains, sort_on=self.manual_sort_on)
706
707
        end = time.time()
708
        logger.info(u"ListingView::search: Search for '{}' executed in "
709
                    u"{:.2f}s ({} matches)"
710
                    .format(searchterm, end - start, len(brains)))
711
        return brains
712
713
    def isItemAllowed(self, obj):
714
        """ return if the item can be added to the items list.
715
        """
716
        return True
717
718
    def folderitem(self, obj, item, index):
719
        """Service triggered each time an item is iterated in folderitems.
720
721
        The use of this service prevents the extra-loops in child objects.
722
723
        :obj: the instance of the class to be foldered
724
        :item: dict containing the properties of the object to be used by
725
            the template
726
        :index: current index of the item
727
        """
728
        return item
729
730
    def folderitems(self, full_objects=False, classic=True):
731
        """This function returns an array of dictionaries where each dictionary
732
        contains the columns data to render the list.
733
734
        No object is needed by default. We should be able to get all
735
        the listing columns taking advantage of the catalog's metadata,
736
        so that the listing will be much more faster. If a very specific
737
        info has to be retrieve from the objects, we can define
738
        full_objects as True but performance can be lowered.
739
740
        :full_objects: a boolean, if True, each dictionary will contain an item
741
                       with the object itself. item.get('obj') will return a
742
                       object. Only works with the 'classic' way.
743
        WARNING: :full_objects: could create a big performance hit!
744
        :classic: if True, the old way folderitems works will be executed. This
745
                  function is mainly used to maintain the integrity with the
746
                  old version.
747
        """
748
        # Getting a security manager instance for the current request
749
        self.security_manager = getSecurityManager()
750
        self.workflow = getToolByName(self.context, 'portal_workflow')
751
752
        if classic:
753
            return self._folderitems(full_objects)
754
755
        # idx increases one unit each time an object is added to the 'items'
756
        # dictionary to be returned. Note that if the item is not rendered,
757
        # the idx will not increase.
758
        idx = 0
759
        results = []
760
        self.show_more = False
761
        brains = self._fetch_brains(self.limit_from)
762
        for obj in brains:
763
            # avoid creating unnecessary info for items outside the current
764
            # batch;  only the path is needed for the "select all" case...
765
            # we only take allowed items into account
766
            if idx >= self.pagesize:
767
                # Maximum number of items to be shown reached!
768
                self.show_more = True
769
                break
770
771
            # check if the item must be rendered or not (prevents from
772
773
            # doing it later in folderitems) and dealing with paging
774
            if not obj or not self.isItemAllowed(obj):
775
                continue
776
777
            # Get the css for this row in accordance with the obj's state
778
            states = obj.getObjectWorkflowStates
779
            if not states:
780
                states = {}
781
            state_class = ['state-{0}'.format(v) for v in states.values()]
782
            state_class = ' '.join(state_class)
783
784
            # Building the dictionary with basic items
785
            results_dict = dict(
786
                # To colour the list items by state
787
                state_class=state_class,
788
                # a list of names of fields that may be edited on this item
789
                allow_edit=[],
790
                # a dict where the column name works as a key and the value is
791
                # the name of the field related with the column. It is used
792
                # when the name given to the column and the content field it
793
                # represents diverges. bika_listing_table_items.pt defines an
794
                # attribute for each item, this attribute is named 'field' and
795
                # the system fills it taking advantage of this dictionary or
796
                # the name of the column if it isn't defined in the dict.
797
                field={},
798
                # "before", "after" and replace: dictionary (key is column ID)
799
                # A snippet of HTML which will be rendered
800
                # before/after/instead of the table cell content.
801
                before={},  # { before : "<a href=..>" }
802
                after={},
803
                replace={},
804
                choices={},
805
            )
806
807
            # update with the base item info
808
            results_dict.update(self.get_item_info(obj))
809
810
            # Set states and state titles
811
            ptype = obj.portal_type
812
            workflow = api.get_tool('portal_workflow')
813
            for state_var, state in states.items():
814
                results_dict[state_var] = state
815
                state_title = self.state_titles.get(state, None)
816
                if not state_title:
817
                    state_title = workflow.getTitleForStateOnType(state, ptype)
818
                    if state_title:
819
                        self.state_titles[state] = state_title
820
                if state_title and state == obj.review_state:
821
                    results_dict['state_title'] = _(state_title)
822
823
            # extra classes for individual fields on this item
824
            # { field_id : "css classes" }
825
            results_dict['class'] = {}
826
827
            # Search for values for all columns in obj
828
            for key in self.columns.keys():
829
                # if the key is already in the results dict
830
                # then we don't replace it's value
831
                value = results_dict.get(key, '')
832 View Code Duplication
                if not value:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
833
                    attrobj = getFromString(obj, key)
834
                    value = attrobj if attrobj else value
835
836
                    # Custom attribute? Inspect to set the value
837
                    # for the current column dynamically
838
                    vattr = self.columns[key].get('attr', None)
839
                    if vattr:
840
                        attrobj = getFromString(obj, vattr)
841
                        value = attrobj if attrobj else value
842
                    results_dict[key] = value
843
                # Replace with an url?
844
                replace_url = self.columns[key].get('replace_url', None)
845
                if replace_url:
846
                    attrobj = getFromString(obj, replace_url)
847
                    if attrobj:
848
                        results_dict['replace'][key] = \
849
                            '<a href="%s">%s</a>' % (attrobj, value)
850
            # The item basics filled. Delegate additional actions to folderitem
851
            # service. folderitem service is frequently overriden by child
852
            # objects
853
            item = self.folderitem(obj, results_dict, idx)
854
            if item:
855
                results.append(item)
856
                idx += 1
857
        return results
858
859
    @deprecated("Using bikalisting.folderitems(classic=True) is very slow")
860
    def _folderitems(self, full_objects=False):
861
        """WARNING: :full_objects: could create a big performance hit.
862
        """
863
        # Setting up some attributes
864
        plone_layout = getMultiAdapter((self.context.aq_inner, self.request),
865
                                       name=u'plone_layout')
866
        plone_utils = getToolByName(self.context.aq_inner, 'plone_utils')
867
        portal_types = getToolByName(self.context.aq_inner, 'portal_types')
868
        if self.request.form.get('show_all', '').lower() == 'true' \
869
                or self.show_all is True \
870
                or self.pagesize == 0:
871
            show_all = True
872
        else:
873
            show_all = False
874
875
        # idx increases one unit each time an object is added to the 'items'
876
        # dictionary to be returned. Note that if the item is not rendered,
877
        # the idx will not increase.
878
        idx = 0
879
        results = []
880
        self.show_more = False
881
        brains = self._fetch_brains(self.limit_from)
882
        for obj in brains:
883
            # avoid creating unnecessary info for items outside the current
884
            # batch;  only the path is needed for the "select all" case...
885
            # we only take allowed items into account
886
            if not show_all and idx >= self.pagesize:
887
                # Maximum number of items to be shown reached!
888
                self.show_more = True
889
                break
890
891
            # we don't know yet if it's a brain or an object
892
            path = hasattr(obj, 'getPath') and obj.getPath() or \
893
                "/".join(obj.getPhysicalPath())
894
895
            # This item must be rendered, we need the object instead of a brain
896
            obj = obj.getObject() if hasattr(obj, 'getObject') else obj
897
898
            # check if the item must be rendered or not (prevents from
899
            # doing it later in folderitems) and dealing with paging
900
            if not obj or not self.isItemAllowed(obj):
901
                continue
902
903
            uid = obj.UID()
904
            title = obj.Title()
905
            description = obj.Description()
906
            icon = plone_layout.getIcon(obj)
907
            url = obj.absolute_url()
908
            relative_url = obj.absolute_url(relative=True)
909
910
            fti = portal_types.get(obj.portal_type)
911
            if fti is not None:
912
                type_title_msgid = fti.Title()
913
            else:
914
                type_title_msgid = obj.portal_type
915
916
            url_href_title = '%s at %s: %s' % (
917
                t(type_title_msgid),
918
                path,
919
                to_utf8(description))
920
921
            modified = self.ulocalized_time(obj.modified()),
922
923
            # element css classes
924
            type_class = 'contenttype-' + \
925
                         plone_utils.normalizeString(obj.portal_type)
926
927
            state_class = ''
928
            states = {}
929
            for w in self.workflow.getWorkflowsFor(obj):
930
                state = w._getWorkflowStateOf(obj).id
931
                states[w.state_var] = state
932
                state_class += "state-%s " % state
933
934
            results_dict = dict(
935
                obj=obj,
936
                id=obj.getId(),
937
                title=title,
938
                uid=uid,
939
                path=path,
940
                url=url,
941
                fti=fti,
942
                item_data=json.dumps([]),
943
                url_href_title=url_href_title,
944
                obj_type=obj.Type,
945
                size=obj.getObjSize,
946
                modified=modified,
947
                icon=icon.html_tag(),
948
                type_class=type_class,
949
                # a list of lookups for single-value-select fields
950
                choices={},
951
                state_class=state_class,
952
                relative_url=relative_url,
953
                view_url=url,
954
                table_row_class="",
955
                category='None',
956
957
                # a list of names of fields that may be edited on this item
958
                allow_edit=[],
959
960
                # a list of names of fields that are compulsory (if editable)
961
                required=[],
962
                # a dict where the column name works as a key and the value is
963
                # the name of the field related with the column. It is used
964
                # when the name given to the column and the content field it
965
                # represents diverges. bika_listing_table_items.pt defines an
966
                # attribute for each item, this attribute is named 'field' and
967
                # the system fills it taking advantage of this dictionary or
968
                # the name of the column if it isn't defined in the dict.
969
                field={},
970
                # "before", "after" and replace: dictionary (key is column ID)
971
                # A snippet of HTML which will be rendered
972
                # before/after/instead of the table cell content.
973
                before={},  # { before : "<a href=..>" }
974
                after={},
975
                replace={},
976
            )
977
978
            rs = None
979
            wf_state_var = None
980
981
            workflows = self.workflow.getWorkflowsFor(obj)
982
            for wf in workflows:
983
                if wf.state_var:
984
                    wf_state_var = wf.state_var
985
                    break
986
987
            if wf_state_var is not None:
988
                rs = self.workflow.getInfoFor(obj, wf_state_var)
989
                st_title = self.workflow.getTitleForStateOnType(
990
                    rs, obj.portal_type)
991
                st_title = t(_(st_title))
992
993
            if rs:
994
                results_dict['review_state'] = rs
995
996
            for state_var, state in states.items():
997
                if not st_title:
0 ignored issues
show
introduced by
The variable st_title does not seem to be defined for all execution paths.
Loading history...
998
                    st_title = self.workflow.getTitleForStateOnType(
999
                        state, obj.portal_type)
1000
                results_dict[state_var] = state
1001
            results_dict['state_title'] = st_title
1002
1003
            results_dict['class'] = {}
1004
1005
            # As far as I am concerned, adapters for IFieldIcons are only used
1006
            # for Analysis content types. Since AnalysesView is not using this
1007
            # "classic" folderitems from bikalisting anymore, this logic has
1008
            # been added in AnalysesView. Even though, this logic hasn't been
1009
            # removed from here, cause this _folderitems function is marked as
1010
            # deprecated, so it will be eventually removed alltogether.
1011
            for name, adapter in getAdapters((obj,), IFieldIcons):
1012
                auid = obj.UID() if hasattr(obj, 'UID') and callable(
1013
                    obj.UID) else None
1014
                if not auid:
1015
                    continue
1016
                alerts = adapter()
1017
                # logger.info(str(alerts))
1018
                if alerts and auid in alerts:
1019
                    if auid in self.field_icons:
1020
                        self.field_icons[auid].extend(alerts[auid])
1021
                    else:
1022
                        self.field_icons[auid] = alerts[auid]
1023
1024
            # Search for values for all columns in obj
1025
            for key in self.columns.keys():
1026
                # if the key is already in the results dict
1027
                # then we don't replace it's value
1028
                value = results_dict.get(key, '')
1029 View Code Duplication
                if key not in results_dict:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1030
                    attrobj = getFromString(obj, key)
1031
                    value = attrobj if attrobj else value
1032
1033
                    # Custom attribute? Inspect to set the value
1034
                    # for the current column dinamically
1035
                    vattr = self.columns[key].get('attr', None)
1036
                    if vattr:
1037
                        attrobj = getFromString(obj, vattr)
1038
                        value = attrobj if attrobj else value
1039
                    results_dict[key] = value
1040
1041
                # Replace with an url?
1042
                replace_url = self.columns[key].get('replace_url', None)
1043
                if replace_url:
1044
                    attrobj = getFromString(obj, replace_url)
1045
                    if attrobj:
1046
                        results_dict['replace'][key] = \
1047
                            '<a href="%s">%s</a>' % (attrobj, value)
1048
1049
            # The item basics filled. Delegate additional actions to folderitem
1050
            # service. folderitem service is frequently overriden by child
1051
            # objects
1052
            item = self.folderitem(obj, results_dict, idx)
1053
            if item:
1054
                results.append(item)
1055
                idx += 1
1056
1057
        return results
1058