Passed
Push — master ( 0e02ba...4def5b )
by Jordi
04:46
created

ListingView.folderitems()   F

Complexity

Conditions 20

Size

Total Lines 128
Code Lines 62

Duplication

Lines 11
Ratio 8.59 %

Importance

Changes 0
Metric Value
eloc 62
dl 11
loc 128
rs 0
c 0
b 0
f 0
cc 20
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like bika.lims.browser.listing.view.ListingView.folderitems() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
3
import 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
    @property
218
    def review_state(self):
219
        """Get workflow state of object in wf_id.
220
221
        First try request: <form_id>_review_state
222
        Then try 'default': self.default_review_state
223
224
        :return: item from self.review_states
225
        """
226
        if not self.review_states:
227
            logger.error("%s.review_states is undefined." % self)
228
            return None
229
        # get state_id from (request or default_review_states)
230
        key = "%s_review_state" % self.form_id
231
        state_id = self.request.form.get(key, self.default_review_state)
232
        if not state_id:
233
            state_id = self.default_review_state
234
        states = [r for r in self.review_states if r["id"] == state_id]
235
        if not states:
236
            logger.error("%s.review_states does not contain id='%s'." %
237
                         (self, state_id))
238
            return None
239
        review_state = states[0] if states else self.review_states[0]
240
        # set selected state into the request
241
        self.request["%s_review_state" % self.form_id] = review_state["id"]
242
        return review_state
243
244
    def remove_column(self, column):
245
        """Removes the column passed-in, if exists
246
247
        :param column: Column key
248
        :returns: True if the column was removed
249
        """
250
        if column not in self.columns:
251
            return False
252
253
        del self.columns[column]
254
        for item in self.review_states:
255
            if column in item.get("columns", []):
256
                item["columns"].remove(column)
257
        return True
258
259
    def getPOSTAction(self):
260
        """This function returns a string as the value for the action attribute
261
        of the form element in the template.
262
263
        This method is used in bika_listing_table.pt
264
        """
265
        return "workflow_action"
266
267
    def get_form_id(self):
268
        """Return the form id
269
270
        Note: The form_id must be unique when rendering multiple listing tables
271
        """
272
        return self.form_id
273
274
    def get_catalog(self, default="portal_catalog"):
275
        """Get the catalog tool to be used in the listing
276
277
        :returns: ZCatalog tool
278
        """
279
        try:
280
            return api.get_tool(self.catalog)
281
        except api.BikaLIMSError:
282
            return api.get_tool(default)
283
284
    @view.memoize
285
    def get_catalog_indexes(self):
286
        """Return a list of registered catalog indexes
287
        """
288
        return self.get_catalog().indexes()
289
290
    @view.memoize
291
    def get_columns_indexes(self):
292
        """Returns a list of allowed sorting indexeds
293
        """
294
        columns = self.columns
295
        indexes = [v["index"] for k, v in columns.items() if "index" in v]
296
        return indexes
297
298
    @view.memoize
299
    def get_metadata_columns(self):
300
        """Get a list of all metadata column names
301
302
        :returns: List of catalog metadata column names
303
        """
304
        catalog = self.get_catalog()
305
        return catalog.schema()
306
307
    @view.memoize
308
    def translate_review_state(self, state, portal_type):
309
        """Translates the review state to the current set language
310
311
        :param state: Review state title
312
        :type state: basestring
313
        :returns: Translated review state title
314
        """
315
        ts = api.get_tool("translation_service")
316
        wf = api.get_tool("portal_workflow")
317
        state_title = wf.getTitleForStateOnType(state, portal_type)
318
        translated_state = ts.translate(
319
            _(state_title or state), context=self.request)
320
        logger.info(u"ListingView:translate_review_state: {} -> {} -> {}"
321
                    .format(state, state_title, translated_state))
322
        return translated_state
323
324
    def metadata_to_searchable_text(self, brain, key, value):
325
        """Parse the given metadata to text
326
327
        :param brain: ZCatalog Brain
328
        :param key: The name of the metadata column
329
        :param value: The raw value of the metadata column
330
        :returns: Searchable and translated unicode value or None
331
        """
332
        if not value:
333
            return u""
334
        if value is Missing.Value:
335
            return u""
336
        if api.is_uid(value):
337
            return u""
338
        if isinstance(value, (bool)):
339
            return u""
