| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2
3 """ Framework for filtered REST requests
4
5 @copyright: 2013-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 @requires: U{B{I{gluon}} <http://web2py.com>}
9
10 Permission is hereby granted, free of charge, to any person
11 obtaining a copy of this software and associated documentation
12 files (the "Software"), to deal in the Software without
13 restriction, including without limitation the rights to use,
14 copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the
16 Software is furnished to do so, subject to the following
17 conditions:
18
19 The above copyright notice and this permission notice shall be
20 included in all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 OTHER DEALINGS IN THE SOFTWARE.
30 """
31
32 __all__ = ("S3FilterWidget",
33 "S3AgeFilter",
34 "S3DateFilter",
35 "S3HierarchyFilter",
36 "S3LocationFilter",
37 "S3MapFilter",
38 "S3OptionsFilter",
39 "S3RangeFilter",
40 "S3SliderFilter",
41 "S3TextFilter",
42 "S3NotEmptyFilter",
43 "S3FilterForm",
44 "S3Filter",
45 "S3FilterString",
46 "s3_get_filter_opts",
47 "s3_set_default_filter",
48 )
49
50 import datetime
51 import json
52 import re
53
54 from collections import OrderedDict
55
56 from gluon import current, URL, A, DIV, FORM, INPUT, LABEL, OPTION, SELECT, \
57 SPAN, TABLE, TAG, TBODY, IS_EMPTY_OR, IS_FLOAT_IN_RANGE, \
58 IS_INT_IN_RANGE, IS_IN_SET
59 from gluon.storage import Storage
60 from gluon.tools import callback
61
62 from s3dal import Field
63 from s3datetime import s3_decode_iso_datetime, S3DateTime
64 from s3query import FS, S3ResourceField, S3ResourceQuery, S3URLQuery
65 from s3rest import S3Method
66 from s3timeplot import S3TimeSeries
67 from s3utils import s3_get_foreign_key, s3_str, s3_unicode, S3TypeConverter
68 from s3validators import IS_UTC_DATE
69 from s3widgets import ICON, S3CalendarWidget, S3CascadeSelectWidget, \
70 S3GroupedOptionsWidget, S3HierarchyWidget, \
71 S3MultiSelectWidget
72
73 # Compact JSON encoding
74 SEPARATORS = (",", ":")
75
76 # =============================================================================
77 -class S3FilterWidget(object):
78 """ Filter widget for interactive search forms (base class) """
79
80 #: the HTML class for the widget type
81 _class = "generic-filter"
82
83 #: the default query operator(s) for the widget type
84 operator = None
85
86 #: alternatives for client-side changeable operators
87 alternatives = None
88
89 # -------------------------------------------------------------------------
91 """
92 Prototype method to render this widget as an instance of
93 a web2py HTML helper class, to be implemented by subclasses.
94
95 @param resource: the S3Resource to render with widget for
96 @param values: the values for this widget from the URL query
97 """
98
99 raise NotImplementedError
100
101 # -------------------------------------------------------------------------
103 """
104 Prototype method to generate the name for the URL query variable
105 for this widget, can be overwritten in subclasses.
106
107 @param resource: the resource
108 @return: the URL query variable name (or list of
109 variable names if there are multiple operators)
110 """
111
112 opts = self.opts
113
114 if "selector" in opts:
115 # Override selector
116 label, selector = None, opts["selector"]
117 else:
118 label, selector = self._selector(resource, self.field)
119 self.selector = selector
120
121 if not selector:
122 return None
123
124 if self.alternatives and get_vars is not None:
125 # Get the actual operator from get_vars
126 operator = self._operator(get_vars, selector)
127 if operator:
128 self.operator = operator
129
130 if "label" not in self.opts:
131 self.opts["label"] = label
132
133 return self._variable(selector, self.operator)
134
135 # -------------------------------------------------------------------------
137 """
138 Prototype method to construct the hidden element that holds the
139 URL query term corresponding to an input element in the widget.
140
141 @param variable: the URL query variable
142 """
143
144 if type(variable) is list:
145 variable = "&".join(variable)
146 return INPUT(_type="hidden",
147 _id="%s-data" % self.attr["_id"],
148 _class="filter-widget-data %s-data" % self._class,
149 _value=variable)
150
151 # -------------------------------------------------------------------------
152 # Helper methods
153 #
155 """
156 Constructor to configure the widget
157
158 @param field: the selector(s) for the field(s) to filter by
159 @param attr: configuration options for this widget
160
161 Common configuration options:
162
163 @keyword label: label for the widget
164 @keyword comment: comment for the widget
165 @keyword hidden: render widget initially hidden
166 (="advanced" option)
167
168 - other options see subclasses
169 """
170
171 self.field = field
172 self.alias = None
173
174 attributes = Storage()
175 options = Storage()
176 for k, v in attr.iteritems():
177 if k[0] == "_":
178 attributes[k] = v
179 else:
180 options[k] = v
181 self.attr = attributes
182 self.opts = options
183
184 self.selector = None
185 self.values = Storage()
186
187 # -------------------------------------------------------------------------
189 """
190 Entry point for the form builder
191
192 @param resource: the S3Resource to render the widget for
193 @param get_vars: the GET vars (URL query vars) to prepopulate
194 the widget
195 @param alias: the resource alias to use
196 """
197
198 self.alias = alias
199
200 # Initialize the widget attributes
201 self._attr(resource)
202
203 # Extract the URL values to populate the widget
204 variable = self.variable(resource, get_vars)
205
206 defaults = {}
207 for k, v in self.values.items():
208 selector = self._prefix(k)
209 defaults[selector] = v
210
211 if type(variable) is list:
212 values = Storage()
213 for k in variable:
214 if k in defaults:
215 values[k] = defaults[k]
216 else:
217 values[k] = self._values(get_vars, k)
218 else:
219 if variable in defaults:
220 values = defaults[variable]
221 else:
222 values = self._values(get_vars, variable)
223
224 # Construct and populate the widget
225 widget = self.widget(resource, values)
226
227 # Recompute variable in case operator got changed in widget()
228 if self.alternatives:
229 variable = self._variable(self.selector, self.operator)
230
231 # Construct the hidden data element
232 data = self.data_element(variable)
233
234 if type(data) is list:
235 data.append(widget)
236 else:
237 data = [data, widget]
238 return TAG[""](*data)
239
240 # -------------------------------------------------------------------------
242 """ Initialize and return the HTML attributes for this widget """
243
244 _class = self._class
245
246 # Construct name and id for the widget
247 attr = self.attr
248 if "_name" not in attr:
249 if not resource:
250 raise SyntaxError("%s: _name parameter required " \
251 "when rendered without resource." % \
252 self.__class__.__name__)
253 flist = self.field
254 if not isinstance(flist, (list, tuple)):
255 flist = [flist]
256 colnames = []
257 for f in flist:
258 rfield = S3ResourceField(resource, f)
259 colname = rfield.colname
260 if colname:
261 colnames.append(colname)
262 else:
263 colnames.append(rfield.fname)
264 name = "%s-%s-%s" % (resource.alias, "-".join(colnames), _class)
265 attr["_name"] = name.replace(".", "_")
266 if "_id" not in attr:
267 attr["_id"] = attr["_name"]
268
269 return attr
270
271 # -------------------------------------------------------------------------
272 @classmethod
274 """
275 Helper method to get the operators from the URL query
276
277 @param get_vars: the GET vars (a dict)
278 @param selector: field selector
279
280 @return: query operator - None, str or list
281 """
282
283 variables = ["%s__%s" % (selector, op) for op in cls.alternatives]
284 slen = len(selector) + 2
285
286 operators = [k[slen:] for k in get_vars if k in variables]
287 if not operators:
288 return None
289 elif len(operators) == 1:
290 return operators[0]
291 else:
292 return operators
293
294 # -------------------------------------------------------------------------
296 """
297 Helper method to prefix an unprefixed field selector
298
299 @param alias: the resource alias to use as prefix
300 @param selector: the field selector
301
302 @return: the prefixed selector
303 """
304
305 alias = self.alias
306 items = selector.split("$", 0)
307 head = items[0]
308 if "." in head:
309 if alias not in (None, "~"):
310 prefix, key = head.split(".", 1)
311 if prefix == "~":
312 prefix = alias
313 elif prefix != alias:
314 prefix = "%s.%s" % (alias, prefix)
315 items[0] = "%s.%s" % (prefix, key)
316 selector = "$".join(items)
317 else:
318 if alias is None:
319 alias = "~"
320 selector = "%s.%s" % (alias, selector)
321 return selector
322
323 # -------------------------------------------------------------------------
325 """
326 Helper method to generate a filter query selector for the
327 given field(s) in the given resource.
328
329 @param resource: the S3Resource
330 @param fields: the field selectors (as strings)
331
332 @return: the field label and the filter query selector, or None
333 if none of the field selectors could be resolved
334 """
335
336 prefix = self._prefix
337 label = None
338
339 if not fields:
340 return label, None
341 if not isinstance(fields, (list, tuple)):
342 fields = [fields]
343 selectors = []
344 for field in fields:
345 if resource:
346 try:
347 rfield = S3ResourceField(resource, field)
348 except (AttributeError, TypeError):
349 continue
350 if not rfield.field and not rfield.virtual:
351 # Unresolvable selector
352 continue
353 if not label:
354 label = rfield.label
355 selectors.append(prefix(rfield.selector))
356 else:
357 selectors.append(field)
358 if selectors:
359 return label, "|".join(selectors)
360 else:
361 return label, None
362
363 # -------------------------------------------------------------------------
364 @staticmethod
366 """
367 Helper method to get all values of a URL query variable
368
369 @param get_vars: the GET vars (a dict)
370 @param variable: the name of the query variable
371
372 @return: a list of values
373 """
374
375 if not variable:
376 return []
377 elif variable in get_vars:
378 values = S3URLQuery.parse_value(get_vars[variable])
379 if not isinstance(values, (list, tuple)):
380 values = [values]
381 return values
382 else:
383 return []
384
385 # -------------------------------------------------------------------------
386 @classmethod
388 """
389 Construct URL query variable(s) name from a filter query
390 selector and the given operator(s)
391
392 @param selector: the selector
393 @param operator: the operator (or tuple/list of operators)
394
395 @return: the URL query variable name (or list of variable names)
396 """
397
398 if isinstance(operator, (tuple, list)):
399 return [cls._variable(selector, o) for o in operator]
400 elif operator:
401 return "%s__%s" % (selector, operator)
402 else:
403 return selector
404
405 # =============================================================================
406 -class S3TextFilter(S3FilterWidget):
407 """
408 Text filter widget
409
410 Configuration options:
411
412 @keyword label: label for the widget
413 @keyword comment: comment for the widget
414 @keyword hidden: render widget initially hidden (="advanced" option)
415 @keyword match_any: match any of the strings
416 """
417
418 _class = "text-filter"
419
420 operator = "like"
421
422 # -------------------------------------------------------------------------
424 """
425 Render this widget as HTML helper object(s)
426
427 @param resource: the resource
428 @param values: the search values from the URL query
429 """
430
431 attr = self.attr
432
433 if "_size" not in attr:
434 attr.update(_size="40")
435 if "_class" in attr and attr["_class"]:
436 _class = "%s %s" % (attr["_class"], self._class)
437 else:
438 _class = self._class
439 attr["_class"] = _class
440 attr["_type"] = "text"
441
442 # Match any or all of the strings entered?
443 data = attr.get("data", {})
444 data["match"] = "any" if self.opts.get("match_any") else "all"
445 attr["data"] = data
446
447 values = [v.strip("*") for v in values if v is not None]
448 if values:
449 attr["_value"] = " ".join(values)
450
451 return INPUT(**attr)
452
453 # =============================================================================
454 -class S3RangeFilter(S3FilterWidget):
455 """
456 Numerical Range Filter Widget
457
458 Configuration options:
459
460 @keyword label: label for the widget
461 @keyword comment: comment for the widget
462 @keyword hidden: render widget initially hidden (="advanced" option)
463 """
464
465 # Overall class
466 _class = "range-filter"
467 # Class for visible input boxes.
468 _input_class = "%s-%s" % (_class, "input")
469
470 operator = ["ge", "le"]
471
472 # Untranslated labels for individual input boxes.
473 input_labels = {"ge": "Minimum", "le": "Maximum"}
474
475 # -------------------------------------------------------------------------
477 """
478 Overrides S3FilterWidget.data_element(), constructs multiple
479 hidden INPUTs (one per variable) with element IDs of the form
480 <id>-<operator>-data (where no operator is translated as "eq").
481
482 @param variables: the variables
483 """
484
485 if variables is None:
486 operators = self.operator
487 if type(operators) is not list:
488 operators = [operators]
489 variables = self._variable(self.selector, operators)
490 else:
491 # Split the operators off the ends of the variables.
492 if type(variables) is not list:
493 variables = [variables]
494 parse_key = S3URLQuery.parse_key
495 operators = [parse_key(v)[1] for v in variables]
496
497 elements = []
498 widget_id = self.attr["_id"]
499
500 for o, v in zip(operators, variables):
501 elements.append(
502 INPUT(_type="hidden",
503 _id="%s-%s-data" % (widget_id, o),
504 _class="filter-widget-data %s-data" % self._class,
505 _value=v))
506
507 return elements
508
509 # -------------------------------------------------------------------------
511 """
512 Method to Ajax-retrieve the current options of this widget
513
514 @param resource: the S3Resource
515 """
516
517 minimum, maximum = self._options(resource)
518
519 attr = self._attr(resource)
520 options = {attr["_id"]: {"min": minimum,
521 "max": maximum,
522 }}
523 return options
524
525 # -------------------------------------------------------------------------
527 """
528 Helper function to retrieve the current options for this
529 filter widget
530
531 @param resource: the S3Resource
532 """
533
534 # Find only values linked to records the user is
535 # permitted to read, and apply any resource filters
536 # (= use the resource query)
537 query = resource.get_query()
538
539 # Must include rfilter joins when using the resource
540 # query (both inner and left):
541 rfilter = resource.rfilter
542 if rfilter:
543 join = rfilter.get_joins()
544 left = rfilter.get_joins(left=True)
545 else:
546 join = left = None
547
548 rfield = S3ResourceField(resource, self.field)
549 field = rfield.field
550
551 row = current.db(query).select(field.min(),
552 field.max(),
553 join=join,
554 left=left,
555 ).first()
556
557 minimum = row[field.min()]
558 maximum = row[field.max()]
559
560 return minimum, maximum
561
562 # -------------------------------------------------------------------------
564 """
565 Render this widget as HTML helper object(s)
566
567 @param resource: the resource
568 @param values: the search values from the URL query
569 """
570
571 T = current.T
572
573 attr = self.attr
574 _class = self._class
575 if "_class" in attr and attr["_class"]:
576 _class = "%s %s" % (attr["_class"], _class)
577 else:
578 _class = _class
579 attr["_class"] = _class
580
581 input_class = self._input_class
582 input_labels = self.input_labels
583 input_elements = DIV()
584 ie_append = input_elements.append
585
586 _id = attr["_id"]
587 _variable = self._variable
588 selector = self.selector
589
590 for operator in self.operator:
591
592 input_id = "%s-%s" % (_id, operator)
593
594 input_box = INPUT(_name=input_id,
595 _id=input_id,
596 _type="text",
597 _class=input_class)
598
599 variable = _variable(selector, operator)
600
601 # Populate with the value, if given
602 # if user has not set any of the limits, we get [] in values.
603 value = values.get(variable, None)
604 if value not in [None, []]:
605 if type(value) is list:
606 value = value[0]
607 input_box["_value"] = value
608 input_box["value"] = value
609
610 ie_append(DIV(DIV(LABEL("%s:" % T(input_labels[operator]),
611 _for = input_id,
612 ),
613 _class = "range-filter-label",
614 ),
615 DIV(input_box,
616 _class = "range-filter-widget",
617 ),
618 _class = "range-filter-field",
619 ))
620
621 return input_elements
622
623 # =============================================================================
624 -class S3AgeFilter(S3RangeFilter):
625
626 _class = "age-filter"
627
628 # Class for visible input boxes.
629 _input_class = "%s-%s" % (_class, "input")
630
631 operator = ["le", "gt"]
632
633 # Untranslated labels for individual input boxes.
634 input_labels = {"le": "", "gt": "To"}
635
636 # -------------------------------------------------------------------------
638 """
639 Render this widget as HTML helper object(s)
640
641 @param resource: the resource
642 @param values: the search values from the URL query
643 """
644
645 T = current.T
646
647 attr = self.attr
648 _class = self._class
649 if "_class" in attr and attr["_class"]:
650 _class = "%s %s" % (attr["_class"], _class)
651 else:
652 _class = _class
653 attr["_class"] = _class
654
655 input_class = self._input_class
656 input_labels = self.input_labels
657 input_elements = DIV()
658 ie_append = input_elements.append
659
660 _id = attr["_id"]
661 _variable = self._variable
662 selector = self.selector
663
664 opts = self.opts
665 minimum = opts.get("minimum", 0)
666 maximum = opts.get("maximum", 120)
667
668 for operator in self.operator:
669
670 input_id = "%s-%s" % (_id, operator)
671
672 # Selectable options
673 input_opts = [OPTION("%s" % i, value=i)
674 for i in range(minimum, maximum + 1)
675 ]
676 input_opts.insert(0, OPTION("", value=""))
677
678 # Input Element
679 input_box = SELECT(input_opts,
680 _id = input_id,
681 _class = input_class,
682 )
683
684 variable = _variable(selector, operator)
685
686 # Populate with the value, if given
687 # if user has not set any of the limits, we get [] in values.
688 value = values.get(variable, None)
689 if value not in [None, []]:
690 if type(value) is list:
691 value = value[0]
692 input_box["_value"] = value
693 input_box["value"] = value
694
695 label = input_labels[operator]
696 if label:
697 label = DIV(LABEL("%s:" % T(input_labels[operator]),
698 _for = input_id,
699 ),
700 _class = "age-filter-label",
701 )
702
703 ie_append(DIV(label,
704 DIV(input_box,
705 _class = "age-filter-widget",
706 ),
707 _class = "range-filter-field",
708 ))
709
710 ie_append(DIV(LABEL(T("Years")),
711 _class = "age-filter-unit",
712 # TODO move style into CSS
713 #_style = "float:left;margin-top:1.2rem;vertical-align:text-bottom",
714 ))
715
716 return input_elements
717
718 # =============================================================================
719 -class S3DateFilter(S3RangeFilter):
720 """
721 Date Range Filter Widget
722 - use a single field or a pair of fields for start_date/end_date
723
724 Configuration options:
725
726 @keyword label: label for the widget
727 @keyword comment: comment for the widget
728 @keyword hidden: render widget initially hidden (="advanced" option)
729
730 @keyword fieldtype: explicit field type "date" or "datetime" to
731 use for context or virtual fields
732 @keyword hide_time: don't show time selector
733 """
734
735 _class = "date-filter"
736
737 # Class for visible input boxes.
738 _input_class = "%s-%s" % (_class, "input")
739
740 operator = ["ge", "le"]
741
742 # Untranslated labels for individual input boxes.
743 input_labels = {"ge": "From", "le": "To"}
744
745 # -------------------------------------------------------------------------
747 """
748 Overrides S3FilterWidget.data_element(), constructs multiple
749 hidden INPUTs (one per variable) with element IDs of the form
750 <id>-<operator>-data (where no operator is translated as "eq").
751
752 @param variables: the variables
753 """
754
755 fields = self.field
756 if type(fields) is not list:
757 # Use function from S3RangeFilter parent class
758 return super(S3DateFilter, self).data_element(variables)
759
760 selectors = self.selector.split("|")
761 operators = self.operator
762
763 elements = []
764 _id = self.attr["_id"]
765
766 start = True
767 for selector in selectors:
768 if start:
769 operator = operators[0]
770 start = False
771 else:
772 operator = operators[1]
773 variable = self._variable(selector, [operator])[0]
774
775 elements.append(
776 INPUT(_type = "hidden",
777 _id = "%s-%s-data" % (_id, operator),
778 _class = "filter-widget-data %s-data" % self._class,
779 _value = variable))
780
781 return elements
782
783 # -------------------------------------------------------------------------
785 """
786 Method to Ajax-retrieve the current options of this widget
787
788 @param resource: the S3Resource
789 """
790
791 # Introspective range?
792 auto_range = self.opts.get("auto_range")
793 if auto_range is None:
794 # Not specified for widget => apply global setting
795 auto_range = current.deployment_settings.get_search_dates_auto_range()
796
797 if auto_range:
798 minimum, maximum, ts = self._options(resource)
799
800 attr = self._attr(resource)
801 options = {attr["_id"]: {"min": minimum,
802 "max": maximum,
803 "ts": ts,
804 }}
805 else:
806 options = {}
807
808 return options
809
810 # -------------------------------------------------------------------------
812 """
813 Helper function to retrieve the current options for this
814 filter widget
815
816 @param resource: the S3Resource
817 @param as_str: return date as ISO-formatted string not raw DateTime
818 """
819
820 # Find only values linked to records the user is
821 # permitted to read, and apply any resource filters
822 # (= use the resource query)
823 query = resource.get_query()
824
825 # Must include rfilter joins when using the resource
826 # query (both inner and left):
827 rfilter = resource.rfilter
828 if rfilter:
829 join = rfilter.get_joins()
830 left = rfilter.get_joins(left=True)
831 else:
832 join = left = None
833
834 fields = self.field
835 if type(fields) is list:
836 # Separate start_date & end_date
837 # Q: How should we handle NULL end_date?
838 # A: Ignore & just provide constraints that provide differentiation?
839 # B: Allow scrolling arbitrarily into the end space?
840 # Going with (A) for now
841 # If wanting to do (B) then can coalesce a long end_date to the NULLs:
842 # http://stackoverflow.com/questions/21286215/how-can-i-include-null-values-in-a-min-or-max
843 # http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#Default-values-with-coalesce-and-coalesce_zero
844 # or can simply do a 2nd query to check for NULLs
845 start_field = S3ResourceField(resource, fields[0]).field
846 end_field = S3ResourceField(resource, fields[1]).field
847 row = current.db(query).select(start_field.min(),
848 start_field.max(),
849 end_field.max(),
850 join = join,
851 left = left,
852 ).first()
853 minimum = row[start_field.min()]
854 maximum = row[start_field.max()]
855 end_max = row[end_field.max()]
856 if end_max:
857 maximum = max(maximum, end_max)
858 else:
859 rfield = S3ResourceField(resource, fields)
860 field = rfield.field
861 row = current.db(query).select(field.min(),
862 field.max(),
863 join = join,
864 left = left,
865 ).first()
866 minimum = row[field.min()]
867 maximum = row[field.max()]
868
869 # Ensure that we can select the extreme values
870 minute_step = 5
871 timedelta = datetime.timedelta
872 if minimum:
873 minimum -= timedelta(minutes = minute_step)
874 if maximum:
875 maximum += timedelta(minutes = minute_step)
876
877 # @ToDo: separate widget/deployment_setting
878 if self.opts.get("slider"):
879 if type(fields) is list:
880 event_start = fields[0]
881 event_end = fields[0]
882 else:
883 event_start = event_end = fields
884 ts = S3TimeSeries(resource,
885 start = minimum,
886 end = maximum,
887 slots = None, # Introspect the data
888 event_start = event_start,
889 event_end = event_end,
890 rows = None,
891 cols = None,
892 facts = None, # Default to Count id
893 baseline = None, # No baseline
894 )
895 # Extract aggregated results as JSON-serializable dict
896 data = ts.as_dict()
897 # We just want the dates & values
898 data = data["p"]
899 #ts = [(v["t"][0], v["t"][1], v["v"][0]) for v in data] # If we send start & end of slots
900 ts = [(v["t"][0], v["v"][0]) for v in data]
901 else:
902 ts = []
903
904 if as_str:
905 ISO = "%Y-%m-%dT%H:%M:%S"
906 if minimum:
907 minimum = minimum.strftime(ISO)
908 if maximum:
909 maximum = maximum.strftime(ISO)
910
911 return minimum, maximum, ts
912
913 # -------------------------------------------------------------------------
915 """
916 Render this widget as HTML helper object(s)
917
918 @param resource: the resource
919 @param values: the search values from the URL query
920 """
921
922 attr = self.attr
923
924 # CSS class and element ID
925 _class = self._class
926 if "_class" in attr and attr["_class"]:
927 _class = "%s %s" % (attr["_class"], _class)
928 else:
929 _class = _class
930 _id = attr["_id"]
931
932 # Classes and labels for the individual date/time inputs
933 T = current.T
934 input_class = self._input_class
935 input_labels = self.input_labels
936
937 # Picker options
938 opts_get = self.opts.get
939 clear_text = opts_get("clear_text", None)
940 hide_time = opts_get("hide_time", False)
941
942 # Introspective range?
943 slider = opts_get("slider", False)
944 if slider:
945 # Default to True
946 auto_range = opts_get("auto_range", True)
947 else:
948 auto_range = opts_get("auto_range")
949 if auto_range is None:
950 # Not specified for widget => apply global setting
951 auto_range = current.deployment_settings.get_search_dates_auto_range()
952
953 if auto_range:
954 minimum, maximum, ts = self._options(resource, as_str=False)
955 else:
956 minimum = maximum = None
957
958 # Generate the input elements
959 filter_widget = DIV(_id=_id, _class=_class)
960 append = filter_widget.append
961
962 if slider:
963 # Load Moment & D3/NVD3 into Browser
964 # @ToDo: Set Moment locale
965 # NB This will probably get used more widely in future, so maybe need to abstract this somewhere else
966 appname = current.request.application
967 s3 = current.response.s3
968 scripts_append = s3.scripts.append
969 if s3.debug:
970 if s3.cdn:
971 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js")
972 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js")
973 # We use a patched v1.8.5 currently, so can't use the CDN version
974 #scripts_append("https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.5/nv.d3.js")
975 else:
976 scripts_append("/%s/static/scripts/moment.js" % appname)
977 scripts_append("/%s/static/scripts/d3/d3.js" % appname)
978 scripts_append("/%s/static/scripts/d3/nv.d3.js" % appname)
979 else:
980 if s3.cdn:
981 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js")
982 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js")
983 #scripts_append("https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.5/nv.d3.min.js")
984 else:
985 scripts_append("/%s/static/scripts/moment.min.js" % appname)
986 scripts_append("/%s/static/scripts/d3/d3.min.js" % appname)
987 scripts_append("/%s/static/scripts/d3/nv.d3.min.js" % appname)
988 range_picker = DIV(_class="range-picker")
989 ISO = "%Y-%m-%dT%H:%M:%S"
990 if minimum:
991 range_picker["_data-min"] = minimum.strftime(ISO)
992 if maximum:
993 range_picker["_data-max"] = maximum.strftime(ISO)
994 if ts:
995 range_picker["_data-ts"] = json.dumps(ts, separators=SEPARATORS)
996 if hide_time:
997 # @ToDo: Translate Settings from Python to Moment
998 # http://momentjs.com/docs/#/displaying/
999 # https://github.com/benjaminoakes/moment-strftime
1000 # range_picker["_data-fmt"] = current.deployment_settings.get_L10n_date_format()
1001 range_picker["_data-fmt"] = "MMM D YYYY"
1002 #range_picker["_data-fmt"] = "LL" # Locale-aware version
1003 else:
1004 #range_picker["_data-fmt"] = current.deployment_settings.get_L10n_datetime_format()
1005 range_picker["_data-fmt"] = "MMM D YYYY HH:mm"
1006 #range_picker["_data-fmt"] = "LLL" # Locale-aware version
1007 append(DIV(range_picker,
1008 _class="range-picker-wrapper"))
1009
1010 get_variable = self._variable
1011
1012 fields = self.field
1013 if type(fields) is not list:
1014 fields = [fields]
1015 selector = self.selector
1016 else:
1017 selectors = self.selector.split("|")
1018
1019 start = True
1020 for field in fields:
1021 # Determine the field type
1022 if resource:
1023 rfield = S3ResourceField(resource, field)
1024 field = rfield.field
1025 else:
1026 rfield = field = None
1027 if not field:
1028 if not rfield or rfield.virtual:
1029 ftype = opts_get("fieldtype", "datetime")
1030 else:
1031 # Unresolvable selector
1032 return ""
1033 else:
1034 ftype = rfield.ftype
1035
1036 # S3CalendarWidget requires a Field
1037 if not field:
1038 if rfield:
1039 tname, fname = rfield.tname, rfield.fname
1040 else:
1041 tname, fname = "notable", "datetime"
1042 if not _id:
1043 raise SyntaxError("%s: _id parameter required " \
1044 "when rendered without resource." % \
1045 self.__class__.__name__)
1046 field = Field(fname, ftype, requires = IS_UTC_DATE())
1047 field.tablename = field._tablename = tname
1048
1049 if len(fields) == 1:
1050 operators = self.operator
1051 else:
1052 # 2 Separate fields
1053 if start:
1054 operators = ["ge"]
1055 selector = selectors[0]
1056 start = False
1057 else:
1058 operators = ["le"]
1059 selector = selectors[1]
1060 input_class += " end_date"
1061
1062 # Do we want a timepicker?
1063 timepicker = False if ftype == "date" or hide_time else True
1064 if timepicker and "datetimepicker" not in input_class:
1065 input_class += " datetimepicker"
1066
1067 for operator in operators:
1068
1069 input_id = "%s-%s" % (_id, operator)
1070
1071 # Make the two inputs constrain each other
1072 set_min = set_max = None
1073 if operator == "ge":
1074 set_min = "#%s-%s" % (_id, "le")
1075 elif operator == "le":
1076 set_max = "#%s-%s" % (_id, "ge")
1077
1078 # Instantiate the widget
1079 widget = S3CalendarWidget(timepicker = timepicker,
1080 minimum = minimum,
1081 maximum = maximum,
1082 set_min = set_min,
1083 set_max = set_max,
1084 clear_text = clear_text,
1085 )
1086
1087 # Populate with the value, if given
1088 # if user has not set any of the limits, we get [] in values.
1089 value = values.get(get_variable(selector, operator))
1090 if value in (None, []):
1091 value = None
1092 elif type(value) is list:
1093 value = value[0]
1094
1095 # Widget expects a string in local calendar and format
1096 if isinstance(value, basestring):
1097 # URL filter or filter default come as string in
1098 # Gregorian calendar and ISO format => convert into
1099 # a datetime
1100 try:
1101 dt = s3_decode_iso_datetime(value)
1102 except ValueError:
1103 dt = None
1104 else:
1105 # Assume datetime
1106 dt = value
1107 if dt:
1108 if timepicker:
1109 dtstr = S3DateTime.datetime_represent(dt, utc=False)
1110 else:
1111 dtstr = S3DateTime.date_represent(dt, utc=False)
1112 else:
1113 dtstr = None
1114
1115 # Render the widget
1116 picker = widget(field,
1117 dtstr,
1118 _class = input_class,
1119 _id = input_id,
1120 _name = input_id,
1121 )
1122
1123 if operator in input_labels:
1124 label = DIV(LABEL("%s:" % T(input_labels[operator]),
1125 _for=input_id,
1126 ),
1127 _class="range-filter-label",
1128 )
1129 else:
1130 label = ""
1131
1132 # Append label and widget
1133 append(DIV(label,
1134 DIV(picker,
1135 _class="range-filter-widget",
1136 ),
1137 _class="range-filter-field",
1138 ))
1139
1140 return filter_widget
1141
1142 # =============================================================================
1143 -class S3SliderFilter(S3RangeFilter):
1144 """
1145 Filter widget for Ranges which is controlled by a Slider instead of
1146 INPUTs
1147 Wraps jQueryUI's Range Slider in S3.range_slider in S3.js
1148
1149 Configuration options:
1150
1151 @keyword label: label for the widget
1152 @keyword comment: comment for the widget
1153 @keyword hidden: render widget initially hidden (="advanced" option)
1154
1155 FIXME broken (multiple issues)
1156 """
1157
1158 _class = "slider-filter"
1159
1160 operator = ["ge", "le"]
1161
1162 # -------------------------------------------------------------------------
1164 """
1165 Render this widget as HTML helper object(s)
1166
1167 @param resource: the resource
1168 @param values: the search values from the URL query
1169 """
1170
1171 attr = self.attr
1172
1173 # CSS class and element ID
1174 _class = self._class
1175 if "_class" in attr and attr["_class"]:
1176 _class = "%s %s" % (attr["_class"], _class)
1177 else:
1178 _class = _class
1179 attr["_class"] = _class
1180 _id = attr["_id"]
1181
1182 # Determine the field type
1183 if resource:
1184 rfield = S3ResourceField(resource, self.field)
1185 field = rfield.field
1186 else:
1187 field = None
1188 if not field:
1189 # Unresolvable selector
1190 return ""
1191
1192 # Options
1193 step = self.opts.get("step", 1)
1194 _type = self.opts.get("type", "int")
1195
1196 # Generate the input elements
1197 field = str(field)
1198 fieldname = field.replace(".", "_")
1199 selector = self.selector
1200 _variable = self._variable
1201 for operator in self.operator:
1202 input_id = "%s-%s" % (_id, operator)
1203 input = INPUT(_name = input_id,
1204 _disabled = True,
1205 _id = input_id,
1206 _style = "border:0",
1207 )
1208 # Populate with the value, if given
1209 # if user has not set any of the limits, we get [] in values.
1210 variable = _variable(selector, operator)
1211 value = values.get(variable, None)
1212 if value not in [None, []]:
1213 if type(value) is list:
1214 value = value[0]
1215 input["_value"] = value
1216 input["value"] = value
1217
1218 slider = DIV(_id="%s_slider" % fieldname, **attributes)
1219
1220 s3 = current.response.s3
1221
1222 validator = field.requires
1223 if isinstance(validator, IS_EMPTY_OR):
1224 validator = validator.other
1225 _min = validator.minimum
1226 # Max Value depends upon validator type
1227 if isinstance(validator, IS_INT_IN_RANGE):
1228 _max = validator.maximum - 1
1229 elif isinstance(validator, IS_FLOAT_IN_RANGE):
1230 _max = validator.maximum
1231
1232 if values is None:
1233 # JSONify
1234 value = "null"
1235 script = '''i18n.slider_help="%s"''' % \
1236 current.T("Click on the slider to choose a value")
1237 s3.js_global.append(script)
1238
1239 if _type == "int":
1240 script = '''S3.range_slider('%s',%i,%i,%i,%s)''' % (fieldname,
1241 _min,
1242 _max,
1243 step,
1244 values)
1245 else:
1246 # Float
1247 script = '''S3.range_slider('%s',%f,%f,%f,%s)''' % (fieldname,
1248 _min,
1249 _max,
1250 step,
1251 values)
1252 s3.jquery_ready.append(script)
1253
1254 return TAG[""](input1, input2, slider)
1255
1256 # =============================================================================
1257 -class S3LocationFilter(S3FilterWidget):
1258 """
1259 Hierarchical Location Filter Widget
1260
1261 NB This will show records linked to all child locations of the Lx
1262
1263 Configuration options:
1264
1265 ** Widget appearance:
1266
1267 @keyword label: label for the widget
1268 @keyword comment: comment for the widget
1269 @keyword hidden: render widget initially hidden (="advanced" option)
1270 @keyword no_opts: text to show if no options available
1271
1272 ** Options-lookup:
1273
1274 @keyword levels: list of location hierarchy levels
1275 @keyword resource: alternative resource to look up options
1276 @keyword lookup: field in the alternative resource to look up
1277 @keyword options: fixed set of options (list of gis_location IDs)
1278
1279 ** Multiselect-dropdowns:
1280
1281 @keyword search: show search-field to search for options
1282 @keyword header: show header with bulk-actions
1283 @keyword selectedList: number of selected items to show on
1284 button before collapsing into number of items
1285 """
1286
1287 _class = "location-filter"
1288
1289 operator = "belongs"
1290
1291 # -------------------------------------------------------------------------
1293 """
1294 Constructor to configure the widget
1295
1296 @param field: the selector(s) for the field(s) to filter by
1297 @param attr: configuration options for this widget
1298 """
1299
1300 if not field:
1301 field = "location_id"
1302
1303 # Translate options using gis_location_name?
1304 settings = current.deployment_settings
1305 translate = settings.get_L10n_translate_gis_location()
1306 if translate:
1307 language = current.session.s3.language
1308 #if language == settings.get_L10n_default_language():
1309 if language == "en": # Can have a default language for system & yet still want to translate from base English
1310 translate = False
1311 self.translate = translate
1312
1313 super(S3LocationFilter, self).__init__(field=field, **attr)
1314
1315 # -------------------------------------------------------------------------
1317 """
1318 Render this widget as HTML helper object(s)
1319
1320 @param resource: the resource
1321 @param values: the search values from the URL query
1322 """
1323
1324 attr = self._attr(resource)
1325 opts = self.opts
1326 name = attr["_name"]
1327
1328 ftype, levels, noopt = self._options(resource, values=values)
1329 if noopt:
1330 return SPAN(noopt, _class="no-options-available")
1331
1332 # Filter class (default+custom)
1333 _class = self._class
1334 if "_class" in attr and attr["_class"]:
1335 _class = "%s %s" % (_class, attr["_class"])
1336 attr["_class"] = _class
1337
1338 # Store id and name for the data element
1339 base_id = attr["_id"]
1340 base_name = attr["_name"]
1341
1342 widgets = []
1343 w_append = widgets.append
1344 operator = self.operator
1345 field_name = self.field
1346
1347 fname = self._prefix(field_name) if resource else field_name
1348
1349 #widget_type = opts["widget"]
1350 # Use groupedopts widget if we specify cols, otherwise assume multiselect
1351 cols = opts.get("cols", None)
1352 if cols:
1353 # Grouped Checkboxes
1354 # @ToDo: somehow working, but ugly, not usable (deprecated?)
1355 if "groupedopts-filter-widget" not in _class:
1356 attr["_class"] = "%s groupedopts-filter-widget" % _class
1357 attr["cols"] = cols
1358
1359 # Add one widget per level
1360 for level in levels:
1361 options = levels[level]["options"]
1362 groupedopts = S3GroupedOptionsWidget(cols = cols,
1363 size = opts["size"] or 12,
1364 )
1365 # Dummy field
1366 name = "%s-%s" % (base_name, level)
1367 dummy_field = Storage(name=name,
1368 type=ftype,
1369 requires=IS_IN_SET(options,
1370 multiple=True))
1371 # Unique ID/name
1372 attr["_id"] = "%s-%s" % (base_id, level)
1373 attr["_name"] = name
1374
1375 # Find relevant values to pre-populate
1376 _values = values.get("%s$%s__%s" % (fname, level, operator))
1377 w_append(groupedopts(dummy_field, _values, **attr))
1378
1379 else:
1380 # Multiselect is default
1381 T = current.T
1382
1383 # Multiselect Dropdown with Checkboxes
1384 if "multiselect-filter-widget" not in _class:
1385 _class = "%s multiselect-filter-widget" % _class
1386
1387 header_opt = opts.get("header", False)
1388 if header_opt is False or header_opt is True:
1389 setting = current.deployment_settings \
1390 .get_ui_location_filter_bulk_select_option()
1391 if setting is not None:
1392 header_opt = setting
1393
1394 # Add one widget per level
1395 first = True
1396 hide = True
1397 s3 = current.response.s3
1398 for level in levels:
1399 # Dummy field
1400 name = "%s-%s" % (base_name, level)
1401 # Unique ID/name
1402 attr["_id"] = "%s-%s" % (base_id, level)
1403 attr["_name"] = name
1404 # Find relevant values to pre-populate the widget
1405 _values = values.get("%s$%s__%s" % (fname, level, operator))
1406 w = S3MultiSelectWidget(search = opts.get("search", "auto"),
1407 header = header_opt,
1408 selectedList = opts.get("selectedList", 3),
1409 noneSelectedText = T("Select %(location)s") % \
1410 dict(location=levels[level]["label"]))
1411 if first:
1412 # Visible Multiselect Widget added to the page
1413 attr["_class"] = _class
1414 options = levels[level]["options"]
1415 dummy_field = Storage(name=name,
1416 type=ftype,
1417 requires=IS_IN_SET(options,
1418 multiple=True))
1419 widget = w(dummy_field, _values, **attr)
1420 first = False
1421 else:
1422 # Hidden, empty dropdown added to the page, whose options and multiselect will be activated when the higher level is selected
1423 if hide:
1424 _class = "%s hide" % _class
1425 attr["_class"] = _class
1426 hide = False
1427 # Store the current jquery_ready
1428 jquery_ready = s3.jquery_ready
1429 # Build the widget with the MultiSelect activation script
1430 s3.jquery_ready = []
1431 dummy_field = Storage(name=name,
1432 type=ftype,
1433 requires=IS_IN_SET([],
1434 multiple=True))
1435 widget = w(dummy_field, _values, **attr)
1436 # Extract the MultiSelect activation script
1437 script = s3.jquery_ready[0]
1438 # Restore jquery_ready
1439 s3.jquery_ready = jquery_ready
1440 # Wrap the script & reinsert
1441 script = '''S3.%s=function(){%s}''' % (name.replace("-", "_"), script)
1442 s3.js_global.append(script)
1443 w_append(widget)
1444
1445 # Restore id and name for the data_element
1446 attr["_id"] = base_id
1447 attr["_name"] = base_name
1448
1449 # Render the filter widget
1450 return TAG[""](*widgets)
1451
1452 # -------------------------------------------------------------------------
1454 """
1455 Construct the hidden element that holds the
1456 URL query term corresponding to an input element in the widget.
1457
1458 @param variable: the URL query variable
1459 """
1460
1461 output = []
1462 oappend = output.append
1463 i = 0
1464 for level in self.levels:
1465 widget = INPUT(_type="hidden",
1466 _id="%s-%s-data" % (self.attr["_id"], level),
1467 _class="filter-widget-data %s-data" % self._class,
1468 _value=variable[i])
1469 oappend(widget)
1470 i += 1
1471
1472 return output
1473
1474 # -------------------------------------------------------------------------
1476
1477 attr = self._attr(resource)
1478 levels, noopt = self._options(resource, inject_hierarchy=False)[1:3]
1479
1480 opts = {}
1481 base_id = attr["_id"]
1482 for level in levels:
1483 if noopt:
1484 opts["%s-%s" % (base_id, level)] = str(noopt)
1485 else:
1486 options = levels[level]["options"]
1487 opts["%s-%s" % (base_id, level)] = options
1488 return opts
1489
1490 # -------------------------------------------------------------------------
1491 @staticmethod
1493
1494 if inject_hierarchy:
1495 parent = None
1496 grandparent = None
1497 greatgrandparent = None
1498 greatgreatgrandparent = None
1499 greatgreatgreatgrandparent = None
1500 i = 0
1501 for level in levels:
1502 v = row[level]
1503 if v:
1504 o = levels[level]["options"]
1505 if v not in o:
1506 if translate:
1507 o[v] = name_l10n.get(v, v)
1508 else:
1509 o.append(v)
1510 if inject_hierarchy:
1511 if i == 0:
1512 h = hierarchy[_level]
1513 if v not in h:
1514 h[v] = {}
1515 parent = v
1516 elif i == 1:
1517 h = hierarchy[_level][parent]
1518 if v not in h:
1519 h[v] = {}
1520 grandparent = parent
1521 parent = v
1522 elif i == 2:
1523 h = hierarchy[_level][grandparent][parent]
1524 if v not in h:
1525 h[v] = {}
1526 greatgrandparent = grandparent
1527 grandparent = parent
1528 parent = v
1529 elif i == 3:
1530 h = hierarchy[_level][greatgrandparent][grandparent][parent]
1531 if v not in h:
1532 h[v] = {}
1533 greatgreatgrandparent = greatgrandparent
1534 greatgrandparent = grandparent
1535 grandparent = parent
1536 parent = v
1537 elif i == 4:
1538 h = hierarchy[_level][greatgreatgrandparent][greatgrandparent][grandparent][parent]
1539 if v not in h:
1540 h[v] = {}
1541 greatgreatgreatgrandparent = greatgreatgrandparent
1542 greatgreatgrandparent = greatgrandparent
1543 greatgrandparent = grandparent
1544 grandparent = parent
1545 parent = v
1546 elif i == 5:
1547 h = hierarchy[_level][greatgreatgreatgrandparent][greatgreatgrandparent][greatgrandparent][grandparent][parent]
1548 if v not in h:
1549 h[v] = {}
1550 i += 1
1551
1552 # -------------------------------------------------------------------------
1554 """
1555 Look up the immediate Lx ancestors of relevant levels
1556 for all locations referenced by selector
1557
1558 @param levels: the relevant Lx levels, tuple of "L1", "L2" etc
1559 @param resource: the master resource
1560 @param selector: the selector for the location reference
1561
1562 @returns: gis_location Rows, or empty list
1563 """
1564
1565 db = current.db
1566 s3db = current.s3db
1567
1568 ltable = s3db.gis_location
1569 if location_ids:
1570 # Fixed set
1571 location_ids = set(location_ids)
1572 else:
1573 # Lookup from resource
1574 location_ids = set()
1575
1576 # Resolve the selector
1577 rfield = resource.resolve_selector(selector)
1578
1579 # Get the joins for the selector
1580 from s3query import S3Joins
1581 joins = S3Joins(resource.tablename)
1582 joins.extend(rfield._joins)
1583 join = joins.as_list()
1584
1585 # Add a join for gis_location
1586 join.append(ltable.on(ltable.id == rfield.field))
1587
1588 # Accessible query for the master table
1589 query = resource.get_query()
1590
1591 # Fields we want to extract for Lx ancestors
1592 fields = [ltable.id] + [ltable[level] for level in levels]
1593 if path:
1594 fields.append(ltable.path)
1595
1596 # Suppress instantiation of LazySets in rows (we don't need them)
1597 rname = db._referee_name
1598 db._referee_name = None
1599
1600 rows = []
1601 while True:
1602
1603 if location_ids:
1604 query = ltable.id.belongs(location_ids)
1605 join = None
1606
1607 # Extract all target locations resp. parents which
1608 # are Lx of relevant levels
1609 relevant_lx = (ltable.level.belongs(levels))
1610 lx = db(query & relevant_lx).select(join = join,
1611 groupby = ltable.id,
1612 *fields
1613 )
1614
1615 # Add to result rows
1616 if lx:
1617 rows = rows & lx if rows else lx
1618
1619 # Pick subset for parent lookup
1620 if lx and location_ids:
1621 # ...all parents which are not Lx of relevant levels
1622 remaining = location_ids - set(row.id for row in lx)
1623 if remaining:
1624 query = ltable.id.belongs(remaining)
1625 else:
1626 # No more parents to look up
1627 break
1628 else:
1629 # ...all locations which are not Lx or not of relevant levels
1630 query &= ((ltable.level == None) | (~(ltable.level.belongs(levels))))
1631
1632 # From subset, just extract the parent ID
1633 query &= (ltable.parent != None)
1634 parents = db(query).select(ltable.parent,
1635 join = join,
1636 groupby = ltable.parent,
1637 )
1638
1639 location_ids = set(row.parent for row in parents if row.parent)
1640 if not location_ids:
1641 break
1642
1643 # Restore referee name
1644 db._referee_name = rname
1645
1646 return rows
1647
1648 # -------------------------------------------------------------------------
1650
1651 T = current.T
1652 s3db = current.s3db
1653 gtable = s3db.gis_location
1654
1655 NOOPT = T("No options available")
1656
1657 #attr = self.attr
1658 opts = self.opts
1659 translate = self.translate
1660
1661 # Which levels should we display?
1662 # Lookup the appropriate labels from the GIS configuration
1663 if "levels" in opts:
1664 hierarchy = current.gis.get_location_hierarchy()
1665 levels = OrderedDict()
1666 for level in opts["levels"]:
1667 levels[level] = hierarchy.get(level, level)
1668 else:
1669 levels = current.gis.get_relevant_hierarchy_levels(as_dict=True)
1670
1671 # Pass to data_element
1672 self.levels = levels
1673
1674 if "label" not in opts:
1675 opts["label"] = T("Filter by Location")
1676
1677 # Initialise Options Storage & Hierarchy
1678 hierarchy = {}
1679 first = True
1680 for level in levels:
1681 if first:
1682 hierarchy[level] = {}
1683 _level = level
1684 first = False
1685 levels[level] = {"label": levels[level],
1686 "options": {} if translate else [],
1687 }
1688
1689 ftype = "reference gis_location"
1690 default = (ftype, levels, opts.get("no_opts", NOOPT))
1691
1692 # Resolve the field selector
1693 selector = None
1694 if resource is None:
1695 rname = opts.get("resource")
1696 if rname:
1697 resource = s3db.resource(rname)
1698 selector = opts.get("lookup", "location_id")
1699 else:
1700 selector = self.field
1701
1702 filters_added = False
1703
1704 options = opts.get("options")
1705 if options:
1706 # Fixed options (=list of location IDs)
1707 resource = s3db.resource("gis_location", id=options)
1708 fields = ["id"] + [l for l in levels]
1709 if translate:
1710 fields.append("path")
1711 joined = False
1712
1713 elif selector:
1714
1715 # Lookup options from resource
1716 rfield = S3ResourceField(resource, selector)
1717 if not rfield.field or rfield.ftype != ftype:
1718 # Must be a real reference to gis_location
1719 return default
1720
1721 fields = [selector] + ["%s$%s" % (selector, l) for l in levels]
1722 if translate:
1723 fields.append("%s$path" % selector)
1724
1725 # Always joined (gis_location foreign key in resource)
1726 joined = True
1727
1728 # Reduce multi-table joins by excluding empty FKs
1729 resource.add_filter(FS(selector) != None)
1730
1731 # Filter out old Locations
1732 # @ToDo: Allow override
1733 resource.add_filter(FS("%s$end_date" % selector) == None)
1734 filters_added = True
1735
1736 else:
1737 # Neither fixed options nor resource to look them up
1738 return default
1739
1740 # Determine look-up strategy
1741 ancestor_lookup = opts.get("bigtable")
1742 if ancestor_lookup is None:
1743 ancestor_lookup = current.deployment_settings \
1744 .get_gis_location_filter_bigtable_lookups()
1745
1746 # Find the options
1747 if ancestor_lookup:
1748 rows = self.get_lx_ancestors(levels,
1749 resource,
1750 selector = selector,
1751 location_ids = options,
1752 path = translate,
1753 )
1754 joined = False
1755 else:
1756 # Prevent unnecessary extraction of extra fields
1757 extra_fields = resource.get_config("extra_fields")
1758 resource.clear_config("extra_fields")
1759
1760 # Suppress instantiation of LazySets in rows (we don't need them)
1761 db = current.db
1762 rname = db._referee_name
1763 db._referee_name = None
1764 rows = resource.select(fields = fields,
1765 limit = None,
1766 virtual = False,
1767 as_rows = True,
1768 )
1769
1770 # Restore referee name
1771 db._referee_name = rname
1772
1773 # Restore extra fields
1774 resource.configure(extra_fields=extra_fields)
1775
1776 if filters_added:
1777 # Remove them
1778 rfilter = resource.rfilter
1779 rfilter.filters.pop()
1780 rfilter.filters.pop()
1781 rfilter.query = None
1782 rfilter.transformed = None
1783
1784 rows2 = []
1785 if not rows:
1786 if values:
1787 # Make sure the selected options are in the available options
1788
1789 fields = ["id"] + [l for l in levels]
1790 if translate:
1791 fields.append("path")
1792
1793 resource2 = None
1794 joined = False
1795 rows = []
1796 for f in values:
1797 v = values[f]
1798 if not v:
1799 continue
1800 level = "L%s" % f.split("L", 1)[1][0]
1801 query = (gtable.level == level) & \
1802 (gtable.name.belongs(v))
1803 if resource2 is None:
1804 resource2 = s3db.resource("gis_location",
1805 filter = query,
1806 )
1807 else:
1808 resource2.clear_query()
1809 resource2.add_filter(query)
1810 # Filter out old Locations
1811 # @ToDo: Allow override
1812 resource2.add_filter(gtable.end_date == None)
1813 _rows = resource2.select(fields = fields,
1814 limit = None,
1815 virtual = False,
1816 as_rows = True,
1817 )
1818 if rows:
1819 rows &= _rows
1820 else:
1821 rows = _rows
1822
1823 if not rows:
1824 # No options
1825 return default
1826
1827 elif values:
1828 # Make sure the selected options are in the available options
1829
1830 fields = ["id"] + [l for l in levels]
1831 if translate:
1832 fields.append("path")
1833
1834 resource2 = None
1835 for f in values:
1836 v = values[f]
1837 if not v:
1838 continue
1839 level = "L%s" % f.split("L", 1)[1][0]
1840
1841 if resource2 is None:
1842 resource2 = s3db.resource("gis_location")
1843 resource2.clear_query()
1844
1845 query = (gtable.level == level) & \
1846 (gtable.name.belongs(v))
1847 resource2.add_filter(query)
1848 # Filter out old Locations
1849 # @ToDo: Allow override
1850 resource2.add_filter(gtable.end_date == None)
1851 _rows = resource2.select(fields=fields,
1852 limit=None,
1853 virtual=False,
1854 as_rows=True)
1855 if rows2:
1856 rows2 &= _rows
1857 else:
1858 rows2 = _rows
1859
1860 # Generate a name localization lookup dict
1861 name_l10n = {}
1862 if translate:
1863 # Get IDs via Path to lookup name_l10n
1864 ids = set()
1865 if joined:
1866 selector = rfield.colname
1867 for row in rows:
1868 _row = getattr(row, "gis_location") if joined else row
1869 path = _row.path
1870 if path:
1871 path = path.split("/")
1872 else:
1873 # Build it
1874 if joined:
1875 location_id = row[selector]
1876 if location_id:
1877 _row.id = location_id
1878 if "id" in _row:
1879 path = current.gis.update_location_tree(_row)
1880 path = path.split("/")
1881 if path:
1882 ids |= set(path)
1883 for row in rows2:
1884 path = row.path
1885 if path:
1886 path = path.split("/")
1887 else:
1888 # Build it
1889 if "id" in row:
1890 path = current.gis.update_location_tree(row)
1891 path = path.split("/")
1892 if path:
1893 ids |= set(path)
1894
1895 # Build lookup table for name_l10n
1896 ntable = s3db.gis_location_name
1897 query = (gtable.id.belongs(ids)) & \
1898 (ntable.deleted == False) & \
1899 (ntable.location_id == gtable.id) & \
1900 (ntable.language == current.session.s3.language)
1901 nrows = current.db(query).select(gtable.name,
1902 ntable.name_l10n,
1903 limitby=(0, len(ids)),
1904 )
1905 for row in nrows:
1906 name_l10n[row["gis_location.name"]] = row["gis_location_name.name_l10n"]
1907
1908 # Populate the Options and the Hierarchy
1909 for row in rows:
1910 _row = getattr(row, "gis_location") if joined else row
1911 self.__options(_row, levels, inject_hierarchy, hierarchy, _level, translate, name_l10n)
1912 for row in rows2:
1913 self.__options(row, levels, inject_hierarchy, hierarchy, _level, translate, name_l10n)
1914
1915 if translate:
1916 # Sort the options dicts
1917 for level in levels:
1918 options = levels[level]["options"]
1919 options = OrderedDict(sorted(options.iteritems()))
1920 else:
1921 # Sort the options lists
1922 for level in levels:
1923 levels[level]["options"].sort()
1924
1925 if inject_hierarchy:
1926 # Inject the Location Hierarchy
1927 hierarchy = "S3.location_filter_hierarchy=%s" % \
1928 json.dumps(hierarchy, separators=SEPARATORS)
1929 js_global = current.response.s3.js_global
1930 js_global.append(hierarchy)
1931 if translate:
1932 # Inject lookup list
1933 name_l10n = "S3.location_name_l10n=%s" % \
1934 json.dumps(name_l10n, separators=SEPARATORS)
1935 js_global.append(name_l10n)
1936
1937 return (ftype, levels, None)
1938
1939 # -------------------------------------------------------------------------
1941 """
1942 Helper method to generate a filter query selector for the
1943 given field(s) in the given resource.
1944
1945 @param resource: the S3Resource
1946 @param fields: the field selectors (as strings)
1947
1948 @return: the field label and the filter query selector, or None if none of the
1949 field selectors could be resolved
1950 """
1951
1952 prefix = self._prefix
1953
1954 if resource:
1955 rfield = S3ResourceField(resource, fields)
1956 label = rfield.label
1957 else:
1958 label = None
1959
1960 if "levels" in self.opts:
1961 levels = self.opts.levels
1962 else:
1963 levels = current.gis.get_relevant_hierarchy_levels()
1964
1965 fields = ["%s$%s" % (fields, level) for level in levels]
1966 if resource:
1967 selectors = []
1968 for field in fields:
1969 try:
1970 rfield = S3ResourceField(resource, field)
1971 except (AttributeError, TypeError):
1972 continue
1973 selectors.append(prefix(rfield.selector))
1974 else:
1975 selectors = fields
1976 if selectors:
1977 return label, "|".join(selectors)
1978 else:
1979 return label, None
1980
1981 # -------------------------------------------------------------------------
1982 @classmethod
1984 """
1985 Construct URL query variable(s) name from a filter query
1986 selector and the given operator(s)
1987
1988 @param selector: the selector
1989 @param operator: the operator (or tuple/list of operators)
1990
1991 @return: the URL query variable name (or list of variable names)
1992 """
1993
1994 selectors = selector.split("|")
1995 return ["%s__%s" % (selector, operator) for selector in selectors]
1996
1997 # =============================================================================
1998 -class S3MapFilter(S3FilterWidget):
1999 """
2000 Map filter widget
2001 Normally configured for "~.location_id$the_geom"
2002
2003 Configuration options:
2004
2005 @keyword label: label for the widget
2006 @keyword comment: comment for the widget
2007 @keyword hidden: render widget initially hidden (="advanced" option)
2008 """
2009
2010 _class = "map-filter"
2011
2012 operator = "intersects"
2013
2014 # -------------------------------------------------------------------------
2016 """
2017 Render this widget as HTML helper object(s)
2018
2019 @param resource: the resource
2020 @param values: the search values from the URL query
2021 """
2022
2023 settings = current.deployment_settings
2024
2025 if not settings.get_gis_spatialdb():
2026 current.log.warning("No Spatial DB => Cannot do Intersects Query yet => Disabling S3MapFilter")
2027 return ""
2028
2029 attr_get = self.attr.get
2030 opts_get = self.opts.get
2031
2032 _class = attr_get("class")
2033 if _class:
2034 _class = "%s %s" % (_class, self._class)
2035 else:
2036 _class = self._class
2037
2038 _id = attr_get("_id")
2039
2040 # Hidden INPUT to store the WKT
2041 hidden_input = INPUT(_type = "hidden",
2042 _class = _class,
2043 _id = _id,
2044 )
2045
2046 # Populate with the value, if given
2047 if values not in (None, []):
2048 if type(values) is list:
2049 values = values[0]
2050 hidden_input["_value"] = values
2051
2052 # Map Widget
2053 map_id = "%s-map" % _id
2054
2055 c, f = resource.tablename.split("_", 1)
2056 c = opts_get("controller", c)
2057 f = opts_get("function", f)
2058
2059 ltable = current.s3db.gis_layer_feature
2060 query = (ltable.controller == c) & \
2061 (ltable.function == f) & \
2062 (ltable.deleted == False)
2063 layer = current.db(query).select(ltable.layer_id,
2064 ltable.name,
2065 limitby=(0, 1)
2066 ).first()
2067 try:
2068 layer_id = layer.layer_id
2069 except AttributeError:
2070 # No prepop done?
2071 layer_id = None
2072 layer_name = resource.tablename
2073 else:
2074 layer_name = layer.name
2075
2076 feature_resources = [{"name" : current.T(layer_name),
2077 "id" : "search_results",
2078 "layer_id" : layer_id,
2079 "filter" : opts_get("filter"),
2080 },
2081 ]
2082
2083 button = opts_get("button")
2084 if button:
2085 # No need for the toolbar
2086 toolbar = opts_get("toolbar", False)
2087 else:
2088 # Need the toolbar
2089 toolbar = True
2090
2091 _map = current.gis.show_map(id = map_id,
2092 height = opts_get("height", settings.get_gis_map_height()),
2093 width = opts_get("width", settings.get_gis_map_width()),
2094 collapsed = True,
2095 callback='''S3.search.s3map('%s')''' % map_id,
2096 feature_resources = feature_resources,
2097 toolbar = toolbar,
2098 add_polygon = True,
2099 )
2100
2101 return TAG[""](hidden_input,
2102 button,
2103 _map,
2104 )
2105
2106 # =============================================================================
2107 -class S3OptionsFilter(S3FilterWidget):
2108 """
2109 Options filter widget
2110
2111 Configuration options:
2112
2113 ** Widget appearance:
2114
2115 @keyword label: label for the widget
2116 @keyword comment: comment for the widget
2117 @keyword hidden: render widget initially hidden (="advanced" option)
2118 @keyword widget: widget to use:
2119 "select", "multiselect" (default), or "groupedopts"
2120 @keyword no_opts: text to show if no options available
2121
2122 ** Options-lookup:
2123
2124 @keyword resource: alternative resource to look up options
2125 @keyword lookup: field in the alternative resource to look up
2126 @keyword options: fixed set of options (of {value: label} or
2127 a callable that returns one)
2128
2129 ** Options-representation:
2130
2131 @keyword represent: custom represent for looked-up options
2132 (overrides field representation method)
2133 @keyword translate: translate the option labels in the fixed set
2134 (looked-up option sets will use the
2135 field representation method instead)
2136 @keyword none: label for explicit None-option in many-to-many fields
2137
2138 ** multiselect-specific options:
2139
2140 @keyword search: show search-field to search for options
2141 @keyword header: show header with bulk-actions
2142 @keyword selectedList: number of selected items to show on
2143 button before collapsing into number of items
2144
2145 ** groupedopts-specific options:
2146
2147 @keyword cols: number of columns of checkboxes
2148 @keyword size: maximum size of multi-letter options groups
2149 @keyword help_field: field in the referenced table to display on
2150 hovering over a foreign key option
2151 """
2152
2153 _class = "options-filter"
2154
2155 operator = "belongs"
2156
2157 alternatives = ["anyof", "contains"]
2158
2159 # -------------------------------------------------------------------------
2161 """
2162 Render this widget as HTML helper object(s)
2163
2164 @param resource: the resource
2165 @param values: the search values from the URL query
2166 """
2167
2168 attr = self._attr(resource)
2169 opts_get = self.opts.get
2170 name = attr["_name"]
2171
2172 # Get the options
2173 ftype, options, noopt = self._options(resource, values=values)
2174 if options is None:
2175 options = []
2176 hide_widget = True
2177 hide_noopt = ""
2178 else:
2179 options = OrderedDict(options)
2180 hide_widget = False
2181 hide_noopt = " hide"
2182
2183 # Any-All-Option : for many-to-many fields the user can
2184 # search for records containing all the options or any
2185 # of the options:
2186 if len(options) > 1 and ftype[:4] == "list":
2187 operator = opts_get("operator", None)
2188 if operator:
2189 # Fixed operator
2190 any_all = ""
2191 else:
2192 # User choice (initially set to "all")
2193 any_all = True
2194 operator = "contains"
2195
2196 if operator == "anyof":
2197 filter_type = "any"
2198 else:
2199 filter_type = "all"
2200 self.operator = operator
2201
2202 if any_all:
2203 # Provide a form to prompt the user to choose
2204 T = current.T
2205 any_all = DIV(LABEL("%s:" % T("Match")),
2206 LABEL(INPUT(_name = "%s_filter" % name,
2207 _id = "%s_filter_any" % name,
2208 _type = "radio",
2209 _value = "any",
2210 value = filter_type,
2211 ),
2212 T("Any##filter_options"),
2213 _for = "%s_filter_any" % name,
2214 ),
2215 LABEL(INPUT(_name = "%s_filter" % name,
2216 _id = "%s_filter_all" % name,
2217 _type = "radio",
2218 _value = "all",
2219 value = filter_type,
2220 ),
2221 T("All##filter_options"),
2222 _for = "%s_filter_all" % name,
2223 ),
2224 _class="s3-options-filter-anyall",
2225 )
2226 else:
2227 any_all = ""
2228
2229 # Initialize widget
2230 #widget_type = opts_get("widget")
2231 # Use groupedopts widget if we specify cols, otherwise assume multiselect
2232 cols = opts_get("cols", None)
2233 if cols:
2234 widget_class = "groupedopts-filter-widget"
2235 w = S3GroupedOptionsWidget(options = options,
2236 multiple = opts_get("multiple", True),
2237 cols = cols,
2238 size = opts_get("size", 12),
2239 help_field = opts_get("help_field"),
2240 sort = opts_get("sort", True),
2241 orientation = opts_get("orientation"),
2242 table = opts_get("table", True),
2243 no_opts = opts_get("no_opts", None),
2244 option_comment = opts_get("option_comment", False),
2245 )
2246 else:
2247 # Default widget_type = "multiselect"
2248 widget_class = "multiselect-filter-widget"
2249 w = S3MultiSelectWidget(search = opts_get("search", "auto"),
2250 header = opts_get("header", False),
2251 selectedList = opts_get("selectedList", 3),
2252 noneSelectedText = opts_get("noneSelectedText", "Select"),
2253 multiple = opts_get("multiple", True),
2254 )
2255
2256
2257 # Add widget class and default class
2258 classes = attr.get("_class", "").split() + [widget_class, self._class]
2259 if hide_widget:
2260 classes.append("hide")
2261 attr["_class"] = " ".join(set(classes)) if classes else None
2262
2263 # Render the widget
2264 dummy_field = Storage(name=name,
2265 type=ftype,
2266 requires=IS_IN_SET(options, multiple=True))
2267 widget = w(dummy_field, values, **attr)
2268
2269 return TAG[""](any_all,
2270 widget,
2271 SPAN(noopt,
2272 _class="no-options-available%s" % hide_noopt,
2273 ),
2274 )
2275
2276 # -------------------------------------------------------------------------
2278 """
2279 Method to Ajax-retrieve the current options of this widget
2280
2281 @param resource: the S3Resource
2282 """
2283
2284 opts = self.opts
2285 attr = self._attr(resource)
2286 ftype, options, noopt = self._options(resource)
2287
2288 if options is None:
2289 options = {attr["_id"]: {"empty": str(noopt)}}
2290 else:
2291 #widget_type = opts["widget"]
2292 # Use groupedopts widget if we specify cols, otherwise assume multiselect
2293 cols = opts.get("cols", None)
2294 if cols:
2295 # Use the widget method to group and sort the options
2296 widget = S3GroupedOptionsWidget(
2297 options = options,
2298 multiple = True,
2299 cols = cols,
2300 size = opts["size"] or 12,
2301 help_field = opts["help_field"],
2302 sort = opts.get("sort", True),
2303 )
2304 options = {attr["_id"]:
2305 widget._options({"type": ftype}, [])}
2306 else:
2307 # Multiselect
2308 # Produce a simple list of tuples
2309 options = {attr["_id"]: [(k, s3_unicode(v))
2310 for k, v in options]}
2311
2312 return options
2313
2314 # -------------------------------------------------------------------------
2316 """
2317 Helper function to retrieve the current options for this
2318 filter widget
2319
2320 @param resource: the S3Resource
2321 """
2322
2323 T = current.T
2324 NOOPT = T("No options available")
2325 EMPTY = T("None")
2326
2327 #attr = self.attr
2328 opts = self.opts
2329
2330 # Resolve the field selector
2331 selector = self.field
2332 if isinstance(selector, (tuple, list)):
2333 selector = selector[0]
2334
2335 if resource is None:
2336 rname = opts.get("resource")
2337 if rname:
2338 resource = current.s3db.resource(rname)
2339
2340 if resource:
2341 rfield = S3ResourceField(resource, selector)
2342 field = rfield.field
2343 colname = rfield.colname
2344 ftype = rfield.ftype
2345 else:
2346 rfield = field = colname = None
2347 ftype = "string"
2348
2349 # Find the options
2350 opt_keys = []
2351
2352 multiple = ftype[:5] == "list:"
2353 if opts.options is not None:
2354 # Custom dict of options {value: label} or a callable
2355 # returning such a dict:
2356 options = opts.options
2357 if callable(options):
2358 options = options()
2359 opt_keys = options.keys()
2360
2361 elif resource:
2362 # Determine the options from the field type
2363 options = None
2364 if ftype == "boolean":
2365 opt_keys = (True, False)
2366
2367 elif field or rfield.virtual:
2368
2369 groupby = field if field and not multiple else None
2370 virtual = field is None
2371
2372 # If the search field is a foreign key, then try to perform
2373 # a reverse lookup of primary IDs in the lookup table which
2374 # are linked to at least one record in the resource => better
2375 # scalability
2376 # => only if the number of lookup options is much (!) smaller than
2377 # the number of records in the resource to filter, otherwise
2378 # this can have the opposite effect (e.g. person_id being the
2379 # search field); however, counting records in both tables before
2380 # deciding this would be even less scalable, hence:
2381 # @todo: implement a widget option to enforce forward-lookup if
2382 # the look-up table is the big table
2383 rows = None
2384 if field:
2385 ktablename, key, m = s3_get_foreign_key(field, m2m=False)
2386 if ktablename:
2387
2388 multiple = m
2389
2390 ktable = current.s3db.table(ktablename)
2391 key_field = ktable[key]
2392 colname = str(key_field)
2393
2394 # Find only values linked to records the user is
2395 # permitted to read, and apply any resource filters
2396 # (= use the resource query)
2397 query = resource.get_query()
2398
2399 # Must include rfilter joins when using the resource
2400 # query (both inner and left):
2401 rfilter = resource.rfilter
2402 if rfilter:
2403 join = rfilter.get_joins()
2404 left = rfilter.get_joins(left=True)
2405 else:
2406 join = left = None
2407
2408 # The actual query for the look-up table
2409 # @note: the inner join here is required even if rfilter
2410 # already left-joins the look-up table, because we
2411 # must make sure look-up values are indeed linked
2412 # to the resource => not redundant!
2413 query &= (key_field == field) & \
2414 current.auth.s3_accessible_query("read", ktable)
2415
2416 # If the filter field is in a joined table itself,
2417 # then we also need the join for that table (this
2418 # could be redundant, but checking that will likely
2419 # take more effort than we can save by avoiding it)
2420 joins = rfield.join
2421 for tname in joins:
2422 query &= joins[tname]
2423
2424 # Filter options by location?
2425 location_filter = opts.get("location_filter")
2426 if location_filter and "location_id" in ktable:
2427 location = current.session.s3.location_filter
2428 if location:
2429 query &= (ktable.location_id == location)
2430
2431 # Filter options by organisation?
2432 org_filter = opts.get("org_filter")
2433 if org_filter and "organisation_id" in ktable:
2434 root_org = current.auth.root_org()
2435 if root_org:
2436 query &= ((ktable.organisation_id == root_org) | \
2437 (ktable.organisation_id == None))
2438 #else:
2439 # query &= (ktable.organisation_id == None)
2440
2441 rows = current.db(query).select(key_field,
2442 resource._id.min(),
2443 groupby = key_field,
2444 join = join,
2445 left = left,
2446 )
2447
2448 # If we can not perform a reverse lookup, then we need
2449 # to do a forward lookup of all unique values of the
2450 # search field from all records in the table :/ still ok,
2451 # but not endlessly scalable:
2452 if rows is None:
2453 rows = resource.select([selector],
2454 limit = None,
2455 orderby = field,
2456 groupby = groupby,
2457 virtual = virtual,
2458 as_rows = True,
2459 )
2460
2461 opt_keys = [] # Can't use set => would make orderby pointless
2462 if rows:
2463 kappend = opt_keys.append
2464 kextend = opt_keys.extend
2465 for row in rows:
2466 val = row[colname]
2467 if virtual and callable(val):
2468 val = val()
2469 if (multiple or \
2470 virtual) and isinstance(val, (list, tuple, set)):
2471 kextend([v for v in val
2472 if v not in opt_keys])
2473 elif val not in opt_keys:
2474 kappend(val)
2475
2476 # Make sure the selected options are in the available options
2477 # (not possible if we have a fixed options dict)
2478 if options is None and values:
2479 numeric = rfield.ftype in ("integer", "id") or \
2480 rfield.ftype[:9] == "reference"
2481 for _val in values:
2482 if numeric and _val is not None:
2483 try:
2484 val = int(_val)
2485 except ValueError:
2486 # not valid for this field type => skip
2487 continue
2488 else:
2489 val = _val
2490 if val not in opt_keys and \
2491 (not isinstance(val, (int, long)) or not str(val) in opt_keys):
2492 opt_keys.append(val)
2493
2494 # No options?
2495 if len(opt_keys) < 1 or len(opt_keys) == 1 and not opt_keys[0]:
2496 return (ftype, None, opts.get("no_opts", NOOPT))
2497
2498 # Represent the options
2499 opt_list = [] # list of tuples (key, value)
2500
2501 # Custom represent? (otherwise fall back to field.represent)
2502 represent = opts.represent
2503 if not represent: # or ftype[:9] != "reference":
2504 represent = field.represent if field else None
2505
2506 if options is not None:
2507 # Custom dict of {value:label} => use this label
2508 if opts.get("translate"):
2509 # Translate the labels
2510 opt_list = [(opt, T(label))
2511 if isinstance(label, basestring) else (opt, label)
2512 for opt, label in options.items()
2513 ]
2514 else:
2515 opt_list = options.items()
2516
2517
2518 elif callable(represent):
2519 # Callable representation function:
2520
2521 if hasattr(represent, "bulk"):
2522 # S3Represent => use bulk option
2523 opt_dict = represent.bulk(opt_keys,
2524 list_type=False,
2525 show_link=False)
2526 if None in opt_keys:
2527 opt_dict[None] = EMPTY
2528 elif None in opt_dict:
2529 del opt_dict[None]
2530 if "" in opt_keys:
2531 opt_dict[""] = EMPTY
2532 opt_list = opt_dict.items()
2533
2534 else:
2535 # Simple represent function
2536 args = {"show_link": False} \
2537 if "show_link" in represent.func_code.co_varnames else {}
2538 if multiple:
2539 repr_opt = lambda opt: opt in (None, "") and (opt, EMPTY) or \
2540 (opt, represent([opt], **args))
2541 else:
2542 repr_opt = lambda opt: opt in (None, "") and (opt, EMPTY) or \
2543 (opt, represent(opt, **args))
2544 opt_list = map(repr_opt, opt_keys)
2545
2546 elif isinstance(represent, str) and ftype[:9] == "reference":
2547 # Represent is a string template to be fed from the
2548 # referenced record
2549
2550 # Get the referenced table
2551 db = current.db
2552 ktable = db[ftype[10:]]
2553
2554 k_id = ktable._id.name
2555
2556 # Get the fields referenced by the string template
2557 fieldnames = [k_id]
2558 fieldnames += re.findall(r"%\(([a-zA-Z0-9_]*)\)s", represent)
2559 represent_fields = [ktable[fieldname] for fieldname in fieldnames]
2560
2561 # Get the referenced records
2562 query = (ktable.id.belongs([k for k in opt_keys
2563 if str(k).isdigit()])) & \
2564 (ktable.deleted == False)
2565 rows = db(query).select(*represent_fields).as_dict(key=k_id)
2566
2567 # Run all referenced records against the format string
2568 opt_list = []
2569 ol_append = opt_list.append
2570 for opt_value in opt_keys:
2571 if opt_value in rows:
2572 opt_represent = represent % rows[opt_value]
2573 if opt_represent:
2574 ol_append((opt_value, opt_represent))
2575
2576 else:
2577 # Straight string representations of the values (fallback)
2578 opt_list = [(opt_value, s3_unicode(opt_value))
2579 for opt_value in opt_keys if opt_value]
2580
2581 if opts.get("sort", True):
2582 try:
2583 opt_list.sort(key=lambda item: item[1])
2584 except:
2585 opt_list.sort(key=lambda item: s3_unicode(item[1]))
2586 options = []
2587 empty = False
2588 none = opts["none"]
2589 for k, v in opt_list:
2590 if k is None:
2591 if none:
2592 empty = True
2593 if none is True:
2594 # Use the represent
2595 options.append((k, v))
2596 else:
2597 # Must be a string to use as the represent:
2598 options.append((k, none))
2599 else:
2600 options.append((k, v))
2601 if none and not empty:
2602 # Add the value anyway (e.g. not found via the reverse lookup)
2603 if none is True:
2604 none = current.messages["NONE"]
2605 options.append((None, none))
2606
2607 if not opts.get("multiple", True) and not self.values:
2608 # Browsers automatically select the first option in single-selects,
2609 # but that doesn't filter the data, so the first option must be
2610 # empty if we don't have a default:
2611 options.insert(0, ("", "")) # XML(" ") better?
2612
2613 # Sort the options
2614 return (ftype, options, opts.get("no_opts", NOOPT))
2615
2616 # -------------------------------------------------------------------------
2617 @staticmethod
2619 """
2620 Helper method to get all values of a URL query variable
2621
2622 @param get_vars: the GET vars (a dict)
2623 @param variable: the name of the query variable
2624
2625 @return: a list of values
2626 """
2627
2628 if not variable:
2629 return []
2630
2631 # Match __eq before checking any other operator
2632 selector = S3URLQuery.parse_key(variable)[0]
2633 for key in ("%s__eq" % selector, selector, variable):
2634 if key in get_vars:
2635 values = S3URLQuery.parse_value(get_vars[key])
2636 if not isinstance(values, (list, tuple)):
2637 values = [values]
2638 return values
2639
2640 return []
2641
2642 # =============================================================================
2643 -class S3HierarchyFilter(S3FilterWidget):
2644 """
2645 Filter widget for hierarchical types
2646
2647 Configuration Options (see also: S3HierarchyWidget):
2648
2649 @keyword lookup: name of the lookup table
2650 @keyword represent: representation method for the key
2651 @keyword multiple: allow selection of multiple options
2652 @keyword leafonly: only leaf nodes can be selected
2653 @keyword cascade: automatically select child nodes when
2654 selecting a parent node
2655 @keyword bulk_select: provide an option to select/deselect all nodes
2656 """
2657
2658 _class = "hierarchy-filter"
2659
2660 operator = "belongs"
2661
2662 # -------------------------------------------------------------------------
2664 """
2665 Render this widget as HTML helper object(s)
2666
2667 @param resource: the resource
2668 @param values: the search values from the URL query
2669 """
2670
2671 # Currently selected values
2672 selected = []
2673 append = selected.append
2674 if not isinstance(values, (list, tuple, set)):
2675 values = [values]
2676 for v in values:
2677 if isinstance(v, (int, long)) or str(v).isdigit():
2678 append(v)
2679
2680 # Resolve the field selector
2681 rfield = S3ResourceField(resource, self.field)
2682
2683 # Instantiate the widget
2684 opts = self.opts
2685 bulk_select = current.deployment_settings \
2686 .get_ui_hierarchy_filter_bulk_select_option()
2687 if bulk_select is None:
2688 bulk_select = opts.get("bulk_select", False)
2689
2690 if opts.get("widget") == "cascade":
2691 formstyle = current.deployment_settings.get_ui_filter_formstyle()
2692 w = S3CascadeSelectWidget(lookup = opts.get("lookup"),
2693 formstyle = formstyle,
2694 multiple = opts.get("multiple", True),
2695 filter = opts.get("filter"),
2696 leafonly = opts.get("leafonly", True),
2697 cascade = opts.get("cascade"),
2698 represent = opts.get("represent"),
2699 inline = True,
2700 )
2701 else:
2702 w = S3HierarchyWidget(lookup = opts.get("lookup"),
2703 multiple = opts.get("multiple", True),
2704 filter = opts.get("filter"),
2705 leafonly = opts.get("leafonly", True),
2706 cascade = opts.get("cascade", False),
2707 represent = opts.get("represent"),
2708 bulk_select = bulk_select,
2709 none = opts.get("none"),
2710 )
2711
2712 # Render the widget
2713 widget = w(rfield.field, selected, **self._attr(resource))
2714 widget.add_class(self._class)
2715
2716 return widget
2717
2718 # -------------------------------------------------------------------------
2720 """
2721 Generate the name for the URL query variable for this
2722 widget, detect alternative __typeof queries.
2723
2724 @param resource: the resource
2725 @return: the URL query variable name (or list of
2726 variable names if there are multiple operators)
2727 """
2728
2729 label, self.selector = self._selector(resource, self.field)
2730
2731 if not self.selector:
2732 return None
2733
2734 if "label" not in self.opts:
2735 self.opts["label"] = label
2736
2737 selector = self.selector
2738
2739 if self.alternatives and get_vars is not None:
2740 # Get the actual operator from get_vars
2741 operator = self._operator(get_vars, self.selector)
2742 if operator:
2743 self.operator = operator
2744
2745 variable = self._variable(selector, self.operator)
2746
2747 if not get_vars or not resource or variable in get_vars:
2748 return variable
2749
2750 # Detect and resolve __typeof queries
2751 resolve = S3ResourceQuery._resolve_hierarchy
2752 selector = resource.prefix_selector(selector)
2753 for key, value in get_vars.items():
2754
2755 if key.startswith(selector):
2756 selectors, op = S3URLQuery.parse_expression(key)[:2]
2757 else:
2758 continue
2759 if op != "typeof" or len(selectors) != 1:
2760 continue
2761
2762 rfield = resource.resolve_selector(selectors[0])
2763 if rfield.field:
2764 values = S3URLQuery.parse_value(value)
2765 field, nodeset, none = resolve(rfield.field, values)[1:]
2766 if field and (nodeset or none):
2767 if nodeset is None:
2768 nodeset = set()
2769 if none:
2770 nodeset.add(None)
2771 get_vars.pop(key, None)
2772 get_vars[variable] = [str(v) for v in nodeset]
2773 break
2774
2775 return variable
2776
2777 # =============================================================================
2778 -class S3NotEmptyFilter(S3FilterWidget):
2779 """
2780 Filter to check for presence of any (non-None) value in a field
2781 """
2782
2783 _class = "value-filter"
2784
2785 operator = "ne"
2786
2787 # -------------------------------------------------------------------------
2789 """
2790 Render this widget as HTML helper object(s)
2791
2792 @param resource: the resource
2793 @param values: the search values from the URL query
2794 """
2795
2796 attr = self.attr
2797 _class = self._class
2798 if "_class" in attr and attr["_class"]:
2799 _class = "%s %s" % (attr["_class"], _class)
2800 else:
2801 _class = _class
2802 attr["_class"] = _class
2803 attr["_type"] = "checkbox"
2804 attr["value"] = True if "None" in values else False
2805
2806 return INPUT(**attr)
2807
2808 # =============================================================================
2809 -class S3FilterForm(object):
2810 """ Helper class to construct and render a filter form for a resource """
2811
2813 """
2814 Constructor
2815
2816 @param widgets: the widgets (as list)
2817 @param attr: HTML attributes for this form
2818 """
2819
2820 self.widgets = widgets
2821
2822 attributes = Storage()
2823 options = Storage()
2824 for k, v in attr.iteritems():
2825 if k[0] == "_":
2826 attributes[k] = v
2827 else:
2828 options[k] = v
2829 self.attr = attributes
2830 self.opts = options
2831
2832 # -------------------------------------------------------------------------
2834 """
2835 Render this filter form as HTML form.
2836
2837 @param resource: the S3Resource
2838 @param get_vars: the request GET vars (URL query dict)
2839 @param target: the HTML element ID of the target object for
2840 this filter form (e.g. a datatable)
2841 @param alias: the resource alias to use in widgets
2842 """
2843
2844 attr = self.attr
2845 form_id = attr.get("_id")
2846 if not form_id:
2847 form_id = "filter-form"
2848 attr["_id"] = form_id
2849
2850 # Prevent issues with Webkit-based browsers & Back buttons
2851 attr["_autocomplete"] = "off"
2852
2853 opts_get = self.opts.get
2854 settings = current.deployment_settings
2855
2856 # Form style
2857 formstyle = opts_get("formstyle", None)
2858 if not formstyle:
2859 formstyle = settings.get_ui_filter_formstyle()
2860
2861 # Filter widgets
2862 rows = self._render_widgets(resource,
2863 get_vars=get_vars or {},
2864 alias=alias,
2865 formstyle=formstyle)
2866
2867 # Other filter form controls
2868 controls = self._render_controls(resource)
2869 if controls:
2870 rows.append(formstyle(None, "", controls, ""))
2871
2872 # Submit elements
2873 ajax = opts_get("ajax", False)
2874 submit = opts_get("submit", False)
2875 if submit:
2876 # Auto-submit?
2877 auto_submit = settings.get_ui_filter_auto_submit()
2878 if auto_submit and opts_get("auto_submit", True):
2879 script = '''S3.search.filterFormAutoSubmit('%s',%s)''' % \
2880 (form_id, auto_submit)
2881 current.response.s3.jquery_ready.append(script)
2882
2883 # Custom label and class
2884 _class = None
2885 if submit is True:
2886 label = current.T("Search")
2887 elif isinstance(submit, (list, tuple)):
2888 label, _class = submit
2889 else:
2890 label = submit
2891
2892 # Submit button
2893 submit_button = INPUT(_type="button",
2894 _value=label,
2895 _class="filter-submit")
2896 if _class:
2897 submit_button.add_class(_class)
2898
2899 # Where to request filtered data from:
2900 submit_url = opts_get("url", URL(vars={}))
2901
2902 # Where to request updated options from:
2903 ajax_url = opts_get("ajaxurl", URL(args=["filter.options"], vars={}))
2904
2905 # Submit row elements
2906 submit = TAG[""](submit_button,
2907 INPUT(_type="hidden",
2908 _class="filter-ajax-url",
2909 _value=ajax_url),
2910 INPUT(_type="hidden",
2911 _class="filter-submit-url",
2912 _value=submit_url))
2913 if ajax and target:
2914 submit.append(INPUT(_type="hidden",
2915 _class="filter-submit-target",
2916 _value=target))
2917
2918 # Append submit row
2919 submit_row = formstyle(None, "", submit, "")
2920 if auto_submit and hasattr(submit_row, "add_class"):
2921 submit_row.add_class("hide")
2922 rows.append(submit_row)
2923
2924 # Filter Manager (load/apply/save filters)
2925 fm = settings.get_search_filter_manager()
2926 if fm and opts_get("filter_manager", resource is not None):
2927 filter_manager = self._render_filters(resource, form_id)
2928 if filter_manager:
2929 fmrow = formstyle(None, "", filter_manager, "")
2930 if hasattr(fmrow, "add_class"):
2931 fmrow.add_class("hide filter-manager-row")
2932 rows.append(fmrow)
2933
2934 # Adapt to formstyle: render a TABLE only if formstyle returns TRs
2935 if rows:
2936 elements = rows[0]
2937 if not isinstance(elements, (list, tuple)):
2938 elements = elements.elements()
2939 n = len(elements)
2940 if n > 0 and elements[0].tag == "tr" or \
2941 n > 1 and elements[0].tag == "" and elements[1].tag == "tr":
2942 form = FORM(TABLE(TBODY(rows)), **attr)
2943 else:
2944 form = FORM(DIV(rows), **attr)
2945 if settings.ui.formstyle == "bootstrap":
2946 # We need to amend the HTML markup to support this CSS framework
2947 form.add_class("form-horizontal")
2948 form.add_class("filter-form")
2949 if ajax:
2950 form.add_class("filter-ajax")
2951 else:
2952 return ""
2953
2954 # Put a copy of formstyle into the form for access by the view
2955 form.formstyle = formstyle
2956 return form
2957
2958 # -------------------------------------------------------------------------
2960 """
2961 Render the filter widgets without FORM wrapper, e.g. to
2962 embed them as fieldset in another form.
2963
2964 @param resource: the S3Resource
2965 @param get_vars: the request GET vars (URL query dict)
2966 @param alias: the resource alias to use in widgets
2967 """
2968
2969 formstyle = self.opts.get("formstyle", None)
2970 if not formstyle:
2971 formstyle = current.deployment_settings.get_ui_filter_formstyle()
2972
2973 rows = self._render_widgets(resource,
2974 get_vars=get_vars,
2975 alias=alias,
2976 formstyle=formstyle)
2977
2978 controls = self._render_controls(resource)
2979 if controls:
2980 rows.append(formstyle(None, "", controls, ""))
2981
2982 # Adapt to formstyle: only render a TABLE if formstyle returns TRs
2983 if rows:
2984 elements = rows[0]
2985 if not isinstance(elements, (list, tuple)):
2986 elements = elements.elements()
2987 n = len(elements)
2988 if n > 0 and elements[0].tag == "tr" or \
2989 n > 1 and elements[0].tag == "" and elements[1].tag == "tr":
2990 fields = TABLE(TBODY(rows))
2991 else:
2992 fields = DIV(rows)
2993
2994 return fields
2995
2996 # -------------------------------------------------------------------------
2998 """
2999 Render optional additional filter form controls: advanced
3000 options toggle, clear filters.
3001 """
3002
3003 T = current.T
3004 controls = []
3005 opts = self.opts
3006
3007 advanced = opts.get("advanced", False)
3008 if advanced:
3009 _class = "filter-advanced"
3010 if advanced is True:
3011 label = T("More Options")
3012 elif isinstance(advanced, (list, tuple)):
3013 label = advanced[0]
3014 label = advanced[1]
3015 if len(advanced > 2):
3016 _class = "%s %s" % (advanced[2], _class)
3017 else:
3018 label = advanced
3019 label_off = T("Less Options")
3020 advanced = A(SPAN(label,
3021 data = {"on": label,
3022 "off": label_off,
3023 },
3024 _class="filter-advanced-label",
3025 ),
3026 ICON("down"),
3027 ICON("up", _style="display:none"),
3028 _class=_class
3029 )
3030 controls.append(advanced)
3031
3032 clear = opts.get("clear", True)
3033 if clear:
3034 _class = "filter-clear"
3035 if clear is True:
3036 label = T("Clear Filter")
3037 elif isinstance(clear, (list, tuple)):
3038 label = clear[0]
3039 _class = "%s %s" % (clear[1], _class)
3040 else:
3041 label = clear
3042 clear = A(label, _class=_class)
3043 clear.add_class("action-lnk")
3044 controls.append(clear)
3045
3046 fm = current.deployment_settings.get_search_filter_manager()
3047 if fm and opts.get("filter_manager", resource is not None):
3048 show_fm = A(T("Saved Filters"),
3049 _class="show-filter-manager action-lnk")
3050 controls.append(show_fm)
3051
3052 if controls:
3053 return DIV(controls, _class="filter-controls")
3054 else:
3055 return None
3056
3057 # -------------------------------------------------------------------------
3058 - def _render_widgets(self,
3059 resource,
3060 get_vars=None,
3061 alias=None,
3062 formstyle=None):
3063 """
3064 Render the filter widgets
3065
3066 @param resource: the S3Resource
3067 @param get_vars: the request GET vars (URL query dict)
3068 @param alias: the resource alias to use in widgets
3069 @param formstyle: the formstyle to use
3070
3071 @return: a list of form rows
3072 """
3073
3074 rows = []
3075 rappend = rows.append
3076 advanced = False
3077 for f in self.widgets:
3078 if not f:
3079 continue
3080 widget = f(resource, get_vars, alias=alias)
3081 widget_opts = f.opts
3082 label = widget_opts["label"]
3083 comment = widget_opts["comment"]
3084 hidden = widget_opts["hidden"]
3085 widget_formstyle = widget_opts.get("formstyle", formstyle)
3086 if hidden:
3087 advanced = True
3088 widget_id = f.attr["_id"]
3089 if widget_id:
3090 row_id = "%s__row" % widget_id
3091 label_id = "%s__label" % widget_id
3092 else:
3093 row_id = None
3094 label_id = None
3095 if label:
3096 label = LABEL("%s:" % label, _id=label_id, _for=widget_id)
3097 elif label is not False:
3098 label = ""
3099 if not comment:
3100 comment = ""
3101 formrow = widget_formstyle(row_id, label, widget, comment, hidden=hidden)
3102 if hidden:
3103 if isinstance(formrow, DIV):
3104 formrow.add_class("advanced")
3105 elif isinstance(formrow, tuple):
3106 for item in formrow:
3107 if hasattr(item, "add_class"):
3108 item.add_class("advanced")
3109 rappend(formrow)
3110 if advanced:
3111 if resource:
3112 self.opts["advanced"] = resource.get_config(
3113 "filter_advanced", True)
3114 else:
3115 self.opts["advanced"] = True
3116 return rows
3117
3118 # -------------------------------------------------------------------------
3120 """
3121 Render a filter manager widget
3122
3123 @param resource: the resource
3124 @return: the widget
3125 """
3126
3127 SELECT_FILTER = current.T("Saved Filters")
3128
3129 ajaxurl = self.opts.get("saveurl", URL(args=["filter.json"], vars={}))
3130
3131 # Current user
3132 auth = current.auth
3133 pe_id = auth.user.pe_id if auth.s3_logged_in() else None
3134 if not pe_id:
3135 return None
3136
3137 table = current.s3db.pr_filter
3138 query = (table.deleted != True) & \
3139 (table.pe_id == pe_id)
3140
3141 if resource:
3142 query &= (table.resource == resource.tablename)
3143 else:
3144 query &= (table.resource == None)
3145
3146 rows = current.db(query).select(table._id,
3147 table.title,
3148 table.query,
3149 orderby=table.title)
3150
3151 options = [OPTION(SELECT_FILTER,
3152 _value="",
3153 _class="filter-manager-prompt",
3154 _disabled="disabled")]
3155 add_option = options.append
3156 filters = {}
3157 for row in rows:
3158 filter_id = row[table._id]
3159 add_option(OPTION(row.title, _value=filter_id))
3160 query = row.query
3161 if query:
3162 query = json.loads(query)
3163 filters[filter_id] = query
3164 widget_id = "%s-fm" % form_id
3165 widget = DIV(SELECT(options,
3166 _id=widget_id,
3167 _class="filter-manager-widget"),
3168 _class="filter-manager-container")
3169
3170 # JSON-serializable translator
3171 T = current.T
3172 _t = lambda s: s3_str(T(s))
3173
3174 # Configure the widget
3175 settings = current.deployment_settings
3176 config = dict(
3177
3178 # Filters and Ajax URL
3179 filters = filters,
3180 ajaxURL = ajaxurl,
3181
3182 # Workflow Options
3183 allowDelete = settings.get_search_filter_manager_allow_delete(),
3184
3185 # Tooltips for action icons/buttons
3186 createTooltip = _t("Save current options as new filter"),
3187 loadTooltip = _t("Load filter"),
3188 saveTooltip = _t("Update saved filter"),
3189 deleteTooltip = _t("Delete saved filter"),
3190
3191 # Hints
3192 titleHint = _t("Enter a title..."),
3193 selectHint = s3_str(SELECT_FILTER),
3194 emptyHint = _t("No saved filters"),
3195
3196 # Confirm update + confirmation text
3197 confirmUpdate = _t("Update this filter?"),
3198 confirmDelete = _t("Delete this filter?"),
3199 )
3200
3201 # Render actions as buttons with text if configured, otherwise
3202 # they will appear as empty DIVs with classes for CSS icons
3203 create_text = settings.get_search_filter_manager_save()
3204 if create_text:
3205 config["createText"] = _t(create_text)
3206 update_text = settings.get_search_filter_manager_update()
3207 if update_text:
3208 config["saveText"] = _t(update_text)
3209 delete_text = settings.get_search_filter_manager_delete()
3210 if delete_text:
3211 config["deleteText"] = _t(delete_text)
3212 load_text = settings.get_search_filter_manager_load()
3213 if load_text:
3214 config["loadText"] = _t(load_text)
3215
3216 script = '''$("#%s").filtermanager(%s)''' % \
3217 (widget_id,
3218 json.dumps(config, separators=SEPARATORS))
3219
3220 current.response.s3.jquery_ready.append(script)
3221
3222 return widget
3223
3224 # -------------------------------------------------------------------------
3226 """
3227 Render this filter form as JSON (for Ajax requests)
3228
3229 @param resource: the S3Resource
3230 @param get_vars: the request GET vars (URL query dict)
3231 """
3232
3233 raise NotImplementedError
3234
3235 # -------------------------------------------------------------------------
3236 @staticmethod
3238 """
3239 Add default filters to resource, to be called a multi-record
3240 view with a filter form is rendered the first time and before
3241 the view elements get processed
3242
3243 @param request: the request
3244 @param resource: the resource
3245
3246 @return: dict with default filters (URL vars)
3247 """
3248
3249 s3 = current.response.s3
3250
3251 get_vars = request.get_vars
3252 tablename = resource.tablename
3253
3254 default_filters = {}
3255
3256 # Do we have filter defaults for this resource?
3257 filter_defaults = s3
3258 for level in ("filter_defaults", tablename):
3259 if level not in filter_defaults:
3260 filter_defaults = None
3261 break
3262 filter_defaults = filter_defaults[level]
3263
3264 # Which filter widgets do we need to apply defaults for?
3265 filter_widgets = resource.get_config("filter_widgets")
3266 for filter_widget in filter_widgets:
3267
3268 # Do not apply defaults of hidden widgets because they are
3269 # not visible to the user:
3270 if not filter_widget or filter_widget.opts.hidden:
3271 continue
3272
3273 has_default = False
3274 if "default" in filter_widget.opts:
3275 has_default = True
3276 elif filter_defaults is None:
3277 continue
3278
3279 defaults = set()
3280 variable = filter_widget.variable(resource, get_vars)
3281 multiple = type(variable) is list
3282
3283 # Do we have a corresponding value in get_vars?
3284 if multiple:
3285 for k in variable:
3286 values = filter_widget._values(get_vars, k)
3287 if values:
3288 filter_widget.values[k] = values
3289 else:
3290 defaults.add(k)
3291 else:
3292 values = filter_widget._values(get_vars, variable)
3293 if values:
3294 filter_widget.values[variable] = values
3295 else:
3296 defaults.add(variable)
3297
3298 # Extract widget default
3299 if has_default:
3300 widget_default = filter_widget.opts["default"]
3301 if not isinstance(widget_default, dict):
3302 if multiple:
3303 widget_default = dict((k, widget_default)
3304 for k in variable)
3305 else:
3306 widget_default = {variable: widget_default}
3307 for k in widget_default:
3308 if k not in filter_widget.values:
3309 defaults.add(k)
3310 else:
3311 widget_default = {}
3312
3313 for variable in defaults:
3314 selector, operator, invert = S3URLQuery.parse_key(variable)
3315 if invert:
3316 operator = "%s!" % operator
3317
3318 if filter_defaults and selector in filter_defaults:
3319 applicable_defaults = filter_defaults[selector]
3320 elif variable in widget_default:
3321 applicable_defaults = widget_default[variable]
3322 else:
3323 continue
3324
3325 if callable(applicable_defaults):
3326 applicable_defaults = applicable_defaults(selector,
3327 tablename=tablename)
3328 if isinstance(applicable_defaults, dict):
3329 if operator in applicable_defaults:
3330 default = applicable_defaults[operator]
3331 else:
3332 continue
3333 elif operator in (None, "belongs", "eq", "ne"):
3334 default = applicable_defaults
3335 else:
3336 continue
3337 if default is None:
3338 # Ignore (return [None] to filter for None)
3339 continue
3340 elif not isinstance(default, list):
3341 default = [default]
3342 filter_widget.values[variable] = [str(v) if v is None else v
3343 for v in default]
3344 default_filters[variable] = ",".join(s3_unicode(v)
3345 for v in default)
3346
3347 # Apply to resource
3348 queries = S3URLQuery.parse(resource, default_filters)
3349 add_filter = resource.add_filter
3350 for alias in queries:
3351 for q in queries[alias]:
3352 add_filter(q)
3353
3354 return default_filters
3355
3356 # =============================================================================
3357 -class S3Filter(S3Method):
3358 """ Back-end for filter forms """
3359
3361 """
3362 Entry point for REST interface
3363
3364 @param r: the S3Request
3365 @param attr: additional controller parameters
3366 """
3367
3368 representation = r.representation
3369 if representation == "options":
3370 # Return the filter options as JSON
3371 return self._options(r, **attr)
3372
3373 elif representation == "json":
3374 if r.http == "GET":
3375 # Load list of saved filters
3376 return self._load(r, **attr)
3377 elif r.http == "POST":
3378 if "delete" in r.get_vars:
3379 # Delete a filter
3380 return self._delete(r, **attr)
3381 else:
3382 # Save a filter
3383 return self._save(r, **attr)
3384 else:
3385 r.error(405, current.ERROR.BAD_METHOD)
3386
3387 elif representation == "html":
3388 return self._form(r, **attr)
3389
3390 else:
3391 r.error(415, current.ERROR.BAD_FORMAT)
3392
3393 # -------------------------------------------------------------------------
3395 """
3396 Get the filter form for the target resource as HTML snippet
3397
3398 GET filter.html
3399
3400 @param r: the S3Request
3401 @param attr: additional controller parameters
3402 """
3403
3404 r.error(501, current.ERROR.NOT_IMPLEMENTED)
3405
3406 # -------------------------------------------------------------------------
3408 """
3409 Get the updated options for the filter form for the target
3410 resource as JSON.
3411 NB These use a fresh resource, so filter vars are not respected.
3412 s3.filter if respected, so if you need to filter the options, then
3413 can apply filter vars to s3.filter in customise() if the controller
3414 is not the same as the calling one!
3415
3416 GET filter.options
3417
3418 @param r: the S3Request
3419 @param attr: additional controller parameters (ignored currently)
3420 """
3421
3422 resource = self.resource
3423
3424 options = {}
3425
3426 filter_widgets = resource.get_config("filter_widgets", None)
3427 if filter_widgets:
3428 fresource = current.s3db.resource(resource.tablename,
3429 filter = current.response.s3.filter,
3430 )
3431
3432 for widget in filter_widgets:
3433 if hasattr(widget, "ajax_options"):
3434 opts = widget.ajax_options(fresource)
3435 if opts and isinstance(opts, dict):
3436 options.update(opts)
3437
3438 options = json.dumps(options, separators=SEPARATORS)
3439 current.response.headers["Content-Type"] = "application/json"
3440 return options
3441
3442 # -------------------------------------------------------------------------
3443 @staticmethod
3445 """
3446 Delete a filter, responds to POST filter.json?delete=
3447
3448 @param r: the S3Request
3449 @param attr: additional controller parameters
3450 """
3451
3452 # Authorization, get pe_id
3453 auth = current.auth
3454 if auth.s3_logged_in():
3455 pe_id = current.auth.user.pe_id
3456 else:
3457 pe_id = None
3458 if not pe_id:
3459 r.unauthorised()
3460
3461 # Read the source
3462 source = r.body
3463 source.seek(0)
3464
3465 try:
3466 data = json.load(source)
3467 except ValueError:
3468 # Syntax error: no JSON data
3469 r.error(400, current.ERROR.BAD_SOURCE)
3470
3471 # Try to find the record
3472 db = current.db
3473 s3db = current.s3db
3474
3475 table = s3db.pr_filter
3476 record = None
3477 record_id = data.get("id")
3478 if record_id:
3479 query = (table.id == record_id) & (table.pe_id == pe_id)
3480 record = db(query).select(table.id, limitby=(0, 1)).first()
3481 if not record:
3482 r.error(404, current.ERROR.BAD_RECORD)
3483
3484 resource = s3db.resource("pr_filter", id=record_id)
3485 success = resource.delete(format=r.representation)
3486
3487 if not success:
3488 r.error(400, resource.error)
3489 else:
3490 current.response.headers["Content-Type"] = "application/json"
3491 return current.xml.json_message(deleted=record_id)
3492
3493 # -------------------------------------------------------------------------
3495 """
3496 Save a filter, responds to POST filter.json
3497
3498 @param r: the S3Request
3499 @param attr: additional controller parameters
3500 """
3501
3502 # Authorization, get pe_id
3503 auth = current.auth
3504 if auth.s3_logged_in():
3505 pe_id = current.auth.user.pe_id
3506 else:
3507 pe_id = None
3508 if not pe_id:
3509 r.unauthorised()
3510
3511 # Read the source
3512 source = r.body
3513 source.seek(0)
3514
3515 try:
3516 data = json.load(source)
3517 except ValueError:
3518 r.error(501, current.ERROR.BAD_SOURCE)
3519
3520 # Try to find the record
3521 db = current.db
3522 s3db = current.s3db
3523
3524 table = s3db.pr_filter
3525 record_id = data.get("id")
3526 record = None
3527 if record_id:
3528 query = (table.id == record_id) & (table.pe_id == pe_id)
3529 record = db(query).select(table.id, limitby=(0, 1)).first()
3530 if not record:
3531 r.error(404, current.ERROR.BAD_RECORD)
3532
3533 # Build new record
3534 filter_data = {
3535 "pe_id": pe_id,
3536 "controller": r.controller,
3537 "function": r.function,
3538 "resource": self.resource.tablename,
3539 "deleted": False,
3540 }
3541
3542 title = data.get("title")
3543 if title is not None:
3544 filter_data["title"] = title
3545
3546 description = data.get("description")
3547 if description is not None:
3548 filter_data["description"] = description
3549
3550 query = data.get("query")
3551 if query is not None:
3552 filter_data["query"] = json.dumps(query)
3553
3554 url = data.get("url")
3555 if url is not None:
3556 filter_data["url"] = url
3557
3558 # Store record
3559 onaccept = None
3560 form = Storage(vars=filter_data)
3561 if record:
3562 success = db(table.id == record_id).update(**filter_data)
3563 if success:
3564 current.audit("update", "pr", "filter", form, record_id, "json")
3565 info = {"updated": record_id}
3566 onaccept = s3db.get_config(table, "update_onaccept",
3567 s3db.get_config(table, "onaccept"))
3568 else:
3569 success = table.insert(**filter_data)
3570 if success:
3571 record_id = success
3572 current.audit("create", "pr", "filter", form, record_id, "json")
3573 info = {"created": record_id}
3574 onaccept = s3db.get_config(table, "update_onaccept",
3575 s3db.get_config(table, "onaccept"))
3576
3577 if onaccept is not None:
3578 form.vars["id"] = record_id
3579 callback(onaccept, form)
3580
3581 # Success/Error response
3582 xml = current.xml
3583 if success:
3584 msg = xml.json_message(**info)
3585 else:
3586 msg = xml.json_message(False, 400)
3587 current.response.headers["Content-Type"] = "application/json"
3588 return msg
3589
3590 # -------------------------------------------------------------------------
3592 """
3593 Load filters
3594
3595 GET filter.json or GET filter.json?load=<id>
3596
3597 @param r: the S3Request
3598 @param attr: additional controller parameters
3599 """
3600
3601 db = current.db
3602 table = current.s3db.pr_filter
3603
3604 # Authorization, get pe_id
3605 auth = current.auth
3606 if auth.s3_logged_in():
3607 pe_id = current.auth.user.pe_id
3608 else:
3609 pe_id = None
3610 if not pe_id:
3611 r.unauthorized()
3612
3613 # Build query
3614 query = (table.deleted != True) & \
3615 (table.resource == self.resource.tablename) & \
3616 (table.pe_id == pe_id)
3617
3618 # Any particular filters?
3619 load = r.get_vars.get("load")
3620 if load:
3621 record_ids = [i for i in load.split(",") if i.isdigit()]
3622 if record_ids:
3623 if len(record_ids) > 1:
3624 query &= table.id.belongs(record_ids)
3625 else:
3626 query &= table.id == record_ids[0]
3627 else:
3628 record_ids = None
3629
3630 # Retrieve filters
3631 rows = db(query).select(table.id,
3632 table.title,
3633 table.description,
3634 table.query)
3635
3636 # Pack filters
3637 filters = []
3638 for row in rows:
3639 filters.append({
3640 "id": row.id,
3641 "title": row.title,
3642 "description": row.description,
3643 "query": json.loads(row.query) if row.query else [],
3644 })
3645
3646 # JSON response
3647 current.response.headers["Content-Type"] = "application/json"
3648 return json.dumps(filters, separators=SEPARATORS)
3649
3650 # =============================================================================
3651 -class S3FilterString(object):
3652 """
3653 Helper class to render a human-readable representation of a
3654 filter query, as representation method of JSON-serialized
3655 queries in saved filters.
3656 """
3657
3659 """
3660 Constructor
3661
3662 @param query: the URL query (list of key-value pairs or a
3663 string with such a list in JSON)
3664 """
3665
3666 if type(query) is not list:
3667 try:
3668 self.query = json.loads(query)
3669 except ValueError:
3670 self.query = []
3671 else:
3672 self.query = query
3673
3674 get_vars = {}
3675 for k, v in self.query:
3676 if v is not None:
3677 key = resource.prefix_selector(k)
3678 if key in get_vars:
3679 value = get_vars[key]
3680 if type(value) is list:
3681 value.append(v)
3682 else:
3683 get_vars[key] = [value, v]
3684 else:
3685 get_vars[key] = v
3686
3687 self.resource = resource
3688 self.get_vars = get_vars
3689
3690 # -------------------------------------------------------------------------
3692 """ Render the query representation for the given resource """
3693
3694 default = ""
3695
3696 get_vars = self.get_vars
3697 resource = self.resource
3698 if not get_vars:
3699 return default
3700 else:
3701 queries = S3URLQuery.parse(resource, get_vars)
3702
3703 # Get alternative field labels
3704 labels = {}
3705 get_config = resource.get_config
3706 prefix = resource.prefix_selector
3707 for config in ("list_fields", "notify_fields"):
3708 fields = get_config(config, set())
3709 for f in fields:
3710 if type(f) is tuple:
3711 labels[prefix(f[1])] = f[0]
3712
3713 # Iterate over the sub-queries
3714 render = self._render
3715 substrings = []
3716 append = substrings.append
3717 for alias, subqueries in queries.iteritems():
3718
3719 for subquery in subqueries:
3720 s = render(resource, alias, subquery, labels=labels)
3721 if s:
3722 append(s)
3723
3724 if substrings:
3725 result = substrings[0]
3726 T = current.T
3727 for s in substrings[1:]:
3728 result = T("%s AND %s") % (result, s)
3729 return result
3730 else:
3731 return default
3732
3733 # -------------------------------------------------------------------------
3734 @classmethod
3736 """
3737 Recursively render a human-readable representation of a
3738 S3ResourceQuery.
3739
3740 @param resource: the S3Resource
3741 @param query: the S3ResourceQuery
3742 @param invert: invert the query
3743 """
3744
3745 T = current.T
3746
3747 if not query:
3748 return None
3749
3750 op = query.op
3751
3752 l = query.left
3753 r = query.right
3754 render = lambda q, r=resource, a=alias, invert=False, labels=labels: \
3755 cls._render(r, a, q, invert=invert, labels=labels)
3756
3757 if op == query.AND:
3758 # Recurse AND
3759 l = render(l)
3760 r = render(r)
3761 if l is not None and r is not None:
3762 if invert:
3763 result = T("NOT %s OR NOT %s") % (l, r)
3764 else:
3765 result = T("%s AND %s") % (l, r)
3766 else:
3767 result = l if l is not None else r
3768 elif op == query.OR:
3769 # Recurse OR
3770 l = render(l)
3771 r = render(r)
3772 if l is not None and r is not None:
3773 if invert:
3774 result = T("NOT %s AND NOT %s") % (l, r)
3775 else:
3776 result = T("%s OR %s") % (l, r)
3777 else:
3778 result = l if l is not None else r
3779 elif op == query.NOT:
3780 # Recurse NOT
3781 result = render(l, invert=not invert)
3782 else:
3783 # Resolve the field selector against the resource
3784 try:
3785 rfield = l.resolve(resource)
3786 except (AttributeError, SyntaxError):
3787 return None
3788
3789 # Convert the filter values into the field type
3790 try:
3791 values = cls._convert(rfield, r)
3792 except (TypeError, ValueError):
3793 values = r
3794
3795 # Alias
3796 selector = l.name
3797 if labels and selector in labels:
3798 rfield.label = labels[selector]
3799 # @todo: for duplicate labels, show the table name
3800 #else:
3801 #tlabel = " ".join(s.capitalize() for s in rfield.tname.split("_")[1:])
3802 #rfield.label = "(%s) %s" % (tlabel, rfield.label)
3803
3804 # Represent the values
3805 if values is None:
3806 values = T("None")
3807 else:
3808 list_type = rfield.ftype[:5] == "list:"
3809 renderer = rfield.represent
3810 if not callable(renderer):
3811 renderer = s3_unicode
3812 if hasattr(renderer, "linkto"):
3813 #linkto = renderer.linkto
3814 renderer.linkto = None
3815 #else:
3816 # #linkto = None
3817
3818 is_list = type(values) is list
3819
3820 try:
3821 if is_list and hasattr(renderer, "bulk") and not list_type:
3822 fvalues = renderer.bulk(values, list_type=False)
3823 values = [fvalues[v] for v in values if v in fvalues]
3824 elif list_type:
3825 if is_list:
3826 values = renderer(values)
3827 else:
3828 values = renderer([values])
3829 else:
3830 if is_list:
3831 values = [renderer(v) for v in values]
3832 else:
3833 values = renderer(values)
3834 except:
3835 values = s3_unicode(values)
3836
3837 # Translate the query
3838 result = cls._translate_query(query, rfield, values, invert=invert)
3839
3840 return result
3841
3842 # -------------------------------------------------------------------------
3843 @classmethod
3845 """
3846 Convert a filter value according to the field type
3847 before representation
3848
3849 @param rfield: the S3ResourceField
3850 @param value: the value
3851 """
3852
3853 if value is None:
3854 return value
3855
3856 ftype = rfield.ftype
3857 if ftype[:5] == "list:":
3858 if ftype[5:8] in ("int", "ref"):
3859 ftype = long
3860 else:
3861 ftype = unicode
3862 elif ftype == "id" or ftype [:9] == "reference":
3863 ftype = long
3864 elif ftype == "integer":
3865 ftype = int
3866 elif ftype == "date":
3867 ftype = datetime.date
3868 elif ftype == "time":
3869 ftype = datetime.time
3870 elif ftype == "datetime":
3871 ftype = datetime.datetime
3872 elif ftype == "double":
3873 ftype = float
3874 elif ftype == "boolean":
3875 ftype = bool
3876 else:
3877 ftype = unicode
3878
3879 convert = S3TypeConverter.convert
3880 if type(value) is list:
3881 output = []
3882 append = output.append
3883 for v in value:
3884 try:
3885 append(convert(ftype, v))
3886 except (TypeError, ValueError):
3887 continue
3888 else:
3889 try:
3890 output = convert(ftype, value)
3891 except (TypeError, ValueError):
3892 output = None
3893 return output
3894
3895 # -------------------------------------------------------------------------
3896 @classmethod
3898 """
3899 Translate the filter query into human-readable language
3900
3901 @param query: the S3ResourceQuery
3902 @param rfield: the S3ResourceField the query refers to
3903 @param values: the filter values
3904 @param invert: invert the operation
3905 """
3906
3907 T = current.T
3908
3909 # Value list templates
3910 vor = T("%s or %s")
3911 vand = T("%s and %s")
3912
3913 # Operator templates
3914 otemplates = {
3915 query.LT: (query.GE, vand, "%(label)s < %(values)s"),
3916 query.LE: (query.GT, vand, "%(label)s <= %(values)s"),
3917 query.EQ: (query.NE, vor, T("%(label)s is %(values)s")),
3918 query.GE: (query.LT, vand, "%(label)s >= %(values)s"),
3919 query.GT: (query.LE, vand, "%(label)s > %(values)s"),
3920 query.NE: (query.EQ, vor, T("%(label)s != %(values)s")),
3921 query.LIKE: ("notlike", vor, T("%(label)s like %(values)s")),
3922 query.BELONGS: (query.NE, vor, T("%(label)s = %(values)s")),
3923 query.CONTAINS: ("notall", vand, T("%(label)s contains %(values)s")),
3924 query.ANYOF: ("notany", vor, T("%(label)s contains any of %(values)s")),
3925 "notall": (query.CONTAINS, vand, T("%(label)s does not contain %(values)s")),
3926 "notany": (query.ANYOF, vor, T("%(label)s does not contain %(values)s")),
3927 "notlike": (query.LIKE, vor, T("%(label)s not like %(values)s"))
3928 }
3929
3930 # Quote values as necessary
3931 ftype = rfield.ftype
3932 if ftype in ("string", "text") or \
3933 ftype[:9] == "reference" or \
3934 ftype[:5] == "list:" and ftype[5:8] in ("str", "ref"):
3935 if type(values) is list:
3936 values = ['"%s"' % v for v in values]
3937 elif values is not None:
3938 values = '"%s"' % values
3939 else:
3940 values = current.messages["NONE"]
3941
3942 # Render value list template
3943 def render_values(template=None, values=None):
3944 if not template or type(values) is not list:
3945 return str(values)
3946 elif not values:
3947 return "()"
3948 elif len(values) == 1:
3949 return values[0]
3950 else:
3951 return template % (", ".join(values[:-1]), values[-1])
3952
3953 # Render the operator template
3954 op = query.op
3955 if op in otemplates:
3956 inversion, vtemplate, otemplate = otemplates[op]
3957 if invert:
3958 inversion, vtemplate, otemplate = otemplates[inversion]
3959 return otemplate % {"label": rfield.label,
3960 "values":render_values(vtemplate, values),
3961 }
3962 else:
3963 # Fallback to simple representation
3964 return query.represent(rfield.resource)
3965
3966 # =============================================================================
3967 -def s3_get_filter_opts(tablename,
3968 fieldname = "name",
3969 location_filter = False,
3970 org_filter = False,
3971 key = "id",
3972 none = False,
3973 orderby = None,
3974 translate = False,
3975 ):
3976 """
3977 Lazy options getter - this is useful when the expected number
3978 of options is significantly smaller than the number of records
3979 to iterate through
3980
3981 @note: unlike the built-in reverse lookup in S3OptionsFilter, this
3982 function does *not* check whether the options are actually
3983 in use - so it can be used to enforce filter options to be
3984 shown even if there are no records matching them.
3985
3986 @param tablename: the name of the lookup table
3987 @param fieldname: the name of the field to represent options with
3988 @param location_filter: whether to filter the values by location
3989 @param org_filter: whether to filter the values by root_org
3990 @param key: the option key field (if not "id", e.g. a super key)
3991 @param none: whether to include an option for None
3992 @param orderby: orderby-expression as alternative to alpha-sorting
3993 of options in widget (=> set widget sort=False)
3994 @param translate: whether to translate the values
3995 """
3996
3997 auth = current.auth
3998 table = current.s3db.table(tablename)
3999
4000 if auth.s3_has_permission("read", table):
4001 query = auth.s3_accessible_query("read", table)
4002 if "deleted" in table.fields:
4003 query &= (table.deleted != True)
4004 if location_filter:
4005 location = current.session.s3.location_filter
4006 if location:
4007 query &= (table.location_id == location)
4008 if org_filter:
4009 root_org = auth.root_org()
4010 if root_org:
4011 query &= ((table.organisation_id == root_org) | \
4012 (table.organisation_id == None))
4013 #else:
4014 # query &= (table.organisation_id == None)
4015 if orderby is None:
4016 # Options are alpha-sorted later in widget
4017 odict = dict
4018 else:
4019 # Options-dict to retain order
4020 odict = OrderedDict
4021 rows = current.db(query).select(table[key],
4022 table[fieldname],
4023 orderby = orderby,
4024 )
4025
4026 if translate:
4027 T = current.T
4028 opts = odict((row[key], T(row[fieldname])) for row in rows)
4029 else:
4030 opts = odict((row[key], row[fieldname]) for row in rows)
4031 if none:
4032 opts[None] = current.messages["NONE"]
4033 else:
4034 opts = {}
4035 return opts
4036
4037 # =============================================================================
4038 -def s3_set_default_filter(selector, value, tablename=None):
4039 """
4040 Set a default filter for selector.
4041
4042 @param selector: the field selector
4043 @param value: the value, can be a dict {operator: value},
4044 a list of values, or a single value, or a
4045 callable that returns any of these
4046 @param tablename: the tablename
4047 """
4048
4049 s3 = current.response.s3
4050
4051 filter_defaults = s3
4052 for level in ("filter_defaults", tablename):
4053 if level not in filter_defaults:
4054 filter_defaults[level] = {}
4055 filter_defaults = filter_defaults[level]
4056 filter_defaults[selector] = value
4057
4058 # END =========================================================================
4059
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:52:00 2019 | http://epydoc.sourceforge.net |