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