340
        if isinstance(value, (list, tuple)):
341
            for v in value:
342
                return self.metadata_to_searchable_text(brain, key, v)
343
        if isinstance(value, (dict)):
344
            for k, v in value.items():
345
                return self.metadata_to_searchable_text(brain, k, v)
346
        if self.is_date(value):
347
            return self.to_str_date(value)
348
        if "state" in key.lower():
349
            return self.translate_review_state(
350
                value, api.get_portal_type(brain))
351
        if not isinstance(value, basestring):
352
            value = str(value)
353
        return safe_unicode(value)
354
355
    def get_sort_order(self):
356
        """Get the sort_order criteria from the request or view
357
        """
358
        form_id = self.get_form_id()
359
        allowed = ["ascending", "descending"]
360
        sort_order = [self.request.form.get("{}_sort_order"
361
                                            .format(form_id), None),
362
                      self.contentFilter.get("sort_order", None)]
363
        sort_order = filter(lambda order: order in allowed, sort_order)
364
        return sort_order and sort_order[0] or "descending"
365
366
    def get_sort_on(self, default="created"):
367
        """Get the sort_on criteria to be used
368
369
        :param default: The default sort_on index to be used
370
        :returns: valid sort_on index or None
371
        """
372
        form_id = self.get_form_id()
373
        key = "{}_sort_on".format(form_id)
374
375
        # List of known catalog columns
376
        catalog_columns = self.get_metadata_columns()
377
378
        # The sort_on parameter from the request
379
        sort_on = self.request.form.get(key, None)
380
        # Use the index specified in the columns config
381
        if sort_on in self.columns:
382
            sort_on = self.columns[sort_on].get("index", sort_on)
383
384
        # Return immediately if the request sort_on parameter is found in the
385
        # catalog indexes
386
        if self.is_valid_sort_index(sort_on):
387
            return sort_on
388
389
        # Flag manual sorting if the request sort_on parameter is found in the
390
        # catalog metadata columns
391
        if sort_on in catalog_columns:
392
            self.manual_sort_on = sort_on
393
394
        # The sort_on parameter from the catalog query
395
        content_filter_sort_on = self.contentFilter.get("sort_on", None)
396
        if self.is_valid_sort_index(content_filter_sort_on):
397
            return content_filter_sort_on
398
399
        # The sort_on attribute from the instance
400
        instance_sort_on = self.sort_on
401
        if self.is_valid_sort_index(instance_sort_on):
402
            return instance_sort_on
403
404
        # The default sort_on
405
        if self.is_valid_sort_index(default):
406
            return default
407
408
        return None
409
410
    def is_valid_sort_index(self, sort_on):
411
        """Checks if the sort_on index is capable for a sort_
412
413
        :param sort_on: The name of the sort index
414
        :returns: True if the sort index is capable for sorting
415
        """
416
        # List of known catalog indexes
417
        catalog_indexes = self.get_catalog_indexes()
418
        if sort_on not in catalog_indexes:
419
            return False
420
        catalog = self.get_catalog()
421
        sort_index = catalog.Indexes.get(sort_on)
422
        if not hasattr(sort_index, "documentToKeyMap"):
423
            return False
424
        return True
425
426
    def is_date(self, thing):
427
        """checks if the passed in value is a date
428
429
        :param thing: an arbitrary object
430
        :returns: True if it can be converted to a date time object
431
        """
432
        if isinstance(thing, DateTime.DateTime):
433
            return True
434
        return False
435
436
    def to_str_date(self, date):
437
        """Converts the date to a string
438
439
        :param date: DateTime object or ISO date string
440
        :returns: locale date string
441
        """
442
        date = DateTime.DateTime(date)
443
        try:
444
            return date.strftime(self.date_format_long)
445
        except ValueError:
446
            return str(date)
447
448
    def get_pagesize(self):
449
        """Return the pagesize request parameter
450
        """
451
        form_id = self.get_form_id()
452
        pagesize = self.request.form.get(form_id + '_pagesize', self.pagesize)
453
        try:
454
            return int(pagesize)
455
        except (ValueError, TypeError):
456
            return self.pagesize
457
458
    def get_limit_from(self):
459
        """Return the limit_from request parameter
460
        """
461
        form_id = self.get_form_id()
462
        limit = self.request.form.get(form_id + '_limit_from', 0)
463
        try:
