Package s3 :: Module s3filter
[frames] | no frames]

Source Code for Module s3.s3filter

   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 # -------------------------------------------------------------------------
90 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
102 - def variable(self, resource, get_vars=None):
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 # -------------------------------------------------------------------------
136 - def data_element(self, variable):
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 #
154 - def __init__(self, field=None, **attr):
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 # -------------------------------------------------------------------------
188 - def __call__(self, resource, get_vars=None, alias=None):
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 # -------------------------------------------------------------------------
241 - def _attr(self, resource):
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
273 - def _operator(cls, get_vars, selector):
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 # -------------------------------------------------------------------------
295 - def _prefix(self, selector):
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 # -------------------------------------------------------------------------
324 - def _selector(self, resource, fields):
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
365 - def _values(get_vars, variable):
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
387 - def _variable(cls, selector, operator):
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 # -------------------------------------------------------------------------
423 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
476 - def data_element(self, variables):
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 # -------------------------------------------------------------------------
510 - def ajax_options(self, resource):
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 # -------------------------------------------------------------------------
526 - def _options(self, resource):
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 # -------------------------------------------------------------------------
563 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
637 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
746 - def data_element(self, variables):
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 # -------------------------------------------------------------------------
784 - def ajax_options(self, resource):
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 # -------------------------------------------------------------------------
811 - def _options(self, resource, as_str=True):
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 # -------------------------------------------------------------------------
914 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
1163 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
1292 - def __init__(self, field=None, **attr):
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 # -------------------------------------------------------------------------
1316 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
1453 - def data_element(self, variable):
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 # -------------------------------------------------------------------------
1475 - def ajax_options(self, resource):
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
1492 - def __options(row, levels, inject_hierarchy, hierarchy, _level, translate, name_l10n):
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 # -------------------------------------------------------------------------
1553 - def get_lx_ancestors(self, levels, resource, selector=None, location_ids=None, path=False):
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 # -------------------------------------------------------------------------
1649 - def _options(self, resource, inject_hierarchy=True, values=None):
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 # -------------------------------------------------------------------------
1940 - def _selector(self, resource, fields):
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
1983 - def _variable(cls, selector, operator):
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 # -------------------------------------------------------------------------
2015 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
2160 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
2277 - def ajax_options(self, resource):
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 # -------------------------------------------------------------------------
2315 - def _options(self, resource, values=None):
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("&nbsp;") better? 2612 2613 # Sort the options 2614 return (ftype, options, opts.get("no_opts", NOOPT))
2615 2616 # ------------------------------------------------------------------------- 2617 @staticmethod
2618 - def _values(get_vars, variable):
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 # -------------------------------------------------------------------------
2663 - def widget(self, resource, values):
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 # -------------------------------------------------------------------------
2719 - def variable(self, resource, get_vars=None):
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 # -------------------------------------------------------------------------
2788 - def widget(self, resource, values):
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
2812 - def __init__(self, widgets, **attr):
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 # -------------------------------------------------------------------------
2833 - def html(self, resource, get_vars=None, target=None, alias=None):
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 # -------------------------------------------------------------------------
2959 - def fields(self, resource, get_vars=None, alias=None):
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 # -------------------------------------------------------------------------
2997 - def _render_controls(self, resource):
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 # -------------------------------------------------------------------------
3119 - def _render_filters(self, resource, form_id):
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 # -------------------------------------------------------------------------
3225 - def json(self, resource, get_vars=None):
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
3237 - def apply_filter_defaults(request, resource):
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
3360 - def apply_method(self, r, **attr):
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 # -------------------------------------------------------------------------
3394 - def _form(self, r, **attr):
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 # -------------------------------------------------------------------------
3407 - def _options(self, r, **attr):
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
3444 - def _delete(r, **attr):
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 # -------------------------------------------------------------------------
3494 - def _save(self, r, **attr):
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 # -------------------------------------------------------------------------
3591 - def _load(self, r, **attr):
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
3658 - def __init__(self, resource, query):
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 # -------------------------------------------------------------------------
3691 - def represent(self):
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
3735 - def _render(cls, resource, alias, query, invert=False, labels=None):
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
3844 - def _convert(cls, rfield, value):
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
3897 - def _translate_query(cls, query, rfield, values, invert=False):
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