464
            return int(limit)
465
        except (ValueError, TypeError):
466
            return 0
467
468
    def get_path_query(self, context=None, level=0):
469
        """Return a path query
470
471
        :param context: The context to get the physical path from
472
        :param level: The depth level of the search
473
        :returns: Catalog path query
474
        """
475
        if context is None:
476
            context = self.context
477
        path = api.get_path(context)
478
        return {
479
            "path": {
480
                "query": path,
481
                "level": level,
482
            }
483
        }
484
485
    def get_item_info(self, brain_or_object):
486
        """Return the data of this brain or object
487
        """
488
        return {
489
            "obj": brain_or_object,
490
            "uid": api.get_uid(brain_or_object),
491
            "url": api.get_url(brain_or_object),
492
            "id": api.get_id(brain_or_object),
493
            "title": api.get_title(brain_or_object),
494
            "portal_type": api.get_portal_type(brain_or_object),
495
            "review_state": api.get_workflow_status_of(brain_or_object),
496
        }
497
498
    def get_catalog_query(self, searchterm=None):
499
        """Return the catalog query
500
501
        :param searchterm: Additional filter value to be added to the query
502
        :returns: Catalog query dictionary
503
        """
504
505
        # avoid to change the original content filter
506
        query = copy.deepcopy(self.contentFilter)
507
508
        # contentFilter is allowed in every self.review_state.
509
        for k, v in self.review_state.get("contentFilter", {}).items():
510
            query[k] = v
511
512
        # set the sort_on criteria
513
        sort_on = self.get_sort_on()
514
        if sort_on is not None:
515
            query["sort_on"] = sort_on
516
517
        # set the sort_order criteria
518
        query["sort_order"] = self.get_sort_order()
519
520
        # # Pass the searchterm as well to the Searchable Text index
521
        # if searchterm and isinstance(searchterm, basestring):
522
        #     query.update({"SearchableText": searchterm + "*"})
523
524
        logger.info(u"ListingView::get_catalog_query: query={}".format(query))
525
        return query
526
527
    def make_regex_for(self, searchterm, ignorecase=True):
528
        """Make a regular expression for the given searchterm
529
530
        :param searchterm: The searchterm for the regular expression
531
        :param ignorecase: Flag to compile with re.IGNORECASE
532
        :returns: Compiled regular expression
533
        """
534
        # searchterm comes in as a 8-bit string, e.g. 'D\xc3\xa4'
535
        # but must be a unicode u'D\xe4' to match the metadata
536
        searchterm = safe_unicode(searchterm)
537
        if ignorecase:
538
            return re.compile(searchterm, re.IGNORECASE)
539
        return re.compile(searchterm)
540
541
    def sort_brains(self, brains, sort_on=None):
542
        """Sort the brains
543
544
        :param brains: List of catalog brains
545
        :param sort_on: The metadata column name to sort on
546
        :returns: Manually sorted list of brains
547
        """
548
        if sort_on not in self.get_metadata_columns():
549
            logger.warn(
550
                "ListingView::sort_brains: '{}' not in metadata columns."
551
                .format(sort_on))
552
            return brains
553
554
        logger.warn(
555
            "ListingView::sort_brains: Manual sorting on metadata column '{}'."
556
            "Consider to add an explicit catalog index to speed up filtering."
557
            .format(self.manual_sort_on))
558
559
        # calculate the sort_order
560
        reverse = self.get_sort_order() == "descending"
561
562
        def metadata_sort(a, b):
563
            a = getattr(a, self.manual_sort_on, "")
564
            b = getattr(b, self.manual_sort_on, "")
565
            return cmp(safe_unicode(a), safe_unicode(b))
566
567
        return sorted(brains, cmp=metadata_sort, reverse=reverse)
568
569
    def get_searchterm(self):
570
        """Get the user entered search value from the request
571
572
        :returns: Current search box value from the request
573
        """
574
        form_id = self.get_form_id()
575
        key = "{}_filter".format(form_id)
576
        # we need to ensure unicode here
577
        return safe_unicode(self.request.form.get(key, ""))
578
579
    def metadata_search(self, catalog, query, searchterm, ignorecase=True):
580
        """ Retrieves all the brains from given catalog and returns the ones
581
        with at least one metadata containing the search term
582
        :param catalog: catalog to search
583
        :param query:
584
        :param searchterm:
585
        :param ignorecase:
586
        :return: brains matching search result
587
        """
588
        # create a catalog query
589
        logger.info(u"ListingView::search: Prepare metadata query for '{}'"
590
                    .format(self.catalog))
591
592
        brains = catalog(query)
593
594
        # Build a regular expression for the given searchterm
595
        regex = self.make_regex_for(searchterm, ignorecase=ignorecase)
596
597
        # Get the catalog metadata columns
598
        columns = self.get_metadata_columns()
599
600
        # Filter predicate to match each metadata value against the searchterm
601
        def match(brain):
602
            for column in columns:
603
                value = getattr(brain, column, None)
604
                parsed = self.metadata_to_searchable_text(brain, column, value)
605
                if regex.search(parsed):
606
                    return True
607
            return False
608
609
        # Filtered brains by searchterm -> metadata match
610
        return filter(match, brains)
611
612
    def ng3_index_search(self, catalog, query, searchterm):
613
        """Searches given catalog by query and also looks for a keyword in the
614
        specific index called "listing_searchable_text"
615
616
        #REMEMBER TextIndexNG indexes are the only indexes that wildcards can
617
        be used in the beginning of the string.
618
        http://zope.readthedocs.io/en/latest/zope2book/SearchingZCatalog.html#textindexng
619
620
        :param catalog: catalog to search
621
        :param query:
622
        :param searchterm: a keyword to look for in "listing_searchable_text"
623
        :return: brains matching the search result
624
        """
625
        logger.info(u"ListingView::search: Prepare NG3 index query for '{}'"
626
                    .format(self.catalog))
627
        # Remove quotation mark
628
        searchterm = searchterm.replace('"', '')
629
        # If the keyword is not encoded in searches, TextIndexNG3 encodes by
630
        # default encoding which we cannot always trust
631
        searchterm = searchterm.encode("utf-8")
632
        query["listing_searchable_text"] = "*" + searchterm + "*"
633
        return catalog(query)
634
635
    def _fetch_brains(self, idxfrom=0):
636
        """Fetch the catalog results for the current listing table state
637
        """
638
639
        searchterm = self.get_searchterm()
640
        brains = self.search(searchterm=searchterm)
641
        self.total = len(brains)
642
643
        # Return a subset of results, if necessary
644
        if idxfrom and len(brains) > idxfrom:
645
            return brains[idxfrom:self.pagesize + idxfrom]
646
        return brains[:self.pagesize]
647
648
    def search(self, searchterm="", ignorecase=True):
649
        """Search the catalog tool
650
651
        :param searchterm: The searchterm for the regular expression
652
        :param ignorecase: Flag to compile with re.IGNORECASE
653
        :returns: List of catalog brains
654
        """
655
656
        # TODO Append start and pagesize to return just that slice of results
657
658
        # start the timer for performance checks
659
        start = time.time()
660
661
        # strip whitespaces off the searchterm
662
        searchterm = searchterm.strip()
663
        # Strip illegal characters of the searchterm
664
        searchterm = searchterm.strip(u"*.!$%&/()=-+:'`´^")
665
        logger.info(u"ListingView::search:searchterm='{}'".format(searchterm))
666
667
        # create a catalog query
668
        logger.info(u"ListingView::search: Prepare catalog query for '{}'"
669
                    .format(self.catalog))
670
        query = self.get_catalog_query(searchterm=searchterm)
671
672
        # search the catalog
673
        catalog = api.get_tool(self.catalog)
674
675
        # return the unfiltered catalog results if no searchterm
676
        if not searchterm:
677
            brains = catalog(query)
678
679
        # check if there is ng3 index in the catalog to query by wildcards
680
        elif "listing_searchable_text" in catalog.indexes():
681
            # Always expand all categories if we have a searchterm
682
            self.expand_all_categories = True
683
            brains = self.ng3_index_search(catalog, query, searchterm)
684
685
        else:
686
            self.expand_all_categories = True
687
            brains = self.metadata_search(
688
                catalog, query, searchterm, ignorecase)
689
690
        # Sort manually?
691
        if self.manual_sort_on is not None:
692
            brains = self.sort_brains(brains, sort_on=self.manual_sort_on)
693
694
        end = time.time()
695
        logger.info(u"ListingView::search: Search for '{}' executed in "
696
                    u"{:.2f}s ({} matches)"
697
                    .format(searchterm, end - start, len(brains)))
698
        return brains
699
700
    def isItemAllowed(self, obj):
701
        """ return if the item can be added to the items list.
702
        """
703
        return True
704
705
    def folderitem(self, obj, item, index):
706
        """Service triggered each time an item is iterated in folderitems.
707
708
        The use of this service prevents the extra-loops in child objects.
709
710
        :obj: the instance of the class to be foldered
711
        :item: dict containing the properties of the object to be used by
712
            the template
713
        :index: current index of the item
714
        """
715
        return item
716
717
    def folderitems(self, full_objects=False, classic=True):
718
        """This function returns an array of dictionaries where each dictionary
719
        contains the columns data to render the list.
720
721
        No object is needed by default. We should be able to get all
722
        the listing columns taking advantage of the catalog's metadata,
723
        so that the listing will be much more faster. If a very specific
724
        info has to be retrieve from the objects, we can define
725
        full_objects as True but performance can be lowered.
726
727
        :full_objects: a boolean, if True, each dictionary will contain an item
728
                       with the object itself. item.get('obj') will return a
729
                       object. Only works with the 'classic' way.
730
        WARNING: :full_objects: could create a big performance hit!
731
        :classic: if True, the old way folderitems works will be executed. This
732
                  function is mainly used to maintain the integrity with the
733
                  old version.
734
        """
735
        # Getting a security manager instance for the current request
736
        self.security_manager = getSecurityManager()
737
        self.workflow = getToolByName(self.context, 'portal_workflow')
738
739
        if classic:
740
            return self._folderitems(full_objects)
741
742
        # idx increases one unit each time an object is added to the 'items'
743
        # dictionary to be returned. Note that if the item is not rendered,
744
        # the idx will not increase.
745
        idx = 0
746
        results = []
747
        self.show_more = False
748
        brains = self._fetch_brains(self.limit_from)
749
        for obj in brains:
750
            # avoid creating unnecessary info for items outside the current
751
            # batch;  only the path is needed for the "select all" case...
752
            # we only take allowed items into account
753
            if idx >= self.pagesize:
754
                # Maximum number of items to be shown reached!
755
                self.show_more = True
756
                break
757
758
            # check if the item must be rendered or not (prevents from
759
760
            # doing it later in folderitems) and dealing with paging
761
            if not obj or not self.isItemAllowed(obj):
762
                continue
763
764
            # Get the css for this row in accordance with the obj's state
765
            states = obj.getObjectWorkflowStates
766
            if not states:
767
                states = {}
768
            state_class = ['state-{0}'.format(v) for v in states.values()]
769
            state_class = ' '.join(state_class)
770
771
            # Building the dictionary with basic items
772
            results_dict = dict(
773
                # To colour the list items by state
774
                state_class=state_class,
775
                # a list of names of fields that may be edited on this item
776
                allow_edit=[],
777
                # a dict where the column name works as a key and the value is
778
                # the name of the field related with the column. It is used
779
                # when the name given to the column and the content field it
780
                # represents diverges. bika_listing_table_items.pt defines an
781
                # attribute for each item, this attribute is named 'field' and
782
                # the system fills it taking advantage of this dictionary or
783
                # the name of the column if it isn't defined in the dict.
784
                field={},
785
                # "before", "after" and replace: dictionary (key is column ID)
786
                # A snippet of HTML which will be rendered
787
                # before/after/instead of the table cell content.
788
                before={},  # { before : "<a href=..>" }
789
                after={},
790
                replace={},
791
                choices={},
792
            )
793
794
            # update with the base item info
795
            results_dict.update(self.get_item_info(obj))
796
797
            # Set states and state titles
798
            ptype = obj.portal_type
799
            workflow = api.get_tool('portal_workflow')
800
            for state_var, state in states.items():
801
                results_dict[state_var] = state
802
                state_title = self.state_titles.get(state, None)
803
                if not state_title:
804
                    state_title = workflow.getTitleForStateOnType(state, ptype)
805
                    if state_title:
806
                        self.state_titles[state] = state_title
807
                if state_title and state == obj.review_state:
808
                    results_dict['state_title'] = _(state_title)
809
810
            # extra classes for individual fields on this item
811
            # { field_id : "css classes" }
812
            results_dict['class'] = {}
813
814
            # Search for values for all columns in obj
815
            for key in self.columns.keys():
816
                # if the key is already in the results dict
817
                # then we don't replace it's value
818
                value = results_dict.get(key, '')
819 View Code Duplication
                if not value:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
820
                    attrobj = getFromString(obj, key)
821
                    value = attrobj if attrobj else value
822
823
                    # Custom attribute? Inspect to set the value
824
                    # for the current column dynamically
825
                    vattr = self.columns[key].get('attr', None)
826
                    if vattr:
827
                        attrobj = getFromString(obj, vattr)
828
                        value = attrobj if attrobj else value
829
                    results_dict[key] = value
830
                # Replace with an url?
831
                replace_url = self.columns[key].get('replace_url', None)
832
                if replace_url:
833
                    attrobj = getFromString(obj, replace_url)
834
                    if attrobj:
835
                        results_dict['replace'][key] = \
836
                            '<a href="%s">%s</a>' % (attrobj, value)
837
            # The item basics filled. Delegate additional actions to folderitem
838
            # service. folderitem service is frequently overriden by child
839
            # objects
840
            item = self.folderitem(obj, results_dict, idx)
841
            if item:
842
                results.append(item)
843
                idx += 1
844
        return results
845
846
    @deprecated("Using bikalisting.folderitems(classic=True) is very slow")
847
    def _folderitems(self, full_objects=False):
848
        """WARNING: :full_objects: could create a big performance hit.
849
        """
850
        # Setting up some attributes
851
        plone_layout = getMultiAdapter((self.context.aq_inner, self.request),
852
                                       name=u'plone_layout')
853
        plone_utils = getToolByName(self.context.aq_inner, 'plone_utils')
854
        portal_types = getToolByName(self.context.aq_inner, 'portal_types')
855
        if self.request.form.get('show_all', '').lower() == 'true' \
856
                or self.show_all is True \
857
                or self.pagesize == 0:
858
            show_all = True
859
        else:
860
            show_all = False
861
862
        # idx increases one unit each time an object is added to the 'items'
863
        # dictionary to be returned. Note that if the item is not rendered,
864
        # the idx will not increase.
865
        idx = 0
866
        results = []
867
        self.show_more = False
868
        brains = self._fetch_brains(self.limit_from)
869
        for obj in brains:
870
            # avoid creating unnecessary info for items outside the current
871
            # batch;  only the path is needed for the "select all" case...
872
            # we only take allowed items into account
873
            if not show_all and idx >= self.pagesize:
874
                # Maximum number of items to be shown reached!
875
                self.show_more = True
876
                break
877
878
            # we don't know yet if it's a brain or an object
879
            path = hasattr(obj, 'getPath') and obj.getPath() or \
880
                "/".join(obj.getPhysicalPath())
881
882
            # This item must be rendered, we need the object instead of a brain
883
            obj = obj.getObject() if hasattr(obj, 'getObject') else obj
884
885
            # check if the item must be rendered or not (prevents from
886
            # doing it later in folderitems) and dealing with paging
887
            if not obj or not self.isItemAllowed(obj):
888
                continue
889
890
            uid = obj.UID()
891
            title = obj.Title()
892
            description = obj.Description()
893
            icon = plone_layout.getIcon(obj)
894
            url = obj.absolute_url()
895
            relative_url = obj.absolute_url(relative=True)
896
897
            fti = portal_types.get(obj.portal_type)
898
            if fti is not None:
899
                type_title_msgid = fti.Title()
900
            else:
901
                type_title_msgid = obj.portal_type
902
903
            url_href_title = '%s at %s: %s' % (
904
                t(type_title_msgid),
905
                path,
906
                to_utf8(description))
907
908
            modified = self.ulocalized_time(obj.modified()),
909
910
            # element css classes
911
            type_class = 'contenttype-' + \
912
                         plone_utils.normalizeString(obj.portal_type)
913
914
            state_class = ''
915
            states = {}
916
            for w in self.workflow.getWorkflowsFor(obj):
917
                state = w._getWorkflowStateOf(obj).id
918
                states[w.state_var] = state
919
                state_class += "state-%s " % state
920
921
            results_dict = dict(
922
                obj=obj,
923
                id=obj.getId(),
924
                title=title,
925
                uid=uid,
926
                path=path,
927
                url=url,
928
                fti=fti,
929
                item_data=json.dumps([]),
930
                url_href_title=url_href_title,
931
                obj_type=obj.Type,
932
                size=obj.getObjSize,
933
                modified=modified,
934
                icon=icon.html_tag(),
935
                type_class=type_class,
936
                # a list of lookups for single-value-select fields
937
                choices={},
938
                state_class=state_class,
939
                relative_url=relative_url,
940
                view_url=url,
941
                table_row_class="",
942
                category='None',
943
944
                # a list of names of fields that may be edited on this item
945
                allow_edit=[],
946
947
                # a list of names of fields that are compulsory (if editable)
948
                required=[],
949
                # a dict where the column name works as a key and the value is
950
                # the name of the field related with the column. It is used
951
                # when the name given to the column and the content field it
952
                # represents diverges. bika_listing_table_items.pt defines an
953
                # attribute for each item, this attribute is named 'field' and
954
                # the system fills it taking advantage of this dictionary or
955
                # the name of the column if it isn't defined in the dict.
956
                field={},
957
                # "before", "after" and replace: dictionary (key is column ID)
958
                # A snippet of HTML which will be rendered
959
                # before/after/instead of the table cell content.
960
                before={},  # { before : "<a href=..>" }
961
                after={},
962
                replace={},
963
            )
964
965
            rs = None
966
            wf_state_var = None
967
968
            workflows = self.workflow.getWorkflowsFor(obj)
969
            for wf in workflows:
970
                if wf.state_var:
971
                    wf_state_var = wf.state_var
972
                    break
973
974
            if wf_state_var is not None:
975
                rs = self.workflow.getInfoFor(obj, wf_state_var)
976
                st_title = self.workflow.getTitleForStateOnType(
977
                    rs, obj.portal_type)
978
                st_title = t(_(st_title))
979
980
            if rs:
981
                results_dict['review_state'] = rs
982
983
            for state_var, state in states.items():
984
                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...
985
                    st_title = self.workflow.getTitleForStateOnType(
986
                        state, obj.portal_type)
987
                results_dict[state_var] = state
988
            results_dict['state_title'] = st_title
989
990
            results_dict['class'] = {}
991
992
            # As far as I am concerned, adapters for IFieldIcons are only used
993
            # for Analysis content types. Since AnalysesView is not using this
994
            # "classic" folderitems from bikalisting anymore, this logic has
995
            # been added in AnalysesView. Even though, this logic hasn't been
996
            # removed from here, cause this _folderitems function is marked as
997
            # deprecated, so it will be eventually removed alltogether.
998
            for name, adapter in getAdapters((obj,), IFieldIcons):
999
                auid = obj.UID() if hasattr(obj, 'UID') and callable(
1000
                    obj.UID) else None
1001
                if not auid:
1002
                    continue
1003
                alerts = adapter()
1004
                # logger.info(str(alerts))
1005
                if alerts and auid in alerts:
1006
                    if auid in self.field_icons:
1007
                        self.field_icons[auid].extend(alerts[auid])
1008
                    else:
1009
                        self.field_icons[auid] = alerts[auid]
1010
1011
            # Search for values for all columns in obj
1012
            for key in self.columns.keys():
1013
                # if the key is already in the results dict
1014
                # then we don't replace it's value
1015
                value = results_dict.get(key, '')
1016 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...
1017
                    attrobj = getFromString(obj, key)
1018
                    value = attrobj if attrobj else value
1019
1020
                    # Custom attribute? Inspect to set the value
1021
                    # for the current column dinamically
1022
                    vattr = self.columns[key].get('attr', None)
1023
                    if vattr:
1024
                        attrobj = getFromString(obj, vattr)
1025
                        value = attrobj if attrobj else value
1026
                    results_dict[key] = value
1027
1028
                # Replace with an url?
1029
                replace_url = self.columns[key].get('replace_url', None)
1030
                if replace_url:
1031
                    attrobj = getFromString(obj, replace_url)
1032
                    if attrobj:
1033
                        results_dict['replace'][key] = \
1034
                            '<a href="%s">%s</a>' % (attrobj, value)
1035
1036
            # The item basics filled. Delegate additional actions to folderitem
1037
            # service. folderitem service is frequently overriden by child
1038
            # objects
1039
            item = self.folderitem(obj, results_dict, idx)
1040
            if item:
1041
                results.append(item)
1042
                idx += 1
1043
1044
        return results
1045