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

Source Code for Module s3.s3widgets

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ Custom UI Widgets 
   4   
   5      @requires: U{B{I{gluon}} <http://web2py.com>} 
   6   
   7      @copyright: 2009-2019 (c) Sahana Software Foundation 
   8      @license: MIT 
   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      NB Widgets are processed upon form submission (before form validation) 
  32         in addition to when generating new forms (so are often processed twice) 
  33  """ 
  34   
  35  __all__ = ("S3ACLWidget", 
  36             "S3AddObjectWidget", 
  37             "S3AddPersonWidget", 
  38             "S3AgeWidget", 
  39             "S3AutocompleteWidget", 
  40             "S3BooleanWidget", 
  41             "S3CascadeSelectWidget", 
  42             "S3ColorPickerWidget", 
  43             "S3CalendarWidget", 
  44             "S3DateWidget", 
  45             "S3DateTimeWidget", 
  46             "S3HoursWidget", 
  47             "S3EmbeddedComponentWidget", 
  48             "S3GroupedOptionsWidget", 
  49             #"S3RadioOptionsWidget", 
  50             "S3HiddenWidget", 
  51             "S3HierarchyWidget", 
  52             "S3HumanResourceAutocompleteWidget", 
  53             "S3ImageCropWidget", 
  54             "S3InvBinWidget", 
  55             "S3KeyValueWidget", 
  56             # Only used inside this module 
  57             #"S3LatLonWidget", 
  58             "S3LocationAutocompleteWidget", 
  59             "S3LocationDropdownWidget", 
  60             "S3LocationLatLonWidget", 
  61             "S3PasswordWidget", 
  62             "S3PhoneWidget", 
  63             "S3Selector", 
  64             "S3LocationSelector", 
  65             "S3MultiSelectWidget", 
  66             "S3OrganisationAutocompleteWidget", 
  67             "S3OrganisationHierarchyWidget", 
  68             "S3PersonAutocompleteWidget", 
  69             "S3PentityAutocompleteWidget", 
  70             "S3PriorityListWidget", 
  71             "S3SelectWidget", 
  72             "S3SiteAutocompleteWidget", 
  73             "S3SliderWidget", 
  74             "S3StringWidget", 
  75             "S3TimeIntervalWidget", 
  76             #"S3UploadWidget", 
  77             "S3FixedOptionsWidget", 
  78             "S3QuestionWidget", 
  79             "CheckboxesWidgetS3", 
  80             "s3_comments_widget", 
  81             "s3_richtext_widget", 
  82             "search_ac", 
  83             "S3XMLContents", 
  84             "S3TagCheckboxWidget", 
  85             "ICON", 
  86             ) 
  87   
  88  import datetime 
  89  import json 
  90  import os 
  91  import re 
  92   
  93  try: 
  94      from dateutil.relativedelta import relativedelta 
  95  except ImportError: 
  96      import sys 
  97      sys.stderr.write("ERROR: dateutil module needed for Date handling\n") 
  98      raise 
  99   
 100  from gluon import * 
 101  # Here are dependencies listed for reference: 
 102  #from gluon import current 
 103  #from gluon.html import * 
 104  #from gluon.http import HTTP 
 105  #from gluon.validators import * 
 106  from gluon.html import BUTTON 
 107  from gluon.languages import lazyT 
 108  from gluon.sqlhtml import * 
 109  from gluon.storage import Storage 
 110   
 111  from s3datetime import S3Calendar, S3DateTime 
 112  from s3utils import * 
 113  from s3validators import * 
 114   
 115  DEFAULT = lambda:None 
 116  ogetattr = object.__getattribute__ 
 117  repr_select = lambda l: len(l.name) > 48 and "%s..." % l.name[:44] or l.name 
 118   
 119  # Compact JSON encoding 
 120  SEPARATORS = (",", ":") 
121 122 # ============================================================================= 123 -class S3ACLWidget(CheckboxesWidget):
124 """ 125 Widget class for ACLs 126 127 @todo: add option dependency logic (JS) 128 @todo: configurable vertical/horizontal alignment 129 """ 130 131 @staticmethod
132 - def widget(field, value, **attributes):
133 134 requires = field.requires 135 if not isinstance(requires, (list, tuple)): 136 requires = [requires] 137 if requires: 138 if hasattr(requires[0], "options"): 139 options = requires[0].options() 140 values = [] 141 for k in options: 142 if isinstance(k, (list, tuple)): 143 k = k[0] 144 try: 145 flag = int(k) 146 if flag == 0: 147 if value == 0: 148 values.append(k) 149 break 150 else: 151 continue 152 elif value and value & flag == flag: 153 values.append(k) 154 except ValueError: 155 pass 156 value = values 157 158 #return CheckboxesWidget.widget(field, value, **attributes) 159 160 attr = OptionsWidget._attributes(field, {}, **attributes) 161 162 options = [(k, v) for k, v in options if k != ""] 163 opts = [] 164 cols = attributes.get("cols", 1) 165 totals = len(options) 166 mods = totals%cols 167 rows = totals/cols 168 if mods: 169 rows += 1 170 171 for r_index in range(rows): 172 tds = [] 173 for k, v in options[r_index*cols:(r_index+1)*cols]: 174 tds.append(TD(INPUT(_type="checkbox", 175 _name=attr.get("_name", field.name), 176 requires=attr.get("requires", None), 177 hideerror=True, _value=k, 178 value=(k in value)), v)) 179 opts.append(TR(tds)) 180 181 if opts: 182 opts[-1][0][0]["hideerror"] = False 183 return TABLE(*opts, **attr)
184
185 # was values = re.compile("[\w\-:]+").findall(str(value)) 186 #values = not isinstance(value,(list,tuple)) and [value] or value 187 188 189 #requires = field.requires 190 #if not isinstance(requires, (list, tuple)): 191 #requires = [requires] 192 #if requires: 193 #if hasattr(requires[0], "options"): 194 #options = requires[0].options() 195 #else: 196 #raise SyntaxError, "widget cannot determine options of %s" \ 197 #% field 198 199 # ============================================================================= 200 -class S3AddObjectWidget(FormWidget):
201 """ 202 This widget displays an inline form loaded via AJAX on demand. 203 204 UNUSED 205 206 In the browser: 207 A load request must made to this widget to enable it. 208 The load request must include: 209 - a URL for the form 210 211 after a successful submission, the response callback is handed the 212 response. 213 """
214 - def __init__(self, 215 form_url, 216 table_name, 217 dummy_field_selector, 218 on_show, 219 on_hide 220 ):
221 222 self.form_url = form_url 223 self.table_name = table_name 224 self.dummy_field_selector = dummy_field_selector 225 self.on_show = on_show 226 self.on_hide = on_hide
227
228 - def __call__(self, field, value, **attributes):
229 230 T = current.T 231 s3 = current.response.s3 232 233 if s3.debug: 234 script_name = "/%s/static/scripts/jquery.ba-resize.js" 235 else: 236 script_name = "/%s/static/scripts/jquery.ba-resize.min.js" 237 238 if script_name not in s3.scripts: 239 s3.scripts.append(script_name) 240 return TAG[""]( 241 # @ToDo: Move to Static 242 SCRIPT(''' 243 $(function () { 244 var form_field = $('#%(form_field_name)s') 245 var throbber = $('<div id="%(form_field_name)s_ajax_throbber" class="throbber"/>') 246 throbber.hide() 247 throbber.insertAfter(form_field) 248 249 function request_add_form() { 250 throbber.show() 251 var dummy_field = $('%(dummy_field_selector)s') 252 // create an element for the form 253 var form_iframe = document.createElement('iframe') 254 var $form_iframe = $(form_iframe) 255 $form_iframe.attr('id', '%(form_field_name)s_form_iframe') 256 $form_iframe.attr('frameborder', '0') 257 $form_iframe.attr('scrolling', 'no') 258 $form_iframe.attr('src', '%(form_url)s') 259 260 var initial_iframe_style = { 261 width: add_object_link.width(), 262 height: add_object_link.height() 263 } 264 $form_iframe.css(initial_iframe_style) 265 266 function close_iframe() { 267 $form_iframe.unload() 268 form_iframe.contentWindow.close() 269 //iframe_controls.remove() 270 $form_iframe.animate( 271 initial_iframe_style, 272 { 273 complete: function () { 274 $form_iframe.remove() 275 add_object_link.show() 276 %(on_hide)s 277 dummy_field.show() 278 } 279 } 280 ) 281 } 282 283 function reload_iframe() { 284 form_iframe.contentWindow.location.reload(true) 285 } 286 287 function resize_iframe_to_fit_content() { 288 var form_iframe_content = $form_iframe.contents().find('body'); 289 // do first animation smoothly 290 $form_iframe.animate( 291 { 292 height: form_iframe_content.outerHeight(true), 293 width: 500 294 }, 295 { 296 duration: jQuery.resize.delay, 297 complete: function () { 298 // iframe's own animations should be instant, as they 299 // have their own smoothing (e.g. expanding error labels) 300 function resize_iframe_to_fit_content_immediately() { 301 $form_iframe.css({ 302 height: form_iframe_content.outerHeight(true), 303 width:500 304 }) 305 } 306 // if the iframe content resizes, resize the iframe 307 // this depends on Ben Alman's resize plugin 308 form_iframe_content.bind( 309 'resize', 310 resize_iframe_to_fit_content_immediately 311 ) 312 // when unloading, unbind the resizer (remove poller) 313 $form_iframe.bind( 314 'unload', 315 function () { 316 form_iframe_content.unbind( 317 'resize', 318 resize_iframe_to_fit_content_immediately 319 ) 320 //iframe_controls.hide() 321 } 322 ) 323 // there may have been content changes during animation 324 // so resize to make sure they are shown. 325 form_iframe_content.resize() 326 //iframe_controls.show() 327 %(on_show)s 328 } 329 } 330 ) 331 } 332 333 function iframe_loaded() { 334 dummy_field.hide() 335 resize_iframe_to_fit_content() 336 form_iframe.contentWindow.close_iframe = close_iframe 337 throbber.hide() 338 } 339 340 $form_iframe.bind('load', iframe_loaded) 341 342 function set_object_id() { 343 // the server must give the iframe the object 344 // id of the created object for the field 345 // the iframe must also close itself. 346 var created_object_representation = form_iframe.contentWindow.created_object_representation 347 if (created_object_representation) { 348 dummy_field.val(created_object_representation) 349 } 350 var created_object_id = form_iframe.contentWindow.created_object_id 351 if (created_object_id) { 352 form_field.val(created_object_id) 353 close_iframe() 354 } 355 } 356 $form_iframe.bind('load', set_object_id) 357 add_object_link.hide() 358 359 /* 360 var iframe_controls = $('<span class="iframe_controls" style="float:right; text-align:right;"></span>') 361 iframe_controls.hide() 362 363 var close_button = $('<a>%(Close)s </a>') 364 close_button.click(close_iframe) 365 366 var reload_button = $('<a>%(Reload)s </a>') 367 reload_button.click(reload_iframe) 368 369 iframe_controls.append(close_button) 370 iframe_controls.append(reload_button) 371 iframe_controls.insertBefore(add_object_link) 372 */ 373 $form_iframe.insertAfter(add_object_link) 374 } 375 var add_object_link = $('<a>%(Add)s</a>') 376 add_object_link.click(request_add_form) 377 add_object_link.insertAfter(form_field) 378 })''' % dict( 379 field_name = field.name, 380 form_field_name = "_".join((self.table_name, field.name)), 381 form_url = self.form_url, 382 dummy_field_selector = self.dummy_field_selector(self.table_name, field.name), 383 on_show = self.on_show, 384 on_hide = self.on_hide, 385 Add = T("Add..."), 386 Reload = T("Reload"), 387 Close = T("Close"), 388 ) 389 ) 390 )
391
392 # ============================================================================= 393 -class S3AddPersonWidget(FormWidget):
394 """ 395 Widget for person_id (future also: human_resource_id) fields that 396 allows to either select an existing person (autocomplete), or to 397 create a new person record inline 398 399 Features: 400 - embedded fields configurable in deployment settings 401 - can use single name field (with on-submit name splitting), 402 alternatively separate fields for first/middle/last names 403 - can check for possible duplicates during data entry 404 - fully encapsulated, works with regular validators (IS_ONE_OF) 405 406 => Uses client-side script s3.ui.addperson.js (injected) 407 """ 408
409 - def __init__(self, 410 controller = None, 411 separate_name_fields = None, 412 father_name = None, 413 grandfather_name = None, 414 year_of_birth = None, 415 first_name_only = None, 416 pe_label = False, 417 ):
418 """ 419 Constructor 420 421 @param controller: controller for autocomplete 422 @param separate_name_fields: use separate name fields, overrides 423 deployment setting 424 425 @param father_name: expose father name field, overrides 426 deployment setting 427 @param grandfather_name: expose grandfather name field, overrides 428 deployment setting 429 430 @param year_of_birth: use just year-of-birth field instead of full 431 date-of-birth, overrides deployment setting 432 433 @param first_name_only: treat single name field entirely as 434 first name (=do not split into name parts), 435 overrides auto-detection, otherwise default 436 for right-to-left written languages 437 438 @param pe_label: expose ID label field 439 """ 440 441 self.controller = controller 442 self.separate_name_fields = separate_name_fields 443 self.father_name = father_name 444 self.grandfather_name = grandfather_name 445 self.year_of_birth = year_of_birth 446 self.first_name_only = first_name_only 447 self.pe_label = pe_label
448 449 # -------------------------------------------------------------------------
450 - def __call__(self, field, value, **attributes):
451 """ 452 Widget builder 453 454 @param field: the Field 455 @param value: the current or default value 456 @param attributes: additional HTML attributes for the widget 457 """ 458 459 s3db = current.s3db 460 T = current.T 461 462 # Attributes for the main input 463 default = dict(_type = "text", 464 value = (value is not None and str(value)) or "", 465 ) 466 attr = StringWidget._attributes(field, default, **attributes) 467 468 # Translations 469 i18n = {"none_of_the_above": T("None of the above"), 470 "loading": T("loading") 471 } 472 473 # Determine reference type 474 reference_type = str(field.type)[10:] 475 if reference_type == "pr_person": 476 hrm = False 477 fn = "person" 478 # Currently not supported + no active use-case 479 # @todo: implement in create_person() 480 #elif reference_type == "hrm_human_resource": 481 # hrm = True 482 # fn = "human_resource" 483 else: 484 raise TypeError("S3AddPersonWidget: unsupported field type %s" % field.type) 485 486 settings = current.deployment_settings 487 488 # Field label overrides 489 # (all other labels are looked up from the corresponding Field) 490 self.labels = { 491 "full_name": T(settings.get_pr_label_fullname()), 492 "email": T("Email"), 493 "mobile_phone": settings.get_ui_label_mobile_phone(), 494 "home_phone": T("Home Phone"), 495 } 496 497 # Fields which, if enabled, are required 498 # (all other fields are assumed to not be required) 499 self.required = { 500 "organisation_id": settings.get_hrm_org_required(), 501 "full_name": True, 502 "first_name": True, 503 "middle_name": settings.get_L10n_mandatory_middlename(), 504 "last_name": settings.get_L10n_mandatory_lastname(), 505 "date_of_birth": settings.get_pr_dob_required(), 506 "email": settings.get_hrm_email_required() if hrm else False, 507 } 508 509 # Determine controller for autocomplete 510 controller = self.controller 511 if not controller: 512 controller = current.request.controller 513 if controller not in ("pr", "dvr", "hrm", "vol"): 514 controller = "hrm" if hrm else "pr" 515 516 # Fields to extract and fields in form 517 ptable = s3db.pr_person 518 dtable = s3db.pr_person_details 519 520 fields = {} 521 details = False 522 523 trigger = None 524 formfields = [] 525 fappend = formfields.append 526 527 values = {} 528 529 # Organisation ID 530 if hrm: 531 htable = s3db.hrm_human_resource 532 f = htable.organisation_id 533 if f.default: 534 values["organisation_id"] = s3_str(f.default) 535 fields["organisation_id"] = f 536 fappend("organisation_id") 537 538 # ID Label 539 pe_label = self.pe_label 540 if pe_label: 541 fields["pe_label"] = ptable.pe_label 542 fappend("pe_label") 543 544 # Name fields (always extract all) 545 fields["first_name"] = ptable.first_name 546 fields["last_name"] = ptable.last_name 547 fields["middle_name"] = ptable.middle_name 548 549 separate_name_fields = self.separate_name_fields 550 if separate_name_fields is None: 551 separate_name_fields = settings.get_pr_separate_name_fields() 552 553 if separate_name_fields: 554 555 # Detect order of name fields 556 name_format = settings.get_pr_name_format() 557 keys = StringTemplateParser.keys(name_format) 558 559 if keys and keys[0] == "last_name": 560 # Last name first 561 trigger = "last_name" 562 fappend("last_name") 563 fappend("first_name") 564 else: 565 # First name first 566 trigger = "first_name" 567 fappend("first_name") 568 fappend("last_name") 569 570 if separate_name_fields == 3: 571 if keys and keys[-1] == "middle_name": 572 fappend("middle_name") 573 else: 574 formfields.insert(-1, "middle_name") 575 576 else: 577 578 # Single combined name field 579 fields["full_name"] = True 580 fappend("full_name") 581 582 # Additional name fields 583 father_name = self.father_name 584 if father_name is None: 585 # Not specified => apply deployment setting 586 father_name = settings.get_pr_request_father_name() 587 if father_name: 588 f = dtable.father_name 589 i18n["father_name_label"] = f.label 590 fields["father_name"] = f 591 details = True 592 fappend("father_name") 593 594 grandfather_name = self.grandfather_name 595 if grandfather_name is None: 596 # Not specified => apply deployment setting 597 grandfather_name = settings.get_pr_request_grandfather_name() 598 if grandfather_name: 599 f = dtable.grandfather_name 600 i18n["grandfather_name_label"] = f.label 601 fields["grandfather_name"] = f 602 details = True 603 fappend("grandfather_name") 604 605 # Date of Birth / Year of birth 606 year_of_birth = self.year_of_birth 607 if year_of_birth is None: 608 # Use Global deployment_setting 609 year_of_birth = settings.get_pr_request_year_of_birth() 610 if year_of_birth: 611 fields["year_of_birth"] = dtable.year_of_birth 612 details = True 613 fappend("year_of_birth") 614 elif settings.get_pr_request_dob(): 615 fields["date_of_birth"] = ptable.date_of_birth 616 fappend("date_of_birth") 617 618 # Gender 619 if settings.get_pr_request_gender(): 620 f = ptable.gender 621 if f.default: 622 values["gender"] = s3_str(f.default) 623 fields["gender"] = f 624 fappend("gender") 625 626 # Occupation 627 if controller == "vol": 628 fields["occupation"] = dtable.occupation 629 details = True 630 fappend("occupation") 631 632 # Contact Details 633 if settings.get_pr_request_email(): 634 fields["email"] = True 635 fappend("email") 636 if settings.get_pr_request_mobile_phone(): 637 fields["mobile_phone"] = True 638 fappend("mobile_phone") 639 if settings.get_pr_request_home_phone(): 640 fields["home_phone"] = True 641 fappend("home_phone") 642 643 self.fields = fields 644 645 # Extract existing values 646 if value: 647 record_id = None 648 if isinstance(value, basestring) and not value.isdigit(): 649 data, error = self.parse(value) 650 if not error: 651 if all(k in data for k in formfields): 652 values = data 653 else: 654 record_id = data.get("id") 655 else: 656 record_id = value 657 if record_id: 658 values = self.extract(record_id, fields, hrm=hrm, details=details) 659 660 # Generate the embedded rows 661 widget_id = str(field).replace(".", "_") 662 formrows = self.embedded_form(field.label, widget_id, formfields, values) 663 664 # Widget Options (pass only non-default options) 665 widget_options = {} 666 667 # Duplicate checking? 668 lookup_duplicates = settings.get_pr_lookup_duplicates() 669 if lookup_duplicates: 670 # Add translations for duplicates-review 671 i18n.update({"Yes": T("Yes"), 672 "No": T("No"), 673 "dupes_found": T("_NUM_ duplicates found"), 674 }) 675 widget_options["lookupDuplicates"] = True 676 677 if settings.get_ui_icons() != "font-awesome": 678 # Non-default icon theme => pass icon classes 679 widget_options["downIcon"] = ICON("down").attributes.get("_class") 680 widget_options["yesIcon"] = ICON("deployed").attributes.get("_class") 681 widget_options["noIcon"] = ICON("remove").attributes.get("_class") 682 683 # Use separate name fields? 684 if separate_name_fields: 685 widget_options["separateNameFields"] = True 686 if trigger: 687 widget_options["trigger"] = trigger 688 689 # Non default AC controller/function? 690 if controller != "pr": 691 widget_options["c"] = controller 692 if fn != "person": 693 widget_options["f"] = fn 694 695 # Non-default AC trigger parameters? 696 delay = settings.get_ui_autocomplete_delay() 697 if delay != 800: 698 widget_options["delay"] = delay 699 chars = settings.get_ui_autocomplete_min_chars() 700 if chars != 2: 701 widget_options["chars"] = chars 702 703 # Inject the scripts 704 self.inject_script(widget_id, widget_options, i18n) 705 706 # Create and return the main input 707 attr["_class"] = "hide" 708 709 # Prepend internal validation 710 requires = field.requires 711 if requires: 712 requires = (self.validate, requires) 713 else: 714 requires = self.validate 715 attr["requires"] = requires 716 717 return TAG[""](DIV(INPUT(**attr), _class = "hide"), formrows)
718 719 # -------------------------------------------------------------------------
720 - def extract(self, record_id, fields, details=False, hrm=False):
721 """ 722 Extract the data for a record ID 723 724 @param record_id: the record ID 725 @param fields: the fields to extract, dict {propName: Field} 726 @param details: includes person details 727 @param hrm: record ID is a hrm_human_resource ID rather 728 than person ID 729 730 @return: dict of {propName: value} 731 """ 732 733 db = current.db 734 735 s3db = current.s3db 736 ptable = s3db.pr_person 737 dtable = s3db.pr_person_details 738 739 qfields = [f for f in fields.values() if type(f) is not bool] 740 qfields.append(ptable.pe_id) 741 742 if hrm: 743 htable = s3db.hrm_human_resource 744 query = (htable.id == record_id) 745 join = ptable.on(ptable.id == htable.person_id) 746 else: 747 query = (ptable.id == record_id) 748 join = None 749 750 if details: 751 left = dtable.on(dtable.person_id == ptable.id) 752 else: 753 left = None 754 755 row = db(query).select(join = join, 756 left = left, 757 limitby = (0, 1), 758 *qfields).first() 759 if not row: 760 # Raise? 761 return {} 762 763 764 person = row.pr_person if join or left else row 765 values = dict((k, person[k]) for k in person) 766 767 if fields.get("full_name"): 768 values["full_name"] = s3_fullname(person) 769 770 if details: 771 details = row.pr_person_details 772 for k in details: 773 values[k] = details[k] 774 775 if hrm: 776 human_resource = row.hrm_human_resource 777 for k in human_resource: 778 values[k] = human_resource[k] 779 780 values.update(self.get_contact_data(row.pe_id)) 781 782 return values
783 784 # -------------------------------------------------------------------------
785 - def get_contact_data(self, pe_id):
786 """ 787 Extract the contact data for a pe_id; extracts only the first 788 value per contact method 789 790 @param pe_id: the pe_id 791 792 @return: a dict {fieldname: value}, where field names 793 correspond to the contact method (field map) 794 """ 795 796 # Map contact method <=> form field name 797 names = {"EMAIL": "email", 798 "HOME_PHONE": "home_phone", 799 "SMS": "mobile_phone", 800 } 801 802 # Determine relevant contact methods 803 fields = self.fields 804 methods = set(m for m in names if fields.get(names[m])) 805 806 # Initialize values with relevant fields 807 values = dict.fromkeys((names[m] for m in methods), "") 808 809 if methods: 810 811 # Retrieve the contact data 812 ctable = current.s3db.pr_contact 813 query = (ctable.pe_id == pe_id) & \ 814 (ctable.deleted == False) & \ 815 (ctable.contact_method.belongs(methods)) 816 817 rows = current.db(query).select(ctable.contact_method, 818 ctable.value, 819 orderby=ctable.priority, 820 ) 821 822 # Extract the values 823 for row in rows: 824 method = row.contact_method 825 if method in methods: 826 values[names[method]] = row.value 827 methods.discard(method) 828 if not methods: 829 break 830 831 return values
832 833 # -------------------------------------------------------------------------
834 - def embedded_form(self, label, widget_id, formfields, values):
835 """ 836 Construct the embedded form 837 838 @param label: the label for the embedded form 839 (= field label for the person_id) 840 @param widget_id: the widget ID 841 (=element ID of the person_id field) 842 @param formfields: list of field names indicating which 843 fields to render and in which order 844 @param values: dict with values to populate the embedded 845 form 846 847 @return: a DIV containing the embedded form rows 848 """ 849 850 T = current.T 851 s3 = current.response.s3 852 settings = current.deployment_settings 853 854 # Test the formstyle 855 formstyle = s3.crud.formstyle 856 tuple_rows = isinstance(formstyle("", "", "", ""), tuple) 857 858 rows = DIV() 859 860 # Section Title + Actions 861 title_id = "%s_title" % widget_id 862 label = LABEL(label, _for=title_id) 863 864 widget = DIV(A(ICON("edit"), 865 _class="edit-action", 866 _title=T("Edit Entry"), 867 ), 868 A(ICON("remove"), 869 _class="cancel-action", 870 _title=T("Revert Entry"), 871 ), 872 _class="add_person_edit_bar hide", 873 _id="%s_edit_bar" % widget_id, 874 ) 875 876 if tuple_rows: 877 row = TR(TD(DIV(label, widget, _class="box_top_inner"), 878 _class="box_top_td", 879 _colspan=2, 880 ), 881 _id="%s__row" % title_id, 882 ) 883 else: 884 row = formstyle("%s__row" % title_id, label, widget, "") 885 row.add_class("box_top hide") 886 887 rows.append(row) 888 889 # Input rows 890 for fname in formfields: 891 892 field = self.fields.get(fname) 893 if not field: 894 continue # Field is disabled 895 896 field_id = "%s_%s" % (widget_id, fname) 897 898 label = self.get_label(fname) 899 required = self.required.get(fname, False) 900 if required: 901 label = DIV("%s:" % label, SPAN(" *", _class="req")) 902 else: 903 label = "%s:" % label 904 label = LABEL(label, _for=field_id) 905 906 widget = self.get_widget(fname, field) 907 value = values.get(fname, "") 908 if not widget: 909 value = s3_str(value) 910 widget = INPUT(_id = field_id, 911 _name = fname, 912 _value = value, 913 old_value = value, 914 ) 915 else: 916 widget = widget(field, 917 value, 918 requires = None, 919 _id = field_id, 920 old_value = value, 921 ) 922 923 924 row = formstyle("%s__row" % field_id, label, widget, "") 925 if tuple_rows: 926 row[0].add_class("box_middle") 927 row[1].add_class("box_middle") 928 rows.append(row[0]) 929 rows.append(row[1]) 930 else: 931 row.add_class("box_middle hide") 932 rows.append(row) 933 934 # Divider (bottom box) 935 if tuple_rows: 936 row = formstyle("%s_box_bottom" % widget_id, "", "", "") 937 row = row[0] 938 row.add_class("box_bottom") 939 else: 940 row = DIV(_id="%s_box_bottom" % widget_id, 941 _class="box_bottom hide", 942 ) 943 if settings.ui.formstyle == "bootstrap": 944 # Need to add custom classes to core HTML markup 945 row.add_class("control-group") 946 rows.append(row) 947 948 return rows
949 950 # -------------------------------------------------------------------------
951 - def get_label(self, fieldname):
952 """ 953 Get a label for an embedded field 954 955 @param fieldname: the name of the embedded form field 956 957 @return: the label 958 """ 959 960 label = self.labels.get(fieldname) 961 if label is None: 962 # use self.fields 963 field = self.fields.get(fieldname) 964 if not field or field is True: 965 label = "" 966 else: 967 label = field.label 968 969 return label
970 971 # -------------------------------------------------------------------------
972 - def get_widget(self, fieldname, field):
973 """ 974 Get a widget for an embedded field; only when the field needs 975 a specific widget => otherwise return None here, so the form 976 builder will render a standard INPUT 977 978 @param fieldname: the name of the embedded form field 979 @param field: the Field corresponding to the form field 980 981 @return: the widget; or None if no specific widget is required 982 """ 983 984 # Fields which require a specific widget 985 widget = None 986 987 if fieldname in ("organisation_id", "gender"): 988 widget = OptionsWidget.widget 989 990 elif fieldname == "date_of_birth": 991 if hasattr(field, "widget"): 992 widget = field.widget 993 994 return widget
995 996 # -------------------------------------------------------------------------
997 - def inject_script(self, widget_id, options, i18n):
998 """ 999 Inject the necessary JavaScript for the widget 1000 1001 @param widget_id: the widget ID 1002 (=element ID of the person_id field) 1003 @param options: JSON-serializable dict of widget options 1004 @param i18n: translations of screen messages rendered by 1005 the client-side script, 1006 a dict {messageKey: translation} 1007 """ 1008 1009 request = current.request 1010 s3 = current.response.s3 1011 1012 # Static script 1013 if s3.debug: 1014 script = "/%s/static/scripts/S3/s3.ui.addperson.js" % \ 1015 request.application 1016 else: 1017 script = "/%s/static/scripts/S3/s3.ui.addperson.min.js" % \ 1018 request.application 1019 scripts = s3.scripts 1020 if script not in scripts: 1021 scripts.append(script) 1022 self.inject_i18n(i18n) 1023 1024 # Widget options 1025 opts = {} 1026 if options: 1027 opts.update(options) 1028 1029 # Widget instantiation 1030 script = '''$('#%(widget_id)s').addPerson(%(options)s)''' % \ 1031 {"widget_id": widget_id, 1032 "options": json.dumps(opts), 1033 } 1034 jquery_ready = s3.jquery_ready 1035 if script not in jquery_ready: 1036 jquery_ready.append(script)
1037 1038 # -------------------------------------------------------------------------
1039 - def inject_i18n(self, labels):
1040 """ 1041 Inject translations for screen messages rendered by the 1042 client-side script 1043 1044 @param labels: dict of translations {messageKey: translation} 1045 """ 1046 1047 strings = ['''i18n.%s="%s"''' % (k, s3_str(v)) 1048 for k, v in labels.items()] 1049 current.response.s3.js_global.append("\n".join(strings))
1050 1051 # -------------------------------------------------------------------------
1052 - def validate(self, value):
1053 """ 1054 Validate main input value 1055 1056 @param value: the main input value (JSON) 1057 1058 @return: tuple (id, error), where "id" is the record ID of the 1059 selected or newly created record 1060 """ 1061 1062 if not isinstance(value, basestring) or value.isdigit(): 1063 # Not a JSON object => return as-is 1064 return value, None 1065 1066 data, error = self.parse(value) 1067 if (error): 1068 return value, error 1069 1070 person_id = data.get("id") 1071 if person_id: 1072 # Existing record selected => return ID as-is 1073 return person_id, None 1074 1075 # Establish the name(s) 1076 names = self.get_names(data) 1077 if not names: 1078 # Treat as empty 1079 return None, None 1080 else: 1081 data.update(names) 1082 1083 # Validate phone numbers 1084 mobile = data.get("mobile_phone") 1085 if mobile: 1086 validator = IS_PHONE_NUMBER(international=True) 1087 mobile, error = validator(mobile) 1088 if error: 1089 return (None, error) 1090 1091 home_phone = data.get("home_phone") 1092 if home_phone: 1093 validator = IS_PHONE_NUMBER() 1094 home_phone, error = validator(home_phone) 1095 if error: 1096 return (None, error) 1097 1098 # Validate date of birth 1099 dob = data.get("date_of_birth") 1100 if not dob and \ 1101 self.fields.get("date_of_birth") and \ 1102 self.required.get("date_of_birth"): 1103 return (None, T("Date of Birth is Required")) 1104 1105 # Validate the email 1106 email, error = self.validate_email(data.get("email")) 1107 if error: 1108 return (None, error) 1109 1110 # Try to create the person records (and related records) 1111 return self.create_person(data)
1112 1113 # -------------------------------------------------------------------------
1114 - def parse(self, value):
1115 """ 1116 Parse the main input JSON when the form gets submitted 1117 1118 @param value: the main input value (JSON) 1119 1120 @return: tuple (data, error), where data is a dict with the 1121 submitted data like: {fieldname: value, ...} 1122 """ 1123 1124 from s3validators import JSONERRORS 1125 try: 1126 data = json.loads(value) 1127 except JSONERRORS: 1128 return value, "invalid JSON" 1129 1130 if type(data) is not dict: 1131 return value, "invalid JSON" 1132 1133 return data, None
1134 1135 # -------------------------------------------------------------------------
1136 - def get_names(self, data):
1137 """ 1138 Get first, middle and last names from the input data 1139 1140 @param data: the input data dict 1141 1142 @return: dict with the name parts found 1143 """ 1144 1145 settings = current.deployment_settings 1146 1147 separate_name_fields = self.separate_name_fields 1148 if separate_name_fields is None: 1149 separate_name_fields = settings.get_pr_separate_name_fields() 1150 1151 keys = ["first_name", "middle_name", "last_name"] 1152 1153 if separate_name_fields: 1154 1155 names = {} 1156 1157 for key in keys: 1158 value = data.get(key) 1159 if value: 1160 names[key] = value 1161 1162 else: 1163 1164 fullname = data.get("full_name") 1165 1166 if fullname: 1167 1168 # Shall all name parts go into first_name? 1169 first_name_only = self.first_name_only 1170 if first_name_only is None: 1171 # Activate by default if using RTL 1172 first_name_only = current.response.s3.rtl 1173 1174 if first_name_only: 1175 1176 # Put all name parts into first_name 1177 names = {"first_name": fullname} 1178 1179 else: 1180 1181 # Separate the name parts 1182 name_format = settings.get_pr_name_format() 1183 parts = StringTemplateParser.keys(name_format) 1184 if parts and parts[0] == "last_name": 1185 keys.reverse() 1186 names = dict(zip(keys, self.split_names(fullname))) 1187 1188 else: 1189 1190 names = {} 1191 1192 return names
1193 1194 # ------------------------------------------------------------------------- 1195 @staticmethod
1196 - def split_names(name):
1197 """ 1198 Split a full name into first/middle/last 1199 1200 @param name: the full name 1201 1202 @return: tuple (first, middle, last) 1203 """ 1204 1205 # https://github.com/derek73/python-nameparser 1206 from nameparser import HumanName 1207 name = HumanName(name) 1208 1209 return name.first, name.middle, name.last
1210 1211 # -------------------------------------------------------------------------
1212 - def validate_email(self, value, person_id=None):
1213 """ 1214 Validate the email address; checks whether the email address 1215 is valid and unique 1216 1217 @param value: the email address 1218 @param person_id: the person ID, if known 1219 1220 @return: tuple (value, error), where error is None if the 1221 email address is valid, otherwise contains the 1222 error message 1223 """ 1224 1225 T = current.T 1226 1227 error_message = T("Please enter a valid email address") 1228 1229 if value is not None: 1230 value = value.strip() 1231 1232 # No email? 1233 if not value: 1234 # @todo: may not need to check whether email is enabled? 1235 email_required = self.fields.get("email") and \ 1236 self.required.get("email") 1237 if email_required: 1238 return (value, error_message) 1239 return (value, None) 1240 1241 # Valid email? 1242 value, error = IS_EMAIL()(value) 1243 if error: 1244 return value, error_message 1245 1246 # Unique email? 1247 s3db = current.s3db 1248 ctable = s3db.pr_contact 1249 query = (ctable.deleted != True) & \ 1250 (ctable.contact_method == "EMAIL") & \ 1251 (ctable.value == value) 1252 if person_id: 1253 ptable = s3db.pr_person 1254 query &= (ctable.pe_id == ptable.pe_id) & \ 1255 (ptable.id != person_id) 1256 email = current.db(query).select(ctable.id, limitby=(0, 1)).first() 1257 if email: 1258 error_message = T("This email-address is already registered.") 1259 return value, error_message 1260 1261 # Ok! 1262 return value, None
1263 1264 # -------------------------------------------------------------------------
1265 - def create_person(self, data):
1266 """ 1267 Create a new record from form data 1268 1269 @param data - the submitted data 1270 @return: tuple (id, error), where "id" is the record ID of the 1271 newly created record 1272 """ 1273 1274 s3db = current.s3db 1275 1276 # Validate the person fields 1277 ptable = s3db.pr_person 1278 person = {} 1279 for f in ptable._filter_fields(data): 1280 if f == "id": 1281 continue 1282 value, error = s3_validate(ptable, f, data[f]) 1283 if error: 1284 label = ptable[f].label or f 1285 return (None, "%s: %s" % (label, error)) 1286 else: 1287 person[f] = value 1288 1289 # Onvalidation? (doesn't currently exist) 1290 1291 # Create new person record 1292 person_id = ptable.insert(**person) 1293 1294 if not person_id: 1295 return (None, T("Could not add person record")) 1296 1297 # Update the super-entities 1298 record = {"id": person_id} 1299 s3db.update_super(ptable, record) 1300 1301 # Update ownership & realm 1302 current.auth.s3_set_record_owner(ptable, person_id) 1303 1304 # Onaccept? (not relevant for this case) 1305 1306 # Read the created pe_id 1307 pe_id = record.get("pe_id") 1308 if not pe_id: 1309 return (None, T("Could not add person details")) 1310 1311 # Add contact information as provided 1312 ctable = s3db.pr_contact 1313 contacts = {"email": "EMAIL", 1314 "home_phone": "HOME_PHONE", 1315 "mobile_phone": "SMS", 1316 } 1317 for fname, contact_method in contacts.items(): 1318 value = data.get(fname) 1319 if value: 1320 ctable.insert(pe_id = pe_id, 1321 contact_method = contact_method, 1322 value = value, 1323 ) 1324 1325 # Add details as provided 1326 details = {} 1327 for fname in ("occupation", 1328 "father_name", 1329 "grandfather_name", 1330 "year_of_birth", 1331 ): 1332 value = data.get(fname) 1333 if value: 1334 details[fname] = value 1335 if details: 1336 details["person_id"] = person_id 1337 s3db.pr_person_details.insert(**details) 1338 1339 return person_id, None
1340
1341 # ============================================================================= 1342 -class S3AgeWidget(FormWidget):
1343 """ 1344 Widget to accept and represent date of birth as age in years, 1345 mapping the age to a pseudo date-of-birth internally so that 1346 it progresses over time; contains both widget and representation 1347 method 1348 1349 @example: 1350 s3_date("date_of_birth", 1351 label = T("Age"), 1352 widget = S3AgeWidget.widget, 1353 represent = lambda v: S3AgeWidget.date_as_age(v) \ 1354 if v else current.messages["NONE"], 1355 ... 1356 ) 1357 """ 1358 1359 @classmethod
1360 - def widget(cls, field, value, **attributes):
1361 """ 1362 The widget method, renders a simple integer-input 1363 1364 @param field: the Field 1365 @param value: the current or default value 1366 @param attributes: additional HTML attributes for the widget 1367 """ 1368 1369 if isinstance(value, basestring) and value and not value.isdigit(): 1370 # ISO String 1371 value = current.calendar.parse_date(value) 1372 1373 age = cls.date_as_age(value) 1374 1375 attr = IntegerWidget._attributes(field, {"value": age}, **attributes) 1376 1377 # Inner validation 1378 requires = (IS_INT_IN_RANGE(0, 150), cls.age_as_date) 1379 1380 # Accept empty if field accepts empty 1381 if isinstance(field.requires, IS_EMPTY_OR): 1382 requires = IS_EMPTY_OR(requires) 1383 attr["requires"] = requires 1384 1385 return INPUT(**attr)
1386 1387 # ------------------------------------------------------------------------- 1388 @staticmethod
1389 - def date_as_age(value, row=None):
1390 """ 1391 Convert a date value into age in years, can be used as 1392 representation method 1393 1394 @param value: the date 1395 1396 @return: the age in years (integer) 1397 """ 1398 1399 if value and isinstance(value, datetime.date): 1400 from dateutil.relativedelta import relativedelta 1401 age = relativedelta(current.request.utcnow, value).years 1402 else: 1403 age = value 1404 return age
1405 1406 # ------------------------------------------------------------------------- 1407 @staticmethod
1408 - def age_as_date(value, error_message="invalid age"):
1409 """ 1410 Convert age in years into an approximate date of birth, acts 1411 as inner validator of the widget 1412 1413 @param value: age value 1414 @param error_message: error message (override) 1415 1416 @returns: tuple (date, error) 1417 """ 1418 1419 try: 1420 age = int(value) 1421 except ValueError: 1422 return None, error_message 1423 1424 from dateutil.relativedelta import relativedelta 1425 date = (current.request.utcnow - relativedelta(years=age)).date() 1426 1427 # Map back to January 1st of the year of birth 1428 # => common practice, but needs validation as requirement 1429 date = date.replace(month=1, day=1) 1430 1431 return date, None
1432
1433 # ============================================================================= 1434 -class S3AutocompleteWidget(FormWidget):
1435 """ 1436 Renders a SELECT as an INPUT field with AJAX Autocomplete 1437 """ 1438
1439 - def __init__(self, 1440 module, 1441 resourcename, 1442 fieldname = "name", 1443 filter = "", # REST filter 1444 link_filter = "", 1445 post_process = "", 1446 ):
1447 1448 self.module = module 1449 self.resourcename = resourcename 1450 self.fieldname = fieldname 1451 self.filter = filter 1452 self.link_filter = link_filter 1453 self.post_process = post_process 1454 1455 # @ToDo: Refreshes all dropdowns as-necessary 1456 self.post_process = post_process or ""
1457
1458 - def __call__(self, field, value, **attributes):
1459 1460 s3 = current.response.s3 1461 settings = current.deployment_settings 1462 1463 default = dict( 1464 _type = "text", 1465 value = (value is not None and str(value)) or "", 1466 ) 1467 attr = StringWidget._attributes(field, default, **attributes) 1468 1469 # Hide the real field 1470 attr["_class"] = attr["_class"] + " hide" 1471 1472 if "_id" in attr: 1473 real_input = attr["_id"] 1474 else: 1475 real_input = str(field).replace(".", "_") 1476 dummy_input = "dummy_%s" % real_input 1477 1478 # JS Function defined in static/scripts/S3/S3.js 1479 script = '''S3.autocomplete.normal('%s','%s','%s','%s','%s',"%s"''' % \ 1480 (self.fieldname, 1481 self.module, 1482 self.resourcename, 1483 real_input, 1484 self.filter, 1485 self.link_filter, 1486 ) 1487 1488 options = "" 1489 post_process = self.post_process 1490 delay = settings.get_ui_autocomplete_delay() 1491 min_length = settings.get_ui_autocomplete_min_chars() 1492 if min_length != 2: 1493 options = ''',"%(postprocess)s",%(delay)s,%(min_length)s''' % \ 1494 dict(postprocess = post_process, 1495 delay = delay, 1496 min_length = min_length) 1497 elif delay != 800: 1498 options = ''',"%(postprocess)s",%(delay)s''' % \ 1499 dict(postprocess = post_process, 1500 delay = delay) 1501 elif post_process: 1502 options = ''',"%(postprocess)s"''' % \ 1503 dict(postprocess = post_process) 1504 1505 script = '''%s%s)''' % (script, options) 1506 s3.jquery_ready.append(script) 1507 1508 if value: 1509 try: 1510 value = long(value) 1511 except ValueError: 1512 pass 1513 text = s3_unicode(field.represent(value)) 1514 if "<" in text: 1515 text = s3_strip_markup(text) 1516 represent = text 1517 else: 1518 represent = "" 1519 1520 s3.js_global.append('''i18n.none_of_the_above="%s"''' % current.T("None of the above")) 1521 1522 return TAG[""](INPUT(_id=dummy_input, 1523 _class="string", 1524 _value=represent.encode("utf-8")), 1525 DIV(_id="%s_throbber" % dummy_input, 1526 _class="throbber input_throbber hide"), 1527 INPUT(**attr), 1528 requires = field.requires 1529 )
1530
1531 # ============================================================================= 1532 -class S3BooleanWidget(BooleanWidget):
1533 """ 1534 Standard Boolean widget, with an option to hide/reveal fields conditionally. 1535 """ 1536
1537 - def __init__(self, fields=None, click_to_show=True):
1538 1539 if fields is None: 1540 self.fields = () 1541 else: 1542 self.fields = fields 1543 self.click_to_show = click_to_show
1544
1545 - def __call__(self, field, value, **attributes):
1546 1547 response = current.response 1548 fields = self.fields 1549 click_to_show = self.click_to_show 1550 1551 default = {"_type": "checkbox", 1552 "value": value, 1553 } 1554 1555 attr = BooleanWidget._attributes(field, default, **attributes) 1556 1557 tablename = field.tablename 1558 1559 hide = "" 1560 show = "" 1561 for _field in fields: 1562 fieldname = "%s_%s" % (tablename, _field) 1563 hide += ''' 1564 $('#%s__row1').hide() 1565 $('#%s__row').hide() 1566 ''' % (fieldname, fieldname) 1567 show += ''' 1568 $('#%s__row1').show() 1569 $('#%s__row').show() 1570 ''' % (fieldname, fieldname) 1571 1572 if fields: 1573 checkbox = "%s_%s" % (tablename, field.name) 1574 click_start = ''' 1575 $('#%s').click(function(){ 1576 if(this.checked){ 1577 ''' % checkbox 1578 middle = "} else {\n" 1579 click_end = "}})" 1580 if click_to_show: 1581 # Hide by default 1582 script = '''%s\n%s\n%s\n%s\n%s\n%s''' % \ 1583 (hide, click_start, show, middle, hide, click_end) 1584 else: 1585 # Show by default 1586 script = '''%s\n%s\n%s\n%s\n%s\n%s''' % \ 1587 (show, click_start, hide, middle, show, click_end) 1588 response.s3.jquery_ready.append(script) 1589 1590 return TAG[""](INPUT(**attr), 1591 requires = field.requires 1592 )
1593
1594 # ============================================================================= 1595 -class S3ColorPickerWidget(FormWidget):
1596 """ 1597 Displays a widget to allow the user to pick a 1598 color, and falls back to using JSColor or a regular text input if 1599 necessary. 1600 """ 1601 1602 DEFAULT_OPTIONS = { 1603 "showInput": True, 1604 "showInitial": True, 1605 "preferredFormat": "hex", 1606 #"showPalette": True, 1607 "showPaletteOnly": True, 1608 "togglePaletteOnly": True, 1609 "palette": ("red", "orange", "yellow", "green", "blue", "white", "black") 1610 } 1611
1612 - def __init__(self, options=None):
1613 """ 1614 @param options: options for the JavaScript widget 1615 @see: http://bgrins.github.com/spectrum/ 1616 """ 1617 1618 self.options = dict(self.DEFAULT_OPTIONS) 1619 self.options.update(options or {})
1620
1621 - def __call__(self, field, value, **attributes):
1622 1623 default = dict(#_type = "color", # We don't want to use native HTML5 widget as it doesn't support our options & is worse for documentation 1624 _type = "text", 1625 value = (value is not None and str(value)) or "", 1626 ) 1627 1628 attr = StringWidget._attributes(field, default, **attributes) 1629 1630 widget = INPUT(**attr) 1631 1632 if "_id" in attr: 1633 selector = attr["_id"] 1634 else: 1635 selector = str(field).replace(".", "_") 1636 1637 s3 = current.response.s3 1638 1639 _min = "" if s3.debug else ".min" 1640 1641 script = "/%s/static/scripts/spectrum%s.js" % \ 1642 (current.request.application, _min) 1643 style = "plugins/spectrum%s.css" % _min 1644 1645 if script not in s3.scripts: 1646 s3.scripts.append(script) 1647 1648 if style not in s3.stylesheets: 1649 s3.stylesheets.append(style) 1650 1651 # i18n of Strings 1652 T = current.T 1653 options = self.options 1654 options.update(cancelText = s3_unicode(T("cancel")), 1655 chooseText = s3_unicode(T("choose")), 1656 togglePaletteMoreText = s3_unicode(T("more")), 1657 togglePaletteLessText = s3_unicode(T("less")), 1658 clearText = s3_unicode(T("Clear Color Selection")), 1659 noColorSelectedText = s3_unicode(T("No Color Selected")), 1660 ) 1661 1662 options = json.dumps(options, separators=SEPARATORS) 1663 # Ensure we save in rrggbb format not #rrggbb (IS_HTML_COLOUR) 1664 options = "%s,change:function(c){this.value=c.toHex()}}" % options[:-1] 1665 script = \ 1666 '''$('#%(selector)s').spectrum(%(options)s)''' % dict(selector = selector, 1667 options = options, 1668 ) 1669 s3.jquery_ready.append(script) 1670 1671 return widget
1672
1673 # ============================================================================= 1674 -class S3CalendarWidget(FormWidget):
1675 """ 1676 Widget to select a date from a popup calendar, with 1677 optional time input 1678 1679 @note: this widget must be combined with the IS_UTC_DATE or 1680 IS_UTC_DATETIME validators to have the value properly 1681 converted from/to local timezone and format. 1682 1683 - control script is s3.ui.calendar.js 1684 - uses jQuery UI DatePicker for Gregorian calendars: https://jqueryui.com/datepicker/ 1685 - uses jQuery UI Timepicker-addon if using times: http://trentrichardson.com/examples/timepicker 1686 - uses Calendars for non-Gregorian calendars: http://keith-wood.name/calendars.html 1687 (for this, ensure that css.cfg includes calendars/ui.calendars.picker.css and 1688 calendars/ui-smoothness.calendars.picker.css) 1689 """ 1690
1691 - def __init__(self, 1692 calendar=None, 1693 date_format=None, 1694 time_format=None, 1695 separator=None, 1696 minimum=None, 1697 maximum=None, 1698 past=None, 1699 future=None, 1700 past_months=None, 1701 future_months=None, 1702 month_selector=False, 1703 year_selector=True, 1704 min_year=None, 1705 max_year=None, 1706 week_number=False, 1707 buttons=None, 1708 timepicker=False, 1709 minute_step=5, 1710 set_min=None, 1711 set_max=None, 1712 clear_text=None, 1713 ):
1714 """ 1715 Constructor 1716 1717 @param calendar: which calendar to use (override default) 1718 1719 @param date_format: the date format (override default) 1720 @param time_format: the time format (override default) 1721 @param separator: date-time separator (override default) 1722 1723 @param minimum: the minimum selectable date/time (overrides past) 1724 @param maximum: the maximum selectable date/time (overrides future) 1725 @param past: how many hours into the past are selectable (overrides past_months) 1726 @param future: how many hours into the future are selectable (overrides future_months) 1727 @param past_months: how many months into the past are selectable 1728 @param future_months: how many months into the future are selectable 1729 1730 @param month_selector: show a months drop-down 1731 @param year_selector: show a years drop-down 1732 @param min_year: the minimum selectable year (can be relative to now like "-10") 1733 @param max_year: the maximum selectable year (can be relative to now like "+10") 1734 1735 @param week_number: show the week number in the calendar 1736 @param buttons: show the button panel (defaults to True if 1737 the widget has a timepicker, else False) 1738 1739 @param timepicker: show a timepicker 1740 @param minute_step: minute-step for the timepicker slider 1741 1742 @param set_min: CSS selector for another S3Calendar widget for which to 1743 dynamically update the minimum selectable date/time from 1744 the selected date/time of this widget 1745 @param set_max: CSS selector for another S3Calendar widget for which to 1746 dynamically update the maximum selectable date/time from 1747 the selected date/time of this widget 1748 """ 1749 1750 self.calendar = calendar 1751 1752 self.date_format = date_format 1753 self.time_format = time_format 1754 self.separator = separator 1755 1756 self.minimum = minimum 1757 self.maximum = maximum 1758 self.past = past 1759 self.future = future 1760 self.past_months = past_months 1761 self.future_months = future_months 1762 1763 self.month_selector = month_selector 1764 self.year_selector = year_selector 1765 self.min_year = min_year 1766 self.max_year = max_year 1767 1768 self.week_number = week_number 1769 self.buttons = buttons if buttons is not None else timepicker 1770 1771 self.timepicker = timepicker 1772 self.minute_step = minute_step 1773 1774 self.set_min = set_min 1775 self.set_max = set_max 1776 1777 self.clear_text = clear_text 1778 1779 self._class = "s3-calendar-widget datetimepicker"
1780 1781 # -------------------------------------------------------------------------
1782 - def __call__(self, field, value, **attributes):
1783 """ 1784 Widget builder 1785 1786 @param field: the Field 1787 @param value: the current value 1788 @param attributes: the HTML attributes for the widget 1789 """ 1790 1791 # Modify class as required 1792 _class = self._class 1793 1794 # Default attributes 1795 defaults = {"_type": "text", 1796 "_class": _class, 1797 "value": value, 1798 "requires": field.requires, 1799 } 1800 attr = self._attributes(field, defaults, **attributes) 1801 1802 # Real input ID 1803 input_id = attr.get("_id") 1804 if not input_id: 1805 if isinstance(field, Field): 1806 input_id = str(field).replace(".", "_") 1807 else: 1808 input_id = field.name.replace(".", "_") 1809 attr["_id"] = input_id 1810 1811 1812 # Real input name attribute 1813 input_name = attr.get("_name") 1814 if not input_name: 1815 input_name = field.name.replace(".", "_") 1816 attr["_name"] = input_name 1817 1818 # Container ID 1819 container_id = "%s-calendar-widget" % input_id 1820 1821 # Script options 1822 settings = current.deployment_settings 1823 1824 calendar = self.calendar or current.calendar.name 1825 calendar = calendar if calendar and calendar != "Gregorian" else "gregorian" 1826 1827 date_format = self.date_format or \ 1828 settings.get_L10n_date_format() 1829 time_format = self.time_format or \ 1830 settings.get_L10n_time_format() 1831 separator = self.separator or \ 1832 settings.get_L10n_datetime_separator() 1833 1834 c = current.calendar if not self.calendar else S3Calendar(self.calendar) 1835 firstDOW = c.first_dow 1836 1837 dtformat = separator.join([date_format, time_format]) 1838 extremes = self.extremes(dtformat=dtformat) 1839 1840 T = current.T 1841 1842 clear_text = self.clear_text 1843 if clear_text is None: 1844 clear_text = s3_str(T("Clear")) 1845 else: 1846 clear_text = s3_str(T(clear_text)) 1847 1848 options = {"calendar": calendar, 1849 "dateFormat": str(date_format), 1850 "timeFormat": str(time_format), 1851 "separator": separator, 1852 "firstDOW": firstDOW, 1853 "monthSelector": self.month_selector, 1854 "yearSelector": self.year_selector, 1855 "showButtons": self.buttons, 1856 "weekNumber": self.week_number, 1857 "timepicker": self.timepicker, 1858 "minuteStep": self.minute_step, 1859 "todayText": s3_str(T("Today")), 1860 "nowText": s3_str(T("Now")), 1861 "closeText": s3_str(T("Done")), 1862 "clearText": clear_text, 1863 "setMin": self.set_min, 1864 "setMax": self.set_max, 1865 } 1866 options.update(extremes) 1867 1868 if settings.get_ui_calendar_clear_icon(): 1869 options["clearButton"] = "icon" 1870 1871 # Inject JS 1872 self.inject_script(input_id, options) 1873 1874 # Construct real input 1875 real_input = INPUT(**attr) 1876 1877 # Construct and return the widget 1878 return TAG[""](DIV(real_input, 1879 _id=container_id, 1880 _class="calendar-widget-container", 1881 ), 1882 )
1883 1884 # -------------------------------------------------------------------------
1885 - def extremes(self, dtformat=None):
1886 """ 1887 Compute the minimum/maximum selectable date/time, as well as 1888 the default time (=the minute-step closest to now) 1889 1890 @param dtformat: the user datetime format 1891 1892 @return: a dict {minDateTime, maxDateTime, defaultValue, yearRange} 1893 with the min/max options as ISO-formatted strings, and the 1894 defaultValue in user-format (all in local time), to be 1895 passed as-is to s3.calendarwidget 1896 """ 1897 1898 extremes = {} 1899 now = current.request.utcnow 1900 1901 offset = S3DateTime.get_offset_value(current.session.s3.utc_offset) 1902 1903 # RAD : default to something quite generous 1904 pyears, fyears = 80, 80 1905 1906 # Minimum 1907 earliest = None 1908 fallback = False 1909 if self.minimum: 1910 earliest = self.minimum 1911 if type(earliest) is datetime.date: 1912 # Consistency with S3Calendar 1913 earliest = datetime.datetime.combine(earliest, datetime.time(8, 0, 0)) 1914 elif self.past is not None: 1915 earliest = now - datetime.timedelta(hours=self.past) 1916 elif self.past_months is not None: 1917 earliest = now - relativedelta(months=self.past_months) 1918 else: 1919 fallback = True 1920 earliest = now - datetime.timedelta(hours=876000) 1921 if earliest is not None: 1922 if not fallback: 1923 pyears = abs(earliest.year - now.year) 1924 earliest = earliest.replace(microsecond=0) 1925 if offset: 1926 earliest += datetime.timedelta(seconds=offset) 1927 extremes["minDateTime"] = earliest.isoformat() 1928 1929 # Maximum 1930 latest = None 1931 fallback = False 1932 if self.maximum: 1933 latest = self.maximum 1934 if type(latest) is datetime.date: 1935 # Consistency with S3Calendar 1936 latest = datetime.datetime.combine(latest, datetime.time(8, 0, 0)) 1937 elif self.future is not None: 1938 latest = now + datetime.timedelta(hours=self.future) 1939 elif self.future_months is not None: 1940 latest = now + relativedelta(months=self.future_months) 1941 else: 1942 fallback = True 1943 latest = now + datetime.timedelta(hours=876000) 1944 if latest is not None: 1945 if not fallback: 1946 fyears = abs(latest.year - now.year) 1947 latest = latest.replace(microsecond=0) 1948 if offset: 1949 latest += datetime.timedelta(seconds=offset) 1950 extremes["maxDateTime"] = latest.isoformat() 1951 1952 # Default date/time 1953 if self.timepicker and dtformat: 1954 # Pick a start date/time 1955 if earliest <= now <= latest: 1956 start = now 1957 elif now < earliest: 1958 start = earliest 1959 elif now > latest: 1960 start = latest 1961 # Round to the closest minute-step 1962 step = self.minute_step * 60 1963 seconds = (start - start.min).seconds 1964 rounding = (seconds + step / 2) // step * step 1965 rounded = start + datetime.timedelta(0, 1966 rounding - seconds, 1967 -start.microsecond, 1968 ) 1969 # Limits 1970 if rounded < earliest: 1971 rounded = earliest 1972 elif rounded > latest: 1973 rounded = latest 1974 # Translate into local time 1975 if offset: 1976 rounded += datetime.timedelta(seconds=offset) 1977 # Convert into user format 1978 default = rounded.strftime(dtformat) 1979 extremes["defaultValue"] = default 1980 1981 # Year range 1982 min_year = self.min_year 1983 if not min_year: 1984 min_year = "-%s" % pyears 1985 max_year = self.max_year 1986 if not max_year: 1987 max_year = "+%s" % fyears 1988 extremes["yearRange"] = "%s:%s" % (min_year, max_year) 1989 1990 return extremes
1991 1992 # -------------------------------------------------------------------------
1993 - def inject_script(self, selector, options):
1994 """ 1995 Helper function to inject the document-ready-JavaScript for 1996 this widget. 1997 1998 @param field: the Field 1999 @param value: the current value 2000 @param attr: the HTML attributes for the widget 2001 """ 2002 2003 if not selector: 2004 return 2005 2006 s3 = current.response.s3 2007 appname = current.request.application 2008 2009 request = current.request 2010 s3 = current.response.s3 2011 2012 datepicker_l10n = None 2013 timepicker_l10n = None 2014 calendars_type = None 2015 calendars_l10n = None 2016 calendars_picker_l10n = None 2017 2018 # Paths to localization files 2019 datepicker_l10n_path = os.path.join(request.folder, "static", "scripts", "ui", "i18n") 2020 timepicker_l10n_path = os.path.join(request.folder, "static", "scripts", "ui", "i18n") 2021 calendars_l10n_path = os.path.join(request.folder, "static", "scripts", "calendars", "i18n") 2022 2023 calendar = options["calendar"].lower() 2024 if calendar != "gregorian": 2025 # Include the right calendar script 2026 filename = "jquery.calendars.%s.js" % calendar 2027 lscript = os.path.join(calendars_l10n_path, filename) 2028 if os.path.exists(lscript): 2029 calendars_type = "calendars/i18n/%s" % filename 2030 2031 language = current.session.s3.language 2032 if language in current.deployment_settings.date_formats: 2033 # Localise if we have configured a Date Format and we have a jQueryUI options file 2034 2035 # Do we have a suitable locale file? 2036 #if language in ("prs", "ps"): 2037 # # Dari & Pashto use Farsi 2038 # language = "fa" 2039 #elif language == "ur": 2040 # # Urdu uses Arabic 2041 # language = "ar" 2042 if "-" in language: 2043 parts = language.split("-", 1) 2044 language = "%s-%s" % (parts[0], parts[1].upper()) 2045 2046 # datePicker regional 2047 filename = "datepicker-%s.js" % language 2048 path = os.path.join(timepicker_l10n_path, filename) 2049 if os.path.exists(path): 2050 timepicker_l10n = "ui/i18n/%s" % filename 2051 2052 # timePicker regional 2053 filename = "jquery-ui-timepicker-%s.js" % language 2054 path = os.path.join(datepicker_l10n_path, filename) 2055 if os.path.exists(path): 2056 datepicker_l10n = "ui/i18n/%s" % filename 2057 2058 if calendar != "gregorian" and language: 2059 # calendars regional 2060 filename = "jquery.calendars.%s-%s.js" % (calendar, language) 2061 path = os.path.join(calendars_l10n_path, filename) 2062 if os.path.exists(path): 2063 calendars_l10n = "calendars/i18n/%s" % filename 2064 # calendarsPicker regional 2065 filename = "jquery.calendars.picker-%s.js" % language 2066 path = os.path.join(calendars_l10n_path, filename) 2067 if os.path.exists(path): 2068 calendars_picker_l10n = "calendars/i18n/%s" % filename 2069 else: 2070 language = "" 2071 2072 options["language"] = language 2073 2074 # Global scripts 2075 if s3.debug: 2076 scripts = ("jquery.plugin.js", 2077 "calendars/jquery.calendars.all.js", 2078 "calendars/jquery.calendars.picker.ext.js", 2079 "S3/s3.ui.calendar.js", 2080 datepicker_l10n, 2081 timepicker_l10n, 2082 calendars_type, 2083 calendars_l10n, 2084 calendars_picker_l10n, 2085 ) 2086 else: 2087 scripts = ("jquery.plugin.min.js", 2088 "S3/s3.ui.calendar.min.js", 2089 datepicker_l10n, 2090 timepicker_l10n, 2091 calendars_type, 2092 calendars_l10n, 2093 calendars_picker_l10n, 2094 ) 2095 for script in scripts: 2096 if not script: 2097 continue 2098 path = "/%s/static/scripts/%s" % (appname, script) 2099 if path not in s3.scripts: 2100 s3.scripts.append(path) 2101 2102 # jQuery-ready script 2103 script = '''$('#%(selector)s').calendarWidget(%(options)s);''' % \ 2104 {"selector": selector, "options": json.dumps(options)} 2105 s3.jquery_ready.append(script)
2106
2107 # ============================================================================= 2108 -class S3DateWidget(FormWidget):
2109 """ 2110 Standard Date widget 2111 """ 2112
2113 - def __init__(self, 2114 format = None, 2115 #past=1440, 2116 #future=1440, 2117 past=None, 2118 future=None, 2119 start_field = None, 2120 default_interval = None, 2121 default_explicit = False, 2122 ):
2123 """ 2124 Constructor 2125 2126 @param format: format of date 2127 @param past: how many months into the past the date can be set to 2128 @param future: how many months into the future the date can be set to 2129 @param start_field: "selector" for start date field 2130 @param default_interval: x months from start date 2131 @param default_explicit: bool for explicit default 2132 """ 2133 2134 self.format = format 2135 self.past = past 2136 self.future = future 2137 self.start_field = start_field 2138 self.default_interval = default_interval 2139 self.default_explicit = default_explicit
2140 2141 # -------------------------------------------------------------------------
2142 - def __call__(self, field, value, **attributes):
2143 """ 2144 Widget builder 2145 2146 @param field: the Field 2147 @param value: the current value 2148 @param attributes: the HTML attributes for the widget 2149 """ 2150 2151 # Need to convert value into ISO-format 2152 # (widget expects ISO, but value comes in custom format) 2153 dt = current.calendar.parse_date(value, local=True) 2154 if dt: 2155 value = dt.isoformat() 2156 2157 request = current.request 2158 settings = current.deployment_settings 2159 2160 s3 = current.response.s3 2161 2162 jquery_ready = s3.jquery_ready 2163 language = current.session.s3.language 2164 2165 if language in settings.date_formats: 2166 # Localise if we have configured a Date Format and we have a jQueryUI options file 2167 # Do we have a suitable locale file? 2168 if language in ("prs", "ps"): 2169 # Dari & Pashto use Farsi 2170 language = "fa" 2171 #elif language == "ur": 2172 # # Urdu uses Arabic 2173 # language = "ar" 2174 elif "-" in language: 2175 parts = language.split("-", 1) 2176 language = "%s-%s" % (parts[0], parts[1].upper()) 2177 path = os.path.join(request.folder, "static", "scripts", "ui", "i18n", "datepicker-%s.js" % language) 2178 if os.path.exists(path): 2179 lscript = "/%s/static/scripts/ui/i18n/datepicker-%s.js" % (request.application, language) 2180 if lscript not in s3.scripts: 2181 # 1st Datepicker 2182 s3.scripts.append(lscript) 2183 script = '''$.datepicker.setDefaults($.datepicker.regional["%s"])''' % language 2184 jquery_ready.append(script) 2185 2186 if self.format: 2187 # default: "yy-mm-dd" 2188 format = str(self.format) 2189 else: 2190 dtfmt = settings.get_L10n_date_format() 2191 format = dtfmt.replace("%Y", "yy") \ 2192 .replace("%y", "y") \ 2193 .replace("%m", "mm") \ 2194 .replace("%d", "dd") \ 2195 .replace("%b", "M") 2196 2197 default = dict(_type = "text", 2198 value = (value is not None and str(value)) or "", 2199 ) 2200 2201 attr = StringWidget._attributes(field, default, **attributes) 2202 2203 widget = INPUT(**attr) 2204 widget.add_class("date") 2205 2206 if "_id" in attr: 2207 selector = attr["_id"] 2208 else: 2209 selector = str(field).replace(".", "_") 2210 2211 # Convert to Days 2212 now = current.request.utcnow 2213 past = self.past 2214 if past is None: 2215 past = "" 2216 else: 2217 if past: 2218 past = now - relativedelta(months=past) 2219 if now > past: 2220 days = (now - past).days 2221 minDate = "-%s" % days 2222 else: 2223 days = (past - now).days 2224 minDate = "+%s" % days 2225 else: 2226 minDate = "-0" 2227 past = ",minDate:%s" % minDate 2228 2229 future = self.future 2230 if future is None: 2231 future = "" 2232 else: 2233 if future: 2234 future = now + relativedelta(months=future) 2235 if future > now: 2236 days = (future - now).days 2237 maxDate = "+%s" % days 2238 else: 2239 days = (now - future).days 2240 maxDate = "-%s" % days 2241 else: 2242 maxDate = "+0" 2243 future = ",maxDate:%s" % maxDate 2244 2245 # Set auto updation of end_date based on start_date if start_field attr are set 2246 start_field = self.start_field 2247 default_interval = self.default_interval 2248 2249 script = \ 2250 '''$('#%(selector)s').datepicker('option',{yearRange:'c-100:c+100',dateFormat:'%(format)s'%(past)s%(future)s}).one('click',function(){$(this).focus()})''' % \ 2251 dict(selector = selector, 2252 format = format, 2253 past = past, 2254 future = future, 2255 ) 2256 2257 if script not in jquery_ready: # Prevents loading twice when form has errors 2258 jquery_ready.append(script) 2259 2260 if start_field and default_interval: 2261 2262 T = current.T 2263 2264 # Setting i18n for labels 2265 i18n = ''' 2266 i18n.interval="%(interval_label)s" 2267 i18n.btn_1_label="%(btn_first_label)s" 2268 i18n.btn_2_label="%(btn_second_label)s" 2269 i18n.btn_3_label="%(btn_third_label)s" 2270 i18n.btn_4_label="%(btn_fourth_label)s" 2271 i18n.btn_clear="%(btn_clear)s" 2272 ''' % dict(interval_label = T("Interval"), 2273 btn_first_label = T("+6 MO"), 2274 btn_second_label = T("+1 YR"), 2275 btn_third_label = T("+2 YR"), 2276 btn_fourth_label = T("+5 YR"), 2277 btn_clear = T("Clear"), 2278 ) 2279 2280 s3.js_global.append(i18n) 2281 2282 script = ''' 2283 $('#%(end_selector)s').end_date_interval({ 2284 start_date_selector:"#%(start_selector)s", 2285 interval:%(interval)d 2286 %(default_explicit)s 2287 }) 2288 ''' % dict(end_selector = selector, 2289 start_selector = start_field, 2290 interval = default_interval, 2291 default_explicit = ",default_explicit:true" if self.default_explicit else "", 2292 ) 2293 2294 if script not in jquery_ready: 2295 jquery_ready.append(script) 2296 2297 return TAG[""](widget, requires = field.requires)
2298
2299 # ============================================================================= 2300 -class S3DateTimeWidget(FormWidget):
2301 """ 2302 Date and/or time picker widget based on jquery.ui.datepicker and 2303 jquery.ui.timepicker.addon.js. 2304 """ 2305
2306 - def __init__(self, **opts):
2307 """ 2308 Constructor 2309 2310 @param opts: the widget options 2311 2312 @keyword date_format: the date format (falls back to 2313 deployment_settings.L10n.date_format) 2314 @keyword time_format: the time format (falls back to 2315 deployment_settings.L10n.time_format) 2316 @keyword separator: the date/time separator (falls back to 2317 deployment_settings.L10n.datetime_separator) 2318 2319 @keyword min: the earliest selectable datetime (datetime, overrides "past") 2320 @keyword max: the latest selectable datetime (datetime, overrides "future") 2321 @keyword past: the earliest selectable datetime relative to now (hours) 2322 @keyword future: the latest selectable datetime relative to now (hours) 2323 2324 @keyword min_year: the earliest year in the drop-down (default: now-10 years) 2325 @keyword max_year: the latest year in the drop-down (default: now+10 years) 2326 2327 @keyword hide_time: Hide the time selector (default: False) 2328 @keyword minute_step: number of minutes per slider step (default: 5) 2329 2330 @keyword weeknumber: show week number in calendar widget (default: False) 2331 @keyword month_selector: show drop-down selector for month (default: False) 2332 @keyword year_selector: show drop-down selector for year (default: True) 2333 @keyword buttons: show the button panel (default: True) 2334 2335 @keyword set_min: set a minimum for another datetime widget 2336 @keyword set_max: set a maximum for another datetime widget 2337 """ 2338 2339 self.opts = Storage(opts) 2340 self._class = "datetimepicker"
2341 2342 # -------------------------------------------------------------------------
2343 - def __call__(self, field, value, **attributes):
2344 """ 2345 Widget builder. 2346 2347 @param field: the Field 2348 @param value: the current value 2349 @param attributes: the HTML attributes for the widget 2350 """ 2351 2352 self.inject_script(field, value, **attributes) 2353 2354 default = dict(_type = "text", _class=self._class, value = value) 2355 attr = StringWidget._attributes(field, default, **attributes) 2356 2357 if "_id" not in attr: 2358 attr["_id"] = str(field).replace(".", "_") 2359 2360 widget = INPUT(**attr) 2361 widget.add_class(self._class) 2362 2363 if self.opts.get("hide_time", False): 2364 widget.add_class("hide-time") 2365 2366 return TAG[""](widget, requires = field.requires)
2367 2368 # -------------------------------------------------------------------------
2369 - def inject_script(self, field, value, **attributes):
2370 """ 2371 Helper function to inject the document-ready-JavaScript for 2372 this widget. 2373 2374 @param field: the Field 2375 @param value: the current value 2376 @param attributes: the HTML attributes for the widget 2377 """ 2378 2379 ISO = "%Y-%m-%dT%H:%M:%S" 2380 opts = self.opts 2381 2382 if "_id" in attributes: 2383 selector = attributes["_id"] 2384 else: 2385 selector = str(field).replace(".", "_") 2386 2387 settings = current.deployment_settings 2388 date_format = opts.get("date_format", 2389 settings.get_L10n_date_format()) 2390 time_format = opts.get("time_format", 2391 settings.get_L10n_time_format()) 2392 separator = opts.get("separator", 2393 settings.get_L10n_datetime_separator()) 2394 datetime_format = "%s%s%s" % (date_format, separator, time_format) 2395 2396 request = current.request 2397 s3 = current.response.s3 2398 jquery_ready = s3.jquery_ready 2399 language = current.session.s3.language 2400 if language in settings.date_formats: 2401 # Localise if we have configured a Date Format and we have a jQueryUI options file 2402 # Do we have a suitable locale file? 2403 if language in ("prs", "ps"): 2404 # Dari & Pashto use Farsi 2405 language = "fa" 2406 #elif language == "ur": 2407 # # Urdu uses Arabic 2408 # language = "ar" 2409 elif "-" in language: 2410 parts = language.split("_", 1) 2411 language = "%s-%s" % (parts[0], parts[1].upper()) 2412 path = os.path.join(request.folder, "static", "scripts", "ui", "i18n", "datepicker-%s.js" % language) 2413 if os.path.exists(path): 2414 lscript = "/%s/static/scripts/ui/i18n/datepicker-%s.js" % (request.application, language) 2415 if lscript not in s3.scripts: 2416 # 1st Datepicker 2417 s3.scripts.append(lscript) 2418 script = '''$.datepicker.setDefaults($.datepicker.regional["%s"])''' % language 2419 jquery_ready.append(script) 2420 2421 # Option to hide the time slider 2422 hide_time = opts.get("hide_time", False) 2423 if hide_time: 2424 limit = "Date" 2425 widget = "datepicker" 2426 dtformat = date_format 2427 else: 2428 limit = "DateTime" 2429 widget = "datetimepicker" 2430 dtformat = datetime_format 2431 2432 # Limits 2433 now = request.utcnow 2434 timedelta = datetime.timedelta 2435 offset = S3DateTime.get_offset_value(current.session.s3.utc_offset) 2436 2437 if "min" in opts: 2438 earliest = opts["min"] 2439 else: 2440 past = opts.get("past", 876000) 2441 earliest = now - timedelta(hours = past) 2442 if "max" in opts: 2443 latest = opts["max"] 2444 else: 2445 future = opts.get("future", 876000) 2446 latest = now + timedelta(hours = future) 2447 2448 # Closest minute step as default 2449 minute_step = opts.get("minute_step", 5) 2450 if not hide_time: 2451 if earliest <= now and now <= latest: 2452 start = now 2453 elif now < earliest: 2454 start = earliest 2455 elif now > latest: 2456 start = latest 2457 step = minute_step * 60 2458 seconds = (start - start.min).seconds 2459 rounding = (seconds + step / 2) // step * step 2460 rounded = start + timedelta(0, rounding - seconds, 2461 -start.microsecond) 2462 if rounded < earliest: 2463 rounded = earliest 2464 elif rounded > latest: 2465 rounded = latest 2466 if offset: 2467 rounded += timedelta(seconds=offset) 2468 default = rounded.strftime(dtformat) 2469 else: 2470 default = "" 2471 2472 # Add timezone offset to limits 2473 if offset: 2474 earliest += timedelta(seconds=offset) 2475 latest += timedelta(seconds=offset) 2476 2477 # Update limits of another widget? 2478 set_min = opts.get("set_min", None) 2479 set_max = opts.get("set_max", None) 2480 onclose = '''function(selectedDate){''' 2481 onclear = "" 2482 if set_min: 2483 onclose += '''$('#%s').%s('option','minDate',selectedDate)\n''' % \ 2484 (set_min, widget) 2485 onclear += '''$('#%s').%s('option','minDate',null)\n''' % \ 2486 (set_min, widget) 2487 if set_max: 2488 onclose += '''$('#%s').%s('option','maxDate',selectedDate)''' % \ 2489 (set_max, widget) 2490 onclear += '''$('#%s').%s('option','minDate',null)''' % \ 2491 (set_max, widget) 2492 onclose += '''}''' 2493 2494 # Translate Python format-strings 2495 date_format = settings.get_L10n_date_format().replace("%Y", "yy") \ 2496 .replace("%y", "y") \ 2497 .replace("%m", "mm") \ 2498 .replace("%d", "dd") \ 2499 .replace("%b", "M") 2500 2501 time_format = settings.get_L10n_time_format().replace("%p", "TT") \ 2502 .replace("%I", "hh") \ 2503 .replace("%H", "HH") \ 2504 .replace("%M", "mm") \ 2505 .replace("%S", "ss") 2506 2507 separator = settings.get_L10n_datetime_separator() 2508 2509 # Year range 2510 pyears, fyears = 10, 10 2511 if "min" in opts or "past" in opts: 2512 pyears = abs(earliest.year - now.year) 2513 if "max" in opts or "future" in opts: 2514 fyears = abs(latest.year - now.year) 2515 year_range = "%s:%s" % (opts.get("min_year", "-%s" % pyears), 2516 opts.get("max_year", "+%s" % fyears)) 2517 2518 # Other options 2519 firstDOW = settings.get_L10n_firstDOW() 2520 2521 # Boolean options 2522 getopt = lambda opt, default: opts.get(opt, default) and "true" or "false" 2523 2524 script = \ 2525 '''$('#%(selector)s').%(widget)s({ 2526 showSecond:false, 2527 firstDay:%(firstDOW)s, 2528 min%(limit)s:new Date(Date.parse('%(earliest)s')), 2529 max%(limit)s:new Date(Date.parse('%(latest)s')), 2530 dateFormat:'%(date_format)s', 2531 timeFormat:'%(time_format)s', 2532 separator:'%(separator)s', 2533 stepMinute:%(minute_step)s, 2534 showWeek:%(weeknumber)s, 2535 showButtonPanel:%(buttons)s, 2536 changeMonth:%(month_selector)s, 2537 changeYear:%(year_selector)s, 2538 yearRange:'%(year_range)s', 2539 useLocalTimezone:true, 2540 defaultValue:'%(default)s', 2541 onClose:%(onclose)s 2542 }).one('click',function(){$(this).focus()}) 2543 var clear_button=$('<button id="%(selector)s_clear" class="btn date-clear-btn" type="button">%(clear)s</button>').click(function(){ 2544 $('#%(selector)s').val('');%(onclear)s;$('#%(selector)s').closest('.filter-form').trigger('optionChanged') 2545 }) 2546 if($('#%(selector)s_clear').length==0){ 2547 $('#%(selector)s').after(clear_button) 2548 }''' % dict(selector=selector, 2549 widget=widget, 2550 date_format=date_format, 2551 time_format=time_format, 2552 separator=separator, 2553 weeknumber = getopt("weeknumber", False), 2554 month_selector = getopt("month_selector", False), 2555 year_selector = getopt("year_selector", True), 2556 buttons = getopt("buttons", True), 2557 firstDOW=firstDOW, 2558 year_range=year_range, 2559 minute_step=minute_step, 2560 limit = limit, 2561 earliest = earliest.strftime(ISO), 2562 latest = latest.strftime(ISO), 2563 default=default, 2564 clear=current.T("clear"), 2565 onclose=onclose, 2566 onclear=onclear, 2567 ) 2568 2569 if script not in jquery_ready: # Prevents loading twice when form has errors 2570 jquery_ready.append(script) 2571 2572 return
2573
2574 # ============================================================================= 2575 -class S3HoursWidget(FormWidget):
2576 """ 2577 Widget to enter a duration in hours (e.g. of a task), supporting 2578 flexible input format (e.g. "1h 15min", "1.75", "2:10") 2579 """ 2580 2581 PARTS = re.compile(r"((?:[+-]{0,1}\s*)(?:[0-9,.:]+)\s*(?:[^0-9,.:+-]*))") 2582 TOKEN = re.compile(r"([+-]{0,1}\s*)([0-9,.:]+)([^0-9,.:+-]*)") 2583
2584 - def __init__(self, interval=None, precision=2):
2585 """ 2586 Constructor 2587 2588 @param interval: standard interval to round up to (minutes), 2589 None to disable rounding 2590 @param precision: number of decimal places to keep 2591 """ 2592 2593 self.interval = interval 2594 self.precision = precision
2595 2596 # -------------------------------------------------------------------------
2597 - def __call__(self, field, value, **attributes):
2598 """ 2599 Entry point for form processing 2600 2601 @param field: the Field 2602 @param value: the current/default value 2603 @param attributes: HTML attributes for the widget 2604 """ 2605 2606 default = {"value": (value != None and str(value)) or ""} 2607 attr = StringWidget._attributes(field, default, **attributes) 2608 2609 attr["requires"] = self.validate 2610 2611 widget = INPUT(**attr) 2612 widget.add_class("hours") 2613 2614 return widget
2615 2616 # -------------------------------------------------------------------------
2617 - def validate(self, value):
2618 """ 2619 Pre-validator to parse the input value before validating it 2620 2621 @param value: the input value 2622 2623 @returns: tuple (parsed, error) 2624 """ 2625 2626 try: 2627 return self.s3_parse(value), None 2628 except: 2629 return value, "invalid value"
2630 2631 # -------------------------------------------------------------------------
2632 - def s3_parse(self, value):
2633 """ 2634 Function to parse the input value (if it is a string) 2635 2636 @param value: the value 2637 @returns: the value as float (hours) 2638 """ 2639 2640 hours = 0.0 2641 2642 if value is None or value == "": 2643 return None 2644 elif not value: 2645 return hours 2646 2647 parts = self.PARTS.split(value) 2648 for part in parts: 2649 2650 token = part.strip() 2651 if not token: 2652 continue 2653 2654 m = self.TOKEN.match(token) 2655 if not m: 2656 continue 2657 2658 sign = m.group(1).strip() 2659 num = m.group(2) 2660 2661 unit = m.group(3).lower() 2662 unit = unit[0] if unit else "h" 2663 if unit == "s": 2664 length = 1 2665 factor = 3600.0 2666 elif unit == "m": 2667 length = 2 2668 factor = 60.0 2669 else: 2670 length = 3 2671 factor = 1.0 2672 2673 segments = (num.replace(",", ".").split(":") + ["0", "0", "0"])[:length] 2674 total = 0.0 2675 for segment in segments: 2676 try: 2677 v = float(segment) 2678 except ValueError: 2679 v = 0.0 2680 total += v / factor 2681 factor *= 60 2682 2683 if sign == "-": 2684 hours -= total 2685 else: 2686 hours += total 2687 2688 interval = self.interval 2689 if interval: 2690 import math 2691 interval = float(interval) 2692 hours = math.ceil(hours * 60.0 / interval) * interval / 60.0 2693 2694 return round(hours, self.precision)
2695
2696 # ============================================================================= 2697 -class S3EmbeddedComponentWidget(FormWidget):
2698 """ 2699 Widget used by S3CRUD for link-table components with actuate="embed". 2700 Uses s3.embed_component.js for client-side processing, and 2701 S3CRUD._postprocess_embedded to receive the data. 2702 """ 2703
2704 - def __init__(self, 2705 link=None, 2706 component=None, 2707 autocomplete=None, 2708 link_filter=None, 2709 select_existing=True):
2710 """ 2711 Constructor 2712 2713 @param link: the name of the link table 2714 @param component: the name of the component table 2715 @param autocomplete: name of the autocomplete field 2716 @param link_filter: filter expression to filter out records 2717 in the component that are already linked 2718 to the main record 2719 @param select_existing: allow the selection of existing 2720 component records from the registry 2721 """ 2722 2723 self.link = link 2724 self.component = component 2725 self.autocomplete = autocomplete 2726 self.select_existing = select_existing 2727 self.link_filter = link_filter
2728 2729 # -------------------------------------------------------------------------
2730 - def __call__(self, field, value, **attributes):
2731 """ 2732 Widget renderer 2733 2734 @param field: the Field 2735 @param value: the current value 2736 @param attributes: the HTML attributes for the widget 2737 """ 2738 2739 T = current.T 2740 2741 # Input ID 2742 if "_id" in attributes: 2743 input_id = attributes["_id"] 2744 else: 2745 input_id = str(field).replace(".", "_") 2746 2747 # Form style and widget style 2748 s3 = current.response.s3 2749 formstyle = s3.crud.formstyle 2750 if not callable(formstyle) or \ 2751 isinstance(formstyle("","","",""), tuple): 2752 widgetstyle = self._formstyle 2753 else: 2754 widgetstyle = formstyle 2755 2756 # Subform controls 2757 controls = TAG[""](A(T("Select from Registry"), 2758 _id="%s-select" % input_id, 2759 _class="action-btn", 2760 ), 2761 A(T("Remove Selection"), 2762 _id="%s-clear" % input_id, 2763 _class="action-btn hide", 2764 _style="padding-left:15px;", 2765 ), 2766 A(T("Edit Details"), 2767 _id="%s-edit" % input_id, 2768 _class="action-btn hide", 2769 _style="padding-left:15px;", 2770 ), 2771 DIV(_class="throbber hide", 2772 _style="padding-left:85px;", 2773 ), 2774 ) 2775 controls = widgetstyle("%s-select-row" % input_id, 2776 "", 2777 controls, 2778 "", 2779 ) 2780 controls.add_class("box_top" if self.select_existing else "hide") 2781 2782 s3db = current.s3db 2783 ctable = s3db[self.component] 2784 prefix, resourcename = self.component.split("_", 1) 2785 2786 # Selector 2787 autocomplete = self.autocomplete 2788 if autocomplete: 2789 # Autocomplete widget 2790 ac_field = ctable[autocomplete] 2791 2792 widget = S3AutocompleteWidget(prefix, 2793 resourcename=resourcename, 2794 fieldname=autocomplete, 2795 link_filter=self.link_filter, 2796 ) 2797 selector = widgetstyle("%s-autocomplete-row" % input_id, 2798 LABEL("%s: " % ac_field.label, 2799 _class="hide", 2800 _id="%s-autocomplete-label" % input_id), 2801 widget(field, value), 2802 "", 2803 ) 2804 selector.add_class("box_top") 2805 else: 2806 # Options widget 2807 # @todo: add link_filter here as well 2808 widget = OptionsWidget.widget(field, None, 2809 _class="hide", 2810 _id="dummy_%s" % input_id, 2811 ) 2812 label = LABEL("%s: " % field.label, 2813 _class="hide", 2814 _id="%s-autocomplete-label" % input_id, 2815 ) 2816 hidden_input = INPUT(_id=input_id, _class="hide") 2817 2818 selector = widgetstyle("%s-autocomplete-row" % input_id, 2819 label, 2820 TAG[""](widget, hidden_input), 2821 "", 2822 ) 2823 selector.add_class("box_top") 2824 2825 # Initialize field validators with the correct record ID 2826 fields = [f for f in ctable 2827 if (f.writable or f.readable) and not f.compute] 2828 request = current.request 2829 if field.name in request.post_vars: 2830 selected = request.post_vars[field.name] 2831 else: 2832 selected = None 2833 if selected: 2834 for f in fields: 2835 requires = f.requires or [] 2836 if not isinstance(requires, (list, tuple)): 2837 requires = [requires] 2838 [r.set_self_id(selected) for r in requires 2839 if hasattr(r, "set_self_id")] 2840 2841 # Mark required 2842 labels, required = s3_mark_required(fields) 2843 if required: 2844 s3.has_required = True 2845 2846 # Generate embedded form 2847 form = SQLFORM.factory(table_name=self.component, 2848 labels=labels, 2849 formstyle=formstyle, 2850 upload="default/download", 2851 separator = "", 2852 *fields) 2853 2854 # Re-wrap the embedded form rows in an empty TAG 2855 formrows = [] 2856 append = formrows.append 2857 for formrow in form[0]: 2858 if not formrow.attributes["_id"].startswith("submit_record"): 2859 if hasattr(formrow, "add_class"): 2860 formrow.add_class("box_middle embedded-%s" % input_id) 2861 append(formrow) 2862 formrows = TAG[""](formrows) 2863 2864 # Divider 2865 divider = widgetstyle("", "", DIV(_class="subheading"), "") 2866 divider.add_class("box_bottom embedded") 2867 2868 # Widget script 2869 appname = request.application 2870 if s3.debug: 2871 script = "s3.ui.embeddedcomponent.js" 2872 else: 2873 script = "s3.ui.embeddedcomponent.min.js" 2874 script = "/%s/static/scripts/S3/%s" % (appname, script) 2875 if script not in s3.scripts: 2876 s3.scripts.append(script) 2877 2878 # Script options 2879 url = "/%s/%s/%s/" % (appname, prefix, resourcename) 2880 options = {"ajaxURL": url, 2881 "fieldname": input_id, 2882 "component": self.component, 2883 "recordID": str(value), 2884 "autocomplete": True if autocomplete else False, 2885 } 2886 2887 # Post-process after Selection/Deselection 2888 post_process = s3db.get_config(self.link, "post_process") 2889 if post_process: 2890 try: 2891 pp = post_process % input_id 2892 except TypeError: 2893 pp = post_process 2894 options["postprocess"] = pp 2895 2896 # Initialize UI Widget 2897 script = '''$('#%(input)s').embeddedComponent(%(options)s)''' % \ 2898 {"input": input_id, "options": json.dumps(options)} 2899 s3.jquery_ready.append(script) 2900 2901 # Overall layout of components 2902 return TAG[""](controls, selector, formrows, divider)
2903 2904 # ------------------------------------------------------------------------- 2905 @staticmethod
2906 - def _formstyle(row_id, label, widget, comments):
2907 """ 2908 Fallback for legacy formstyles (i.e. not callable or tuple-rows) 2909 """ 2910 2911 return TR(TD(label, widget, _class="w2p_fw"), 2912 TD(comments), 2913 _id=row_id, 2914 )
2915 2916 # ------------------------------------------------------------------------- 2917 @staticmethod
2953
2954 # ----------------------------------------------------------------------------- 2955 -def S3GenericAutocompleteTemplate(post_process, 2956 delay, 2957 min_length, 2958 field, 2959 value, 2960 attributes, 2961 source = None, 2962 transform_value = lambda value: value, 2963 tablename = None, # Allow variations 2964 ):
2965 """ 2966 Renders a SELECT as an INPUT field with AJAX Autocomplete 2967 """ 2968 2969 value = transform_value(value) 2970 2971 default = dict( 2972 _type = "text", 2973 value = (value is not None and s3_unicode(value)) or "", 2974 ) 2975 attr = StringWidget._attributes(field, default, **attributes) 2976 2977 # Hide the real field 2978 attr["_class"] = attr["_class"] + " hide" 2979 2980 if "_id" in attr: 2981 real_input = attr["_id"] 2982 else: 2983 real_input = str(field).replace(".", "_") 2984 2985 dummy_input = "dummy_%s" % real_input 2986 2987 if value: 2988 try: 2989 value = long(value) 2990 except ValueError: 2991 pass 2992 # Provide the representation for the current/default Value 2993 text = s3_unicode(field.represent(value)) 2994 if "<" in text: 2995 text = s3_strip_markup(text) 2996 represent = text.encode("utf-8") 2997 else: 2998 represent = "" 2999 3000 if tablename == "org_organisation": 3001 # S3OrganisationAutocompleteWidget 3002 script = \ 3003 '''S3.autocomplete.org('%(input)s',"%(postprocess)s",%(delay)s,%(min_length)s)''' % \ 3004 dict(input = real_input, 3005 postprocess = post_process, 3006 delay = delay, 3007 min_length = min_length, 3008 ) 3009 else: 3010 # Currently unused 3011 script = \ 3012 '''S3.autocomplete.generic('%(url)s','%(input)s',"%(postprocess)s",%(delay)s,%(min_length)s)''' % \ 3013 dict(url = source, 3014 input = real_input, 3015 postprocess = post_process, 3016 delay = delay, 3017 min_length = min_length, 3018 ) 3019 current.response.s3.jquery_ready.append(script) 3020 return TAG[""](INPUT(_id=dummy_input, 3021 _class="string", 3022 value=represent), 3023 DIV(_id="%s_throbber" % dummy_input, 3024 _class="throbber input_throbber hide"), 3025 INPUT(**attr), 3026 requires = field.requires 3027 )
3028
3029 #============================================================================== 3030 -class S3GroupedOptionsWidget(FormWidget):
3031 """ 3032 Widget with checkboxes or radio buttons for S3OptionsFilter 3033 - checkboxes can be optionally grouped by letter 3034 """ 3035
3036 - def __init__(self, 3037 options=None, 3038 multiple=True, 3039 size=None, 3040 cols=None, 3041 help_field=None, 3042 none=None, 3043 sort=True, 3044 orientation=None, 3045 table=True, 3046 no_opts=None, 3047 option_comment=None, 3048 ):
3049 """ 3050 Constructor 3051 3052 @param options: the options for the SELECT, as list of tuples 3053 [(value, label)], or as dict {value: label}, 3054 or None to auto-detect the options from the 3055 Field when called 3056 @param multiple: multiple options can be selected 3057 @param size: maximum number of options in merged letter-groups, 3058 None to not group options by initial letter 3059 @param cols: number of columns for the options table 3060 @param help_field: field in the referenced table to retrieve 3061 a tooltip text from (for foreign keys only) 3062 @param none: True to render "None" as normal option 3063 @param sort: sort the options (only effective if size==None) 3064 @param orientation: the ordering orientation, "columns"|"rows" 3065 @param table: whether to render options inside a table or not 3066 @param no_opts: text to show if no options available 3067 @param comment: HTML template to render after the LABELs 3068 """ 3069 3070 self.options = options 3071 self.multiple = multiple 3072 self.size = size 3073 self.cols = cols or 3 3074 self.help_field = help_field 3075 self.none = none 3076 self.sort = sort 3077 self.orientation = orientation 3078 self.table = table 3079 self.no_opts = no_opts 3080 self.option_comment = option_comment
3081 3082 # -------------------------------------------------------------------------
3083 - def __call__(self, field, value, **attributes):
3084 """ 3085 Render this widget 3086 3087 @param field: the Field 3088 @param value: the currently selected value(s) 3089 @param attributes: HTML attributes for the widget 3090 """ 3091 3092 fieldname = field.name 3093 3094 attr = Storage(attributes) 3095 if "_id" in attr: 3096 _id = attr.pop("_id") 3097 else: 3098 _id = "%s-options" % fieldname 3099 attr["_id"] = _id 3100 if "_name" not in attr: 3101 attr["_name"] = fieldname 3102 3103 options = self._options(field, value) 3104 if self.multiple: 3105 attr["_multiple"] = "multiple" 3106 widget = SELECT(**attr) 3107 if "empty" not in options: 3108 groups = options["groups"] 3109 append = widget.append 3110 render_group = self._render_group 3111 for group in groups: 3112 options = render_group(group) 3113 for option in options: 3114 append(option) 3115 3116 no_opts = self.no_opts 3117 if no_opts is None: 3118 no_opts = s3_str(current.T("No options available")) 3119 widget.add_class("groupedopts-widget") 3120 widget_opts = {"columns": self.cols, 3121 "emptyText": no_opts, 3122 "orientation": self.orientation or "columns", 3123 "sort": self.sort, 3124 "table": self.table, 3125 } 3126 3127 if self.option_comment: 3128 widget_opts["comment"] = self.option_comment 3129 s3_include_underscore() 3130 3131 script = '''$('#%s').groupedopts(%s)''' % \ 3132 (_id, json.dumps(widget_opts, separators=SEPARATORS)) 3133 jquery_ready = current.response.s3.jquery_ready 3134 if script not in jquery_ready: 3135 jquery_ready.append(script) 3136 3137 return widget
3138 3139 # -------------------------------------------------------------------------
3140 - def _render_group(self, group):
3141 """ 3142 Helper method to render an options group 3143 3144 @param group: the group as dict {label:label, items:[items]} 3145 """ 3146 3147 items = group["items"] 3148 if items: 3149 label = group["label"] 3150 render_item = self._render_item 3151 options = [render_item(i) for i in items] 3152 if label: 3153 return [OPTGROUP(options, _label=label)] 3154 else: 3155 return options 3156 else: 3157 return None
3158 3159 # ------------------------------------------------------------------------- 3160 @staticmethod
3161 - def _render_item(item):
3162 """ 3163 Helper method to render one option 3164 3165 @param item: the item as tuple (key, label, value, tooltip), 3166 value=True indicates that the item is selected 3167 """ 3168 3169 key, label, value, tooltip = item 3170 attr = {"_value": key} 3171 if value: 3172 attr["_selected"] = "selected" 3173 if tooltip: 3174 attr["_title"] = tooltip 3175 return OPTION(label, **attr)
3176 3177 # -------------------------------------------------------------------------
3178 - def _options(self, field, value):
3179 """ 3180 Find, group and sort the options 3181 3182 @param field: the Field 3183 @param value: the currently selected value(s) 3184 """ 3185 3186 # Get the options as sorted list of tuples (key, value) 3187 options = self.options 3188 if options is None: 3189 requires = field.requires 3190 if not isinstance(requires, (list, tuple)): 3191 requires = [requires] 3192 if hasattr(requires[0], "options"): 3193 options = requires[0].options() 3194 else: 3195 options = [] 3196 elif isinstance(options, dict): 3197 options = options.items() 3198 none = self.none 3199 exclude = ("",) if none is not None else ("", None) 3200 3201 options = [(s3_unicode(k) if k is not None else none, 3202 # Not working with multi-byte str components: 3203 #v.flatten() 3204 # if hasattr(v, "flatten") else s3_unicode(v)) 3205 s3_unicode(s3_strip_markup(v.xml())) 3206 if isinstance(v, DIV) else s3_unicode(v)) 3207 for k, v in options if k not in exclude] 3208 3209 # No options available? 3210 if not options: 3211 return {"empty": current.T("no options available")} 3212 3213 # Get the current values as list of unicode 3214 if not isinstance(value, (list, tuple)): 3215 values = [value] 3216 else: 3217 values = value 3218 values = [s3_unicode(v) for v in values] 3219 3220 # Get the tooltips as dict {key: tooltip} 3221 helptext = {} 3222 help_field = self.help_field 3223 if help_field: 3224 if callable(help_field): 3225 help_field = help_field(options) 3226 if isinstance(help_field, dict): 3227 for key in help_field.keys(): 3228 helptext[s3_unicode(key)] = help_field[key] 3229 else: 3230 ktablename, pkey = s3_get_foreign_key(field)[:2] 3231 if ktablename is not None: 3232 ktable = current.s3db[ktablename] 3233 if hasattr(ktable, help_field): 3234 keys = [k for k, v in options if k.isdigit()] 3235 query = ktable[pkey].belongs(keys) 3236 rows = current.db(query).select(ktable[pkey], 3237 ktable[help_field]) 3238 for row in rows: 3239 helptext[unicode(row[pkey])] = row[help_field] 3240 3241 # Get all letters and their options 3242 letter_options = {} 3243 for key, label in options: 3244 letter = label 3245 if letter: 3246 letter = s3_unicode(label).upper()[0] 3247 if letter in letter_options: 3248 letter_options[letter].append((key, label)) 3249 else: 3250 letter_options[letter] = [(key, label)] 3251 all_letters = letter_options.keys() 3252 3253 # Sort letters 3254 import locale 3255 if all_letters: 3256 all_letters.sort(locale.strcoll) 3257 first_letter = min(u"A", all_letters[0]) 3258 last_letter = max(u"Z", all_letters[-1]) 3259 else: 3260 # No point with grouping if we don't have any labels 3261 size = 0 3262 3263 size = self.size 3264 3265 close_group = self._close_group 3266 3267 if size and len(options) > size and len(letter_options) > 1: 3268 # Multiple groups 3269 3270 groups = [] 3271 group = {"letters": [first_letter], "items": []} 3272 3273 for letter in all_letters: 3274 3275 group_items = group["items"] 3276 current_size = len(group_items) 3277 items = letter_options[letter] 3278 3279 if current_size and current_size + len(items) > size: 3280 3281 # Close + append this group 3282 close_group(group, values, helptext) 3283 groups.append(group) 3284 3285 # Start a new group 3286 group = {"letters": [letter], "items": items} 3287 3288 else: 3289 3290 # Append current letter 3291 if letter != group["letters"][-1]: 3292 group["letters"].append(letter) 3293 3294 # Append items 3295 group["items"].extend(items) 3296 3297 if len(group["items"]): 3298 if group["letters"][-1] != last_letter: 3299 group["letters"].append(last_letter) 3300 close_group(group, values, helptext) 3301 groups.append(group) 3302 3303 else: 3304 # Only one group 3305 group = {"letters": None, "items": options} 3306 close_group(group, values, helptext, sort=self.sort) 3307 groups = [group] 3308 3309 return {"groups": groups}
3310 3311 # ------------------------------------------------------------------------- 3312 @staticmethod
3313 - def _close_group(group, values, helptext, sort=True):
3314 """ 3315 Helper method to finalize an options group, render its label 3316 and sort the options 3317 3318 @param group: the group as dict {letters: [], items: []} 3319 @param values: the currently selected values as list 3320 @param helptext: dict of {key: helptext} for the options 3321 """ 3322 3323 # Construct the group label 3324 group_letters = group["letters"] 3325 if group_letters: 3326 if len(group_letters) > 1: 3327 group["label"] = "%s - %s" % (group_letters[0], 3328 group_letters[-1]) 3329 else: 3330 group["label"] = group_letters[0] 3331 else: 3332 group["label"] = None 3333 del group["letters"] 3334 3335 # Sort the group items 3336 if sort: 3337 group_items = sorted(group["items"], 3338 key = lambda i: i[1].upper()[0] \ 3339 if i[1] else None, 3340 ) 3341 else: 3342 group_items = group["items"] 3343 3344 # Add tooltips 3345 items = [] 3346 T = current.T 3347 for key, label in group_items: 3348 tooltip = helptext.get(key) 3349 if tooltip: 3350 tooltip = s3_str(T(tooltip)) 3351 item = (key, label, key in values, tooltip) 3352 items.append(item) 3353 3354 group["items"] = items 3355 return
3356
3357 #============================================================================== 3358 -class S3RadioOptionsWidget(FormWidget):
3359 """ 3360 Widget with radio buttons for S3OptionsFilter 3361 - unused: can just use S3GroupedOptionsWidget with multiple=False 3362 """ 3363
3364 - def __init__(self, 3365 options=None, 3366 cols=None, 3367 help_field=None, 3368 none=None, 3369 sort=True):
3370 """ 3371 Constructor 3372 3373 @param options: the options for the SELECT, as list of tuples 3374 [(value, label)], or as dict {value: label}, 3375 or None to auto-detect the options from the 3376 Field when called 3377 @param cols: number of columns for the options table 3378 @param help_field: field in the referenced table to retrieve 3379 a tooltip text from (for foreign keys only) 3380 @param none: True to render "None" as normal option 3381 @param sort: sort the options 3382 """ 3383 3384 self.options = options 3385 self.cols = cols or 3 3386 self.help_field = help_field 3387 self.none = none 3388 self.sort = sort
3389 3390 # -------------------------------------------------------------------------
3391 - def __call__(self, field, value, **attributes):
3392 """ 3393 Render this widget 3394 3395 @param field: the Field 3396 @param value: the currently selected value(s) 3397 @param attributes: HTML attributes for the widget 3398 """ 3399 3400 fieldname = field.name 3401 3402 attr = Storage(attributes) 3403 if "_id" in attr: 3404 _id = attr.pop("_id") 3405 else: 3406 _id = "%s-options" % fieldname 3407 attr["_id"] = _id 3408 if "_name" not in attr: 3409 attr["_name"] = fieldname 3410 3411 options = self._options(field, value) 3412 if "empty" in options: 3413 widget = DIV(SPAN(options["empty"], 3414 _class="no-options-available"), 3415 INPUT(_type="hidden", 3416 _name=fieldname, 3417 _value=None), 3418 **attr) 3419 else: 3420 widget = DIV(**attr) 3421 append = widget.append 3422 render_item = self._render_item 3423 for option in options: 3424 item = render_item(fieldname, option) 3425 append(item) 3426 3427 return widget
3428 3429 # ------------------------------------------------------------------------- 3430 @staticmethod
3431 - def _render_item(fieldname, item):
3432 """ 3433 Helper method to render one option 3434 3435 @param item: the item as tuple (key, label, value, tooltip), 3436 value=True indicates that the item is selected 3437 """ 3438 3439 key, label, value, tooltip = item 3440 item_id = "%s%s" % (fieldname, key) 3441 attr = {"_type": "radio", 3442 "_name": fieldname, 3443 "_id": item_id, 3444 "_class": "s3-radioopts-option", 3445 "_value": key, 3446 } 3447 if value: 3448 attr["_checked"] = "checked" 3449 if tooltip: 3450 attr["_title"] = tooltip 3451 return DIV(INPUT(**attr), 3452 LABEL(label, _for=item_id), 3453 )
3454 3455 # -------------------------------------------------------------------------
3456 - def _options(self, field, value):
3457 """ 3458 Find and sort the options 3459 3460 @param field: the Field 3461 @param value: the currently selected value(s) 3462 """ 3463 3464 # Get the options as sorted list of tuples (key, value) 3465 options = self.options 3466 if options is None: 3467 requires = field.requires 3468 if not isinstance(requires, (list, tuple)): 3469 requires = [requires] 3470 if hasattr(requires[0], "options"): 3471 options = requires[0].options() 3472 else: 3473 options = [] 3474 elif isinstance(options, dict): 3475 options = options.items() 3476 none = self.none 3477 exclude = ("",) if none is not None else ("", None) 3478 options = [(s3_unicode(k) if k is not None else none, s3_unicode(v)) 3479 for k, v in options if k not in exclude] 3480 3481 # No options available? 3482 if not options: 3483 return {"empty": current.T("no options available")} 3484 3485 # Get the current values as list of unicode 3486 if not isinstance(value, (list, tuple)): 3487 values = [value] 3488 else: 3489 values = value 3490 values = [s3_unicode(v) for v in values] 3491 3492 # Get the tooltips as dict {key: tooltip} 3493 helptext = {} 3494 help_field = self.help_field 3495 if help_field: 3496 if callable(help_field): 3497 help_field = help_field(options) 3498 if isinstance(help_field, dict): 3499 for key in help_field.keys(): 3500 helptext[s3_unicode(key)] = help_field[key] 3501 else: 3502 ktablename, pkey = s3_get_foreign_key(field)[:2] 3503 if ktablename is not None: 3504 ktable = current.s3db[ktablename] 3505 if hasattr(ktable, help_field): 3506 keys = [k for k, v in options if k.isdigit()] 3507 query = ktable[pkey].belongs(keys) 3508 rows = current.db(query).select(ktable[pkey], 3509 ktable[help_field]) 3510 for row in rows: 3511 helptext[unicode(row[pkey])] = row[help_field] 3512 3513 # Prepare output for _render_item() 3514 _options = [] 3515 oappend = _options.append 3516 for k, v in options: 3517 tooltip = helptext.get(k, None) 3518 item = (k, v, k in values, tooltip) 3519 oappend(item) 3520 3521 if self.sort: 3522 # Sort options 3523 _options = sorted(_options, key=lambda i: i[1].upper()[0]) 3524 3525 return _options
3526
3527 # ============================================================================= 3528 -class S3HiddenWidget(StringWidget):
3529 """ 3530 Standard String widget, but with a class of hide 3531 - used by CAP 3532 """ 3533
3534 - def __call__(self, field, value, **attributes):
3535 3536 default = dict( 3537 _type = "text", 3538 value = (value is not None and str(value)) or "", 3539 ) 3540 attr = StringWidget._attributes(field, default, **attributes) 3541 attr["_class"] = "hide %s" % attr["_class"] 3542 3543 return TAG[""](INPUT(**attr), 3544 requires = field.requires 3545 )
3546
3547 # ============================================================================= 3548 -class S3HumanResourceAutocompleteWidget(FormWidget):
3549 """ 3550 Renders an hrm_human_resource SELECT as an INPUT field with 3551 AJAX Autocomplete. 3552 3553 Differs from the S3AutocompleteWidget in that it uses: 3554 3 name fields 3555 Organisation 3556 Job Role 3557 """ 3558
3559 - def __init__(self, 3560 post_process = "", 3561 group = "", # Filter to staff/volunteers/deployables 3562 ):
3563 3564 self.post_process = post_process 3565 self.group = group
3566
3567 - def __call__(self, field, value, **attributes):
3568 3569 settings = current.deployment_settings 3570 3571 group = self.group 3572 if not group and current.request.controller == "deploy": 3573 group = "deploy" 3574 3575 default = dict( 3576 _type = "text", 3577 value = (value is not None and str(value)) or "", 3578 ) 3579 attr = StringWidget._attributes(field, default, **attributes) 3580 3581 # Hide the real field 3582 attr["_class"] = "%s hide" % attr["_class"] 3583 3584 if "_id" in attr: 3585 real_input = attr["_id"] 3586 else: 3587 real_input = str(field).replace(".", "_") 3588 dummy_input = "dummy_%s" % real_input 3589 3590 if value: 3591 try: 3592 value = long(value) 3593 except ValueError: 3594 pass 3595 # Provide the representation for the current/default Value 3596 text = s3_unicode(field.represent(value)) 3597 if "<" in text: 3598 text = s3_strip_markup(text) 3599 represent = text 3600 else: 3601 represent = "" 3602 3603 delay = settings.get_ui_autocomplete_delay() 3604 min_length = settings.get_ui_autocomplete_min_chars() 3605 3606 script = '''S3.autocomplete.hrm('%(group)s','%(input)s',"%(postprocess)s"''' % \ 3607 dict(group = group, 3608 input = real_input, 3609 postprocess = self.post_process, 3610 ) 3611 if delay != 800: 3612 script = "%s,%s" % (script, delay) 3613 if min_length != 2: 3614 script = "%s,%s" % (script, min_length) 3615 elif min_length != 2: 3616 script = "%s,,%s" % (script, min_length) 3617 script = "%s)" % script 3618 3619 current.response.s3.jquery_ready.append(script) 3620 3621 return TAG[""](INPUT(_id=dummy_input, 3622 _class="string", 3623 _value=represent.encode("utf-8")), 3624 DIV(_id="%s_throbber" % dummy_input, 3625 _class="throbber input_throbber hide"), 3626 INPUT(**attr), 3627 requires = field.requires 3628 )
3629
3630 # ============================================================================= 3631 -class S3ImageCropWidget(FormWidget):
3632 """ 3633 Allows the user to crop an image and uploads it. 3634 Cropping & Scaling( if necessary ) done at client-side 3635 3636 @ToDo: Doesn't currently work with Inline Component Forms 3637 """ 3638
3639 - def __init__(self, image_bounds=None):
3640 """ 3641 @param image_bounds: Limits the Size of the Image that can be 3642 uploaded. 3643 Tuple/List - (MaxWidth, MaxHeight) 3644 """ 3645 self.image_bounds = image_bounds
3646
3647 - def __call__(self, field, value, download_url=None, **attributes):
3648 """ 3649 @param field: Field using this widget 3650 @param value: value if any 3651 @param download_url: Download URL for saved Image 3652 """ 3653 3654 T = current.T 3655 3656 script_dir = "/%s/static/scripts" % current.request.application 3657 3658 s3 = current.response.s3 3659 debug = s3.debug 3660 scripts = s3.scripts 3661 settings = current.deployment_settings 3662 3663 if debug: 3664 script = "%s/jquery.color.js" % script_dir 3665 if script not in scripts: 3666 scripts.append(script) 3667 script = "%s/jquery.Jcrop.js" % script_dir 3668 if script not in scripts: 3669 scripts.append(script) 3670 script = "%s/S3/s3.imagecrop.widget.js" % script_dir 3671 if script not in scripts: 3672 scripts.append(script) 3673 else: 3674 script = "%s/S3/s3.imagecrop.widget.min.js" % script_dir 3675 if script not in scripts: 3676 scripts.append(script) 3677 3678 s3.js_global.append(''' 3679 i18n.invalid_image='%s' 3680 i18n.supported_image_formats='%s' 3681 i18n.upload_new_image='%s' 3682 i18n.upload_image='%s' ''' % (T("Please select a valid image!"), 3683 T("Supported formats"), 3684 T("Upload different Image"), 3685 T("Upload Image"))) 3686 3687 stylesheets = s3.stylesheets 3688 sheet = "plugins/jquery.Jcrop.css" 3689 if sheet not in stylesheets: 3690 stylesheets.append(sheet) 3691 3692 attr = self._attributes(field, {"_type": "file", 3693 "_class": "imagecrop-upload" 3694 }, **attributes) 3695 3696 elements = [INPUT(_type="hidden", _name="imagecrop-points")] 3697 append = elements.append 3698 3699 append(DIV(_class="tooltip", 3700 _title="%s|%s" % \ 3701 (T("Crop Image"), 3702 T("Select an image to upload. You can crop this later by opening this record.")))) 3703 3704 # Set up the canvas 3705 # Canvas is used to scale and crop the Image on the client side 3706 canvas = TAG["canvas"](_class="imagecrop-canvas", 3707 _style="display:none") 3708 image_bounds = self.image_bounds 3709 3710 if image_bounds: 3711 canvas.attributes["_width"] = image_bounds[0] 3712 canvas.attributes["_height"] = image_bounds[1] 3713 else: 3714 # Images are not scaled and are uploaded as it is 3715 canvas.attributes["_width"] = 0 3716 3717 append(canvas) 3718 3719 btn_class = "imagecrop-btn button" 3720 if settings.ui.formstyle == "bootstrap": 3721 btn_class = "imagecrop-btn" 3722 3723 buttons = [ A(T("Enable Crop"), 3724 _id="select-crop-btn", 3725 _class=btn_class, 3726 _role="button"), 3727 A(T("Crop Image"), 3728 _id="crop-btn", 3729 _class=btn_class, 3730 _role="button"), 3731 A(T("Cancel"), 3732 _id="remove-btn", 3733 _class="imagecrop-btn") 3734 ] 3735 3736 parts = [LEGEND(T("Uploaded Image"))] + buttons + \ 3737 [HR(_style="display:none"), 3738 IMG(_id="uploaded-image", 3739 _style="display:none") 3740 ] 3741 3742 display_div = FIELDSET(parts, 3743 _class="image-container") 3744 3745 crop_data_attr = {"_type": "hidden", 3746 "_name": "imagecrop-data", 3747 "_class": "imagecrop-data" 3748 } 3749 3750 if value and download_url: 3751 if callable(download_url): 3752 download_url = download_url() 3753 3754 url = "%s/%s" % (download_url ,value) 3755 # Add Image 3756 crop_data_attr["_value"] = url 3757 append(FIELDSET(LEGEND(A(T("Upload different Image")), 3758 _id="upload-title"), 3759 DIV(INPUT(**attr), 3760 DIV(T("or Drop here"), 3761 _class="imagecrop-drag"), 3762 _id="upload-container", 3763 _style="display:none"))) 3764 else: 3765 append(FIELDSET(LEGEND(T("Upload Image"), 3766 _id="upload-title"), 3767 DIV(INPUT(**attr), 3768 DIV(T("or Drop here"), 3769 _class="imagecrop-drag"), 3770 _id="upload-container"))) 3771 3772 append(INPUT(**crop_data_attr)) 3773 append(display_div) 3774 # Prevent multiple widgets on the same page from interfering with each 3775 # other. 3776 import uuid 3777 uid = "cropwidget-%s" % uuid.uuid4().hex 3778 for element in elements: 3779 element.attributes["_data-uid"] = uid 3780 3781 return DIV(elements)
3782
3783 # ============================================================================= 3784 -class S3InvBinWidget(FormWidget):
3785 """ 3786 Widget used by S3CRUD to offer the user matching bins where 3787 stock items can be placed 3788 """ 3789
3790 - def __init__(self, 3791 tablename,):
3792 self.tablename = tablename
3793
3794 - def __call__(self, field, value, **attributes):
3795 3796 T = current.T 3797 request = current.request 3798 s3db = current.s3db 3799 tracktable = s3db.inv_track_item 3800 stocktable = s3db.inv_inv_item 3801 3802 new_div = INPUT(value = value or "", 3803 requires = field.requires, 3804 _id = "i_%s_%s" % (self.tablename, field.name), 3805 _name = field.name, 3806 ) 3807 id = None 3808 function = self.tablename[4:] 3809 if len(request.args) > 2: 3810 if request.args[1] == function: 3811 id = request.args[2] 3812 3813 if id == None or tracktable[id] == None: 3814 return TAG[""]( 3815 new_div 3816 ) 3817 3818 record = tracktable[id] 3819 site_id = s3db.inv_recv[record.recv_id].site_id 3820 query = (stocktable.site_id == site_id) & \ 3821 (stocktable.item_id == record.item_id) & \ 3822 (stocktable.item_source_no == record.item_source_no) & \ 3823 (stocktable.item_pack_id == record.item_pack_id) & \ 3824 (stocktable.currency == record.currency) & \ 3825 (stocktable.pack_value == record.pack_value) & \ 3826 (stocktable.expiry_date == record.expiry_date) & \ 3827 (stocktable.supply_org_id == record.supply_org_id) 3828 rows = current.db(query).select(stocktable.bin, 3829 stocktable.id) 3830 if len(rows) == 0: 3831 return TAG[""]( 3832 new_div 3833 ) 3834 bins = [] 3835 for row in rows: 3836 bins.append(OPTION(row.bin)) 3837 3838 match_lbl = LABEL(T("Select an existing bin")) 3839 match_div = SELECT(bins, 3840 _id = "%s_%s" % (self.tablename, field.name), 3841 _name = field.name, 3842 ) 3843 new_lbl = LABEL(T("...or add a new bin")) 3844 return TAG[""](match_lbl, 3845 match_div, 3846 new_lbl, 3847 new_div 3848 )
3849
3850 # ============================================================================= 3851 -class S3KeyValueWidget(ListWidget):
3852 """ 3853 Allows for input of key-value pairs and stores them as list:string 3854 """ 3855
3856 - def __init__(self, key_label=None, value_label=None):
3857 """ 3858 Returns a widget with key-value fields 3859 """ 3860 self._class = "key-value-pairs" 3861 T = current.T 3862 3863 self.key_label = key_label or T("Key") 3864 self.value_label = value_label or T("Value")
3865
3866 - def __call__(self, field, value, **attributes):
3867 3868 s3 = current.response.s3 3869 3870 _id = "%s_%s" % (field._tablename, field.name) 3871 _name = field.name 3872 _class = "text hide" 3873 3874 attributes["_id"] = _id 3875 attributes["_name"] = _name 3876 attributes["_class"] = _class 3877 3878 script = SCRIPT( 3879 '''jQuery(document).ready(function(){jQuery('#%s').kv_pairs('%s','%s')})''' % \ 3880 (_id, self.key_label, self.value_label)) 3881 3882 if not value: 3883 value = "[]" 3884 if not isinstance(value, str): 3885 try: 3886 value = json.dumps(value, separators=SEPARATORS) 3887 except: 3888 raise("Bad value for key-value pair field") 3889 appname = current.request.application 3890 jsfile = "/%s/static/scripts/S3/%s" % (appname, "s3.keyvalue.widget.js") 3891 3892 if jsfile not in s3.scripts: 3893 s3.scripts.append(jsfile) 3894 3895 return TAG[""]( 3896 TEXTAREA(value, **attributes), 3897 script 3898 )
3899 3900 @staticmethod
3901 - def represent(value):
3902 if isinstance(value, str): 3903 try: 3904 value = json.loads(value) 3905 if isinstance(value, str): 3906 raise ValueError("key-value JSON is wrong.") 3907 except: 3908 # XXX: log this! 3909 #raise ValueError("Bad json was found as value for a key-value field: %s" % value) 3910 return "" 3911 3912 rep = [] 3913 if isinstance(value, (tuple, list)): 3914 for kv in value: 3915 rep += ["%s: %s" % (kv["key"], kv["value"])] 3916 return ", ".join(rep)
3917
3918 # ============================================================================= 3919 -class S3LatLonWidget(DoubleWidget):
3920 """ 3921 Widget for latitude or longitude input, gives option to input in terms 3922 of degrees, minutes and seconds 3923 """ 3924
3925 - def __init__(self, type, switch=False, disabled=False):
3926 self.type = type 3927 self.disabled = disabled 3928 self.switch = switch
3929
3930 - def widget(self, field, value, **attributes):
3931 3932 T = current.T 3933 s3 = current.response.s3 3934 switch = self.switch 3935 3936 if field: 3937 # LocationLatLonWidget 3938 id = name = "%s_%s" % (str(field).replace(".", "_"), self.type) 3939 else: 3940 # LocationSelectorWidget[2] 3941 id = name = "gis_location_%s" % self.type 3942 attr = dict(value=value, 3943 _class="decimal %s" % self._class, 3944 _id=id, 3945 _name=name) 3946 3947 attr_dms = dict() 3948 3949 if self.disabled: 3950 attr["_disabled"] = "disabled" 3951 attr_dms["_disabled"] = "disabled" 3952 3953 dms_boxes = SPAN(INPUT(_class="degrees", **attr_dms), "° ", 3954 INPUT(_class="minutes", **attr_dms), "' ", 3955 INPUT(_class="seconds", **attr_dms), "\" ", 3956 ["", 3957 DIV(A(T("Use decimal"), 3958 _class="action-btn gis_coord_switch_decimal")) 3959 ][switch], 3960 _style="display:none", 3961 _class="gis_coord_dms", 3962 ) 3963 3964 decimal = SPAN(INPUT(**attr), 3965 ["", 3966 DIV(A(T("Use deg, min, sec"), 3967 _class="action-btn gis_coord_switch_dms")) 3968 ][switch], 3969 _class="gis_coord_decimal", 3970 ) 3971 3972 if not s3.lat_lon_i18n_appended: 3973 s3.js_global.append(''' 3974 i18n.gis_only_numbers={degrees:'%s',minutes:'%s',seconds:'%s',decimal:'%s'} 3975 i18n.gis_range_error={degrees:{lat:'%s',lon:'%s'},minutes:'%s',seconds:'%s',decimal:{lat:'%s',lon:'%s'}} 3976 ''' % (T("Degrees must be a number."), 3977 T("Minutes must be a number."), 3978 T("Seconds must be a number."), 3979 T("Degrees must be a number."), 3980 T("Degrees in a latitude must be between -90 to 90."), 3981 T("Degrees in a longitude must be between -180 to 180."), 3982 T("Minutes must be less than 60."), 3983 T("Seconds must be less than 60."), 3984 T("Latitude must be between -90 and 90."), 3985 T("Longitude must be between -180 and 180."))) 3986 3987 if s3.debug: 3988 script = "/%s/static/scripts/S3/s3.gis.latlon.js" % \ 3989 current.request.application 3990 else: 3991 script = "/%s/static/scripts/S3/s3.gis.latlon.min.js" % \ 3992 current.request.application 3993 s3.scripts.append(script) 3994 s3.lat_lon_i18n_appended = True 3995 3996 return SPAN(decimal, 3997 dms_boxes, 3998 _class="gis_coord_wrap", 3999 )
4000
4001 # ============================================================================= 4002 -class S3LocationAutocompleteWidget(FormWidget):
4003 """ 4004 Renders a gis_location SELECT as an INPUT field with AJAX Autocomplete 4005 4006 Appropriate when the location has been previously created (as is the 4007 case for location groups or other specialized locations that need 4008 the location create form). 4009 S3LocationSelectorWidget is generally more appropriate for specific locations. 4010 4011 Currently used for selecting the region location in gis_config 4012 and for project/location. 4013 """ 4014
4015 - def __init__(self, 4016 level = "", 4017 post_process = "", 4018 ):
4019 4020 self.level = level 4021 self.post_process = post_process
4022
4023 - def __call__(self, field, value, **attributes):
4024 4025 settings = current.deployment_settings 4026 4027 level = self.level 4028 if isinstance(level, list): 4029 levels = "" 4030 counter = 0 4031 for _level in level: 4032 levels += _level 4033 if counter < len(level): 4034 levels += "|" 4035 counter += 1 4036 4037 default = dict( 4038 _type = "text", 4039 value = (value is not None and s3_unicode(value)) or "", 4040 ) 4041 attr = StringWidget._attributes(field, default, **attributes) 4042 4043 # Hide the real field 4044 attr["_class"] = attr["_class"] + " hide" 4045 4046 if "_id" in attr: 4047 real_input = attr["_id"] 4048 else: 4049 real_input = str(field).replace(".", "_") 4050 4051 dummy_input = "dummy_%s" % real_input 4052 4053 if value: 4054 try: 4055 value = long(value) 4056 except ValueError: 4057 pass 4058 # Provide the representation for the current/default Value 4059 text = s3_unicode(field.represent(value)) 4060 if "<" in text: 4061 text = s3_strip_markup(text) 4062 represent = text.encode("utf-8") 4063 else: 4064 represent = "" 4065 4066 delay = settings.get_ui_autocomplete_delay() 4067 min_length = settings.get_ui_autocomplete_min_chars() 4068 4069 # Mandatory part 4070 script = '''S3.autocomplete.location("%s"''' % real_input 4071 # Optional parts 4072 if self.post_process: 4073 # We need all 4074 script = '''%s,'%s',%s,%s,"%s"''' % (script, level, min_length, delay, self.post_process) 4075 elif delay != 800: 4076 script = '''%s,"%s",%s,%s''' % (script, level, min_length, delay) 4077 elif min_length != 2: 4078 script = '''%s,"%s",%s''' % (script, level, min_length) 4079 elif level: 4080 script = '''%s,"%s"''' % (script, level) 4081 # Close 4082 script = "%s)" % script 4083 current.response.s3.jquery_ready.append(script) 4084 return TAG[""](INPUT(_id=dummy_input, 4085 _class="string", 4086 value=represent), 4087 DIV(_id="%s_throbber" % dummy_input, 4088 _class="throbber input_throbber hide"), 4089 INPUT(**attr), 4090 requires = field.requires 4091 )
4092
4093 # ============================================================================= 4094 -class S3LocationDropdownWidget(FormWidget):
4095 """ 4096 Renders a dropdown for an Lx level of location hierarchy 4097 """ 4098
4099 - def __init__(self, level="L0", default=None, validate=False, empty=DEFAULT, blank=False):
4100 """ 4101 Constructor 4102 4103 @param level: the Lx-level (as string) 4104 @param default: the default location name 4105 @param validate: validate input in-widget (special purpose) 4106 @param empty: allow selection to be empty 4107 @param blank: start without options (e.g. when options are 4108 Ajax-added later by filterOptionsS3) 4109 """ 4110 4111 self.level = level 4112 self.default = default 4113 self.validate = validate 4114 self.empty = empty 4115 self.blank = blank
4116
4117 - def __call__(self, field, value, **attributes):
4118 4119 level = self.level 4120 default = self.default 4121 empty = self.empty 4122 4123 opts = [] 4124 # Get locations 4125 s3db = current.s3db 4126 table = s3db.gis_location 4127 if self.blank: 4128 query = (table.id == value) 4129 elif level: 4130 query = (table.deleted != True) & \ 4131 (table.level == level) 4132 else: 4133 # Workaround for merge form 4134 query = (table.id == value) 4135 locations = current.db(query).select(table.name, 4136 table.id, 4137 cache=s3db.cache) 4138 4139 # Build OPTIONs 4140 for location in locations: 4141 opts.append(OPTION(location.name, _value=location.id)) 4142 if not value and default and location.name == default: 4143 value = location.id 4144 4145 # Widget attributes 4146 attr = dict(attributes) 4147 attr["_type"] = "int" 4148 attr["value"] = value 4149 attr = OptionsWidget._attributes(field, attr) 4150 4151 if self.validate: 4152 # Validate widget input to enforce Lx subset 4153 # - not normally needed (Field validation should suffice) 4154 requires = IS_IN_SET(locations.as_dict()) 4155 if empty is DEFAULT: 4156 # Introspect the field 4157 empty = isinstance(field.requires, IS_EMPTY_OR) 4158 if empty: 4159 requires = IS_EMPTY_OR(requires) 4160 4161 # Skip in-widget validation on POST if inline 4162 widget_id = attr.get("_id") 4163 if widget_id and widget_id[:4] == "sub_": 4164 from s3forms import SKIP_POST_VALIDATION 4165 requires = SKIP_POST_VALIDATION(requires) 4166 4167 widget = TAG[""](SELECT(*opts, **attr), requires = requires) 4168 else: 4169 widget = SELECT(*opts, **attr) 4170 4171 return widget
4172
4173 # ============================================================================= 4174 -class S3LocationLatLonWidget(FormWidget):
4175 """ 4176 Renders a Lat & Lon input for a Location 4177 """ 4178
4179 - def __init__(self, empty=False):
4180 """ Set Defaults """ 4181 self.empty = empty
4182
4183 - def __call__(self, field, value, **attributes):
4184 4185 T = current.T 4186 empty = self.empty 4187 requires = IS_LAT_LON(field) 4188 if empty: 4189 requires = IS_EMPTY_OR(requires) 4190 4191 defaults = dict(_type = "text", 4192 value = (value is not None and str(value)) or "") 4193 attr = StringWidget._attributes(field, defaults, **attributes) 4194 # Hide the real field 4195 attr["_class"] = "hide" 4196 4197 if value: 4198 db = current.db 4199 table = db.gis_location 4200 record = db(table.id == value).select(table.lat, 4201 table.lon, 4202 limitby=(0, 1) 4203 ).first() 4204 try: 4205 lat = record.lat 4206 lon = record.lon 4207 except AttributeError: 4208 lat = None 4209 lon = None 4210 else: 4211 lat = None 4212 lon = None 4213 4214 rows = TAG[""]() 4215 4216 formstyle = current.response.s3.crud.formstyle 4217 4218 comment = "" 4219 selector = str(field).replace(".", "_") 4220 row_id = "%s_lat" % selector 4221 label = T("Latitude") 4222 widget = S3LatLonWidget("lat").widget(field, lat) 4223 label = "%s:" % label 4224 if not empty: 4225 label = DIV(label, 4226 SPAN(" *", _class="req")) 4227 4228 row = formstyle(row_id, label, widget, comment) 4229 if isinstance(row, tuple): 4230 for r in row: 4231 rows.append(r) 4232 else: 4233 rows.append(row) 4234 4235 row_id = "%s_lon" % selector 4236 label = T("Longitude") 4237 widget = S3LatLonWidget("lon", switch=True).widget(field, lon) 4238 label = "%s:" % label 4239 if not empty: 4240 label = DIV(label, 4241 SPAN(" *", _class="req")) 4242 row = formstyle(row_id, label, widget, comment) 4243 if isinstance(row, tuple): 4244 for r in row: 4245 rows.append(r) 4246 else: 4247 rows.append(row) 4248 4249 return TAG[""](INPUT(**attr), 4250 *rows, 4251 requires = requires 4252 )
4253
4254 # ============================================================================= 4255 -class S3Selector(FormWidget):
4256 """ 4257 Base class for JSON-based complex selectors (e.g. S3LocationSelector), 4258 used to detect this widget class during form processing, and to apply 4259 a common API. 4260 4261 Subclasses must implement: 4262 - __call__().........widget renderer 4263 - extract()..........extract the values dict from the database 4264 - represent()........representation method for new/updated value 4265 dicts (before DB commit) 4266 - validate().........validator for the JSON input 4267 - postprocess()......post-process to create/update records 4268 4269 Subclasses should use: 4270 - inputfield()......to generate the hidden input field 4271 - parse()...........to parse the JSON from the hidden input field 4272 """ 4273
4274 - def __call__(self, field, value, **attributes):
4275 """ 4276 Widget renderer. 4277 4278 To be implemented in subclass. 4279 4280 @param field: the Field 4281 @param value: the current value(s) 4282 @param attr: additional HTML attributes for the widget 4283 4284 @return: the widget HTML 4285 """ 4286 4287 values = self.parse(value) 4288 4289 return self.inputfield(field, values, "s3-selector", **attributes)
4290 4291 # -------------------------------------------------------------------------
4292 - def extract(self, record_id, values=None):
4293 """ 4294 Extract the record from the database and update values. 4295 4296 To be implemented in subclass. 4297 4298 @param record_id: the record ID 4299 @param values: the values dict 4300 4301 @return: the (updated) values dict 4302 """ 4303 4304 if values is None: 4305 values = {} 4306 values["id"] = record_id 4307 4308 return values
4309 4310 # -------------------------------------------------------------------------
4311 - def represent(self, value):
4312 """ 4313 Representation method for new or updated value dicts. 4314 4315 IMPORTANT: This method *must not* change DB status because it 4316 is called from inline forms before the the row is 4317 committed to the DB, so any DB status change would 4318 be invalid at this point. 4319 4320 To be implemented in subclass. 4321 4322 @param values: the values dict 4323 4324 @return: string representation for the values dict 4325 """ 4326 4327 return s3_unicode(value)
4328 4329 # -------------------------------------------------------------------------
4330 - def validate(self, value, requires=None):
4331 """ 4332 Parse and validate the input value, but don't create or update 4333 any records. This will be called by S3CRUD.validate to validate 4334 inline-form values. 4335 4336 To be implemented in subclass. 4337 4338 @param value: the value from the form 4339 @param requires: the field validator 4340 @returns: tuple (values, error) with values being the parsed 4341 value dict, and error any validation errors 4342 """ 4343 4344 values = self.parse(value) 4345 4346 return values, None
4347 4348 # -------------------------------------------------------------------------
4349 - def postprocess(self, value):
4350 """ 4351 Post-process to create or update records. Called during POST 4352 before validation of the outer form. 4353 4354 To be implemented in subclass. 4355 4356 @param value: the value from the form (as JSON) 4357 @return: tuple (record_id, error) 4358 """ 4359 4360 # Convert value into dict and validate 4361 values, error = self.validate(value) 4362 if values: 4363 record_id = values.get("id") 4364 else: 4365 record_id = None 4366 4367 # Return on validation error 4368 if error: 4369 # Make sure to return None to not override the field values 4370 return None, error 4371 4372 # Post-process goes here (no post-process in base class) 4373 4374 # Make sure to return the record ID, no the values dict 4375 return record_id, None
4376 4377 # -------------------------------------------------------------------------
4378 - def inputfield(self, field, values, classes, **attributes):
4379 """ 4380 Generate the (hidden) input field. Should be used in __call__. 4381 4382 @param field: the Field 4383 @param values: the parsed value (as dict) 4384 @param classes: standard HTML classes 4385 @param attributes: the widget attributes as passed in to the widget 4386 4387 @return: the INPUT field 4388 """ 4389 4390 if isinstance(classes, (tuple, list)): 4391 _class = " ".join(classes) 4392 else: 4393 _class = classes 4394 4395 requires = self.postprocess 4396 4397 fieldname = str(field).replace(".", "_") 4398 if fieldname.startswith("sub_"): 4399 from s3forms import SKIP_POST_VALIDATION 4400 requires = SKIP_POST_VALIDATION(requires) 4401 4402 defaults = dict(requires = requires, 4403 _type = "hidden", 4404 _class = _class, 4405 ) 4406 attr = FormWidget._attributes(field, defaults, **attributes) 4407 4408 return INPUT(_value = self.serialize(values), **attr)
4409 4410 # -------------------------------------------------------------------------
4411 - def serialize(self, values):
4412 """ 4413 Serialize the values (as JSON string). Called from inputfield(). 4414 4415 @param values: the values (as dict) 4416 @return: the serialized values 4417 """ 4418 4419 return json.dumps(values, separators=SEPARATORS)
4420 4421 # -------------------------------------------------------------------------
4422 - def parse(self, value):
4423 """ 4424 Parse the form value into a dict. The value would be a record 4425 id if coming from the database, or a JSON string when coming 4426 from a form. Should be called from validate(), doesn't need to 4427 be re-implemented in subclass. 4428 4429 @param value: the value 4430 @return: the parsed data as dict 4431 """ 4432 4433 record_id = None 4434 values = None 4435 4436 if value: 4437 if isinstance(value, basestring): 4438 if value.isdigit(): 4439 record_id = long(value) 4440 else: 4441 try: 4442 values = json.loads(value) 4443 except ValueError: 4444 pass 4445 else: 4446 record_id = value 4447 else: 4448 record_id = None 4449 4450 if values is None: 4451 values = {"id": record_id} 4452 4453 return values
4454
4455 # ============================================================================= 4456 -class S3LocationSelector(S3Selector):
4457 """ 4458 Form widget to select a location_id that can also 4459 create/update the location 4460 4461 Differences to the original S3LocationSelectorWidget: 4462 * Allows selection of either an Lx or creation of a new Point 4463 within the lowest Lx level 4464 * Uses dropdowns not autocompletes 4465 * Selection of lower Lx levels only happens when higher-level 4466 have been done 4467 4468 Implementation Notes: 4469 * Performance: Create JSON for the hierarchy, along with bboxes for 4470 the map zoom - loaded progressively rather than all as 4471 one big download 4472 h = {id : {'n' : name, 4473 'l' : level, 4474 'f' : parent 4475 }} 4476 4477 Limitations (@todo): 4478 * Doesn't allow creation of new Lx Locations 4479 * Doesn't support manual entry of LatLons 4480 * Doesn't allow selection of existing specific Locations 4481 * Doesn't support variable Levels by Country 4482 * Use in an InlineComponent with multiple=False needs completing: 4483 - Validation errors cause issues 4484 - Needs more testing 4485 * Should support use in an InlineComponent with multiple=True 4486 * Should support multiple on a page 4487 """ 4488 4489 keys = ("L0", "L1", "L2", "L3", "L4", "L5", 4490 "address", "postcode", "lat", "lon", "wkt", "specific", "id", "radius") 4491
4492 - def __init__(self, 4493 levels = None, 4494 required_levels = None, 4495 hide_lx = True, 4496 reverse_lx = False, 4497 show_address = False, 4498 show_postcode = None, 4499 show_latlon = None, 4500 latlon_mode = "decimal", 4501 latlon_mode_toggle = True, 4502 show_map = None, 4503 open_map_on_load = False, 4504 feature_required = False, 4505 lines = False, 4506 points = True, 4507 polygons = False, 4508 circles = False, 4509 color_picker = False, 4510 catalog_layers = False, 4511 min_bbox = None, 4512 labels = True, 4513 placeholders = False, 4514 error_message = None, 4515 represent = None, 4516 prevent_duplicate_addresses = False, 4517 ):
4518 """ 4519 Constructor 4520 4521 @param levels: list or tuple of hierarchy levels (names) to expose, 4522 in order (e.g. ("L0", "L1", "L2")) 4523 or False to disable completely 4524 @param required_levels: list or tuple of required hierarchy levels (if empty, 4525 only the highest selectable Lx will be required) 4526 @param hide_lx: hide Lx selectors until higher level has been selected 4527 @param reverse_lx: render Lx selectors in the order usually used by 4528 street Addresses (lowest level first), and below the 4529 address line 4530 @param show_address: show a field for street address. 4531 If the parameter is set to a string then this is used as the label. 4532 @param show_postcode: show a field for postcode 4533 @param show_latlon: show fields for manual Lat/Lon input 4534 @param latlon_mode: (initial) lat/lon input mode ("decimal" or "dms") 4535 @param latlon_mode_toggle: allow user to toggle lat/lon input mode 4536 @param show_map: show a map to select specific points 4537 @param open_map_on_load: show map on load 4538 @param feature_required: map feature is required 4539 @param lines: use a line draw tool 4540 @param points: use a point draw tool 4541 @param polygons: use a polygon draw tool 4542 @param circles: use a circle draw tool 4543 @param color_picker: display a color-picker to set per-feature styling 4544 (also need to enable in the feature layer to show on map) 4545 @param catalog_layers: display catalogue layers or just the default base layer 4546 @param min_bbox: minimum BBOX in map selector, used to determine automatic 4547 zoom level for single-point locations 4548 @param labels: show labels on inputs 4549 @param placeholders: show placeholder text in inputs 4550 @param error_message: default error message for server-side validation 4551 @param represent: an S3Represent instance that can represent non-DB rows 4552 @param prevent_duplicate_addresses: do a check for duplicate addresses & prevent 4553 creation of record if a dupe is found 4554 """ 4555 4556 settings = current.deployment_settings 4557 4558 self._initlx = True 4559 self._levels = levels 4560 self._required_levels = required_levels 4561 self._load_levels = None 4562 4563 self.hide_lx = hide_lx 4564 self.reverse_lx = reverse_lx 4565 self.show_address = show_address 4566 self.show_postcode = show_postcode 4567 self.prevent_duplicate_addresses = prevent_duplicate_addresses 4568 4569 if show_latlon is None: 4570 show_latlon = settings.get_gis_latlon_selector() 4571 self.show_latlon = show_latlon 4572 self.latlon_mode = latlon_mode 4573 if show_latlon: 4574 # @todo: latlon_toggle_mode should default to a deployment setting 4575 self.latlon_mode_toggle = latlon_mode_toggle 4576 else: 4577 self.latlon_mode_toggle = False 4578 4579 if feature_required: 4580 show_map = True 4581 if not any((points,lines, polygons, circles)): 4582 points = True 4583 if lines or polygons or circles: 4584 required = "wkt" if not points else "any" 4585 else: 4586 required = "latlon" 4587 self.feature_required = required 4588 else: 4589 self.feature_required = None 4590 if show_map is None: 4591 show_map = settings.get_gis_map_selector() 4592 self.show_map = show_map 4593 self.open_map_on_load = show_map and open_map_on_load 4594 4595 self.lines = lines 4596 self.points = points 4597 self.polygons = polygons 4598 self.circles = circles 4599 4600 self.color_picker = color_picker 4601 self.catalog_layers = catalog_layers 4602 4603 self.min_bbox = min_bbox or settings.get_gis_bbox_min_size() 4604 4605 self.labels = labels 4606 self.placeholders = placeholders 4607 4608 self.error_message = error_message 4609 self._represent = represent
4610 4611 # ------------------------------------------------------------------------- 4612 @property
4613 - def levels(self):
4614 """ Lx-levels to expose as dropdowns """ 4615 4616 levels = self._levels 4617 if self._initlx: 4618 lx = [] 4619 if levels is False: 4620 levels = [] 4621 elif not levels: 4622 # Which levels of Hierarchy are we using? 4623 levels = current.gis.get_relevant_hierarchy_levels() 4624 if levels is None: 4625 levels = [] 4626 if not isinstance(levels, (tuple, list)): 4627 levels = [levels] 4628 for level in levels: 4629 if level not in lx: 4630 lx.append(level) 4631 for level in self.required_levels: 4632 if level not in lx: 4633 lx.append(level) 4634 levels = self._levels = lx 4635 self._initlx = False 4636 return levels
4637 4638 # ------------------------------------------------------------------------- 4639 @property
4640 - def required_levels(self):
4641 """ Lx-levels to treat as required """ 4642 4643 levels = self._required_levels 4644 if self._initlx: 4645 if levels is None: 4646 levels = set() 4647 elif not isinstance(levels, (list, tuple)): 4648 levels = [levels] 4649 self._required_levels = levels 4650 return levels
4651 4652 # ------------------------------------------------------------------------- 4653 @property
4654 - def load_levels(self):
4655 """ 4656 Lx-levels to load from the database = all levels down to the 4657 lowest exposed level (L0=highest, L5=lowest) 4658 """ 4659 4660 load_levels = self._load_levels 4661 4662 if load_levels is None: 4663 load_levels = ("L0", "L1", "L2", "L3", "L4", "L5") 4664 while load_levels: 4665 if load_levels[-1] in self.levels: 4666 break 4667 else: 4668 load_levels = load_levels[:-1] 4669 self._load_levels = load_levels 4670 4671 return load_levels
4672 4673 # ------------------------------------------------------------------------- 4674 @property
4675 - def mobile(self):
4676 """ 4677 Mobile widget settings 4678 4679 @ToDo: Expose configuration options 4680 """ 4681 4682 widget = {"type": "location", 4683 } 4684 4685 return widget
4686 4687 # -------------------------------------------------------------------------
4688 - def __call__(self, field, value, **attributes):
4689 """ 4690 Widget renderer 4691 4692 @param field: the Field 4693 @param value: the current value(s) 4694 @param attr: additional HTML attributes for the widget 4695 """ 4696 4697 # Environment 4698 T = current.T 4699 db = current.db 4700 4701 s3db = current.s3db 4702 4703 request = current.request 4704 s3 = current.response.s3 4705 4706 # Is the location input required? 4707 requires = field.requires 4708 if requires: 4709 required = not hasattr(requires, "other") 4710 else: 4711 required = False 4712 4713 # Don't use this widget/validator in appadmin 4714 if request.controller == "appadmin": 4715 attr = FormWidget._attributes(field, {}, **attributes) 4716 if required: 4717 requires = IS_LOCATION() 4718 else: 4719 requires = IS_EMPTY_OR(IS_LOCATION()) 4720 return TAG[""](INPUT(**attr), requires=requires) 4721 4722 # Settings 4723 settings = current.deployment_settings 4724 countries = settings.get_gis_countries() 4725 4726 # Read the currently active GIS config 4727 gis = current.gis 4728 config = gis.get_config() 4729 4730 # Parse the current value 4731 values = self.parse(value) 4732 location_id = values.get("id") 4733 4734 # Determine the default location and bounds 4735 gtable = s3db.gis_location 4736 4737 default = field.default 4738 default_bounds = None 4739 4740 if not default: 4741 # Check for a default location in the active gis_config 4742 default = config.default_location_id 4743 4744 if not default: 4745 # Fall back to default country (if only one) 4746 if len(countries) == 1: 4747 ttable = s3db.gis_location_tag 4748 query = (ttable.tag == "ISO2") & \ 4749 (ttable.value == countries[0]) & \ 4750 (ttable.location_id == gtable.id) 4751 country = db(query).select(gtable.id, 4752 gtable.lat_min, 4753 gtable.lon_min, 4754 gtable.lat_max, 4755 gtable.lon_max, 4756 cache=s3db.cache, 4757 limitby=(0, 1)).first() 4758 try: 4759 default = country.id 4760 default_bounds = [country.lon_min, 4761 country.lat_min, 4762 country.lon_max, 4763 country.lat_max, 4764 ] 4765 except AttributeError: 4766 error = "Default country data not in database (incorrect prepop setting?)" 4767 current.log.critical(error) 4768 if s3.debug: 4769 raise RuntimeError(error) 4770 4771 if not location_id and values.keys() == ["id"]: 4772 location_id = values["id"] = default 4773 4774 # Update the values dict from the database 4775 values = self.extract(location_id, values=values) 4776 4777 # The lowest level we have a value for, but no selector exposed 4778 levels = self.levels 4779 load_levels = self.load_levels 4780 lowest_lx = None 4781 for level in load_levels[::-1]: 4782 if level not in levels and values.get(level): 4783 lowest_lx = level 4784 break 4785 4786 # Field name for ID construction 4787 fieldname = attributes.get("_name") 4788 if not fieldname: 4789 fieldname = str(field).replace(".", "_") 4790 4791 # Load initial Hierarchy Labels (for Lx dropdowns) 4792 labels, labels_compact = self._labels(levels, 4793 country=values.get("L0"), 4794 ) 4795 4796 # Load initial Hierarchy Locations (to populate Lx dropdowns) 4797 location_dict = self._locations(levels, 4798 values, 4799 default_bounds = default_bounds, 4800 lowest_lx = lowest_lx, 4801 config = config, 4802 ) 4803 4804 # Render visual components 4805 components = {} 4806 manual_input = self._input 4807 4808 # Street Address INPUT 4809 show_address = self.show_address 4810 if show_address: 4811 address = values.get("address") 4812 if show_address is True: 4813 label = gtable.addr_street.label 4814 else: 4815 label = show_address 4816 components["address"] = manual_input(fieldname, 4817 "address", 4818 address, 4819 label, 4820 hidden = not address, 4821 ) 4822 4823 # Postcode INPUT 4824 show_postcode = self.show_postcode 4825 if show_postcode is None: 4826 # Use global setting 4827 show_postcode = settings.get_gis_postcode_selector() 4828 if show_postcode: 4829 postcode = values.get("postcode") 4830 components["postcode"] = manual_input(fieldname, 4831 "postcode", 4832 postcode, 4833 settings.get_ui_label_postcode(), 4834 hidden = not postcode, 4835 ) 4836 4837 # Lat/Lon INPUTs 4838 lat = values.get("lat") 4839 lon = values.get("lon") 4840 if self.show_latlon: 4841 hidden = not lat and not lon 4842 components["lat"] = manual_input(fieldname, 4843 "lat", 4844 lat, 4845 T("Latitude"), 4846 hidden = hidden, 4847 _class = "double", 4848 ) 4849 components["lon"] = manual_input(fieldname, 4850 "lon", 4851 lon, 4852 T("Longitude"), 4853 hidden = hidden, 4854 _class = "double", 4855 ) 4856 4857 # Lx Dropdowns 4858 multiselect = settings.get_ui_multiselect_widget() 4859 lx_rows = self._lx_selectors(field, 4860 fieldname, 4861 levels, 4862 labels, 4863 multiselect=multiselect, 4864 required=required, 4865 ) 4866 components.update(lx_rows) 4867 4868 # Lat/Lon Input Mode Toggle 4869 if self.latlon_mode_toggle: 4870 latlon_labels = {"decimal": T("Use decimal"), 4871 "dms": T("Use deg, min, sec"), 4872 } 4873 if self.latlon_mode == "dms": 4874 latlon_label = latlon_labels["decimal"] 4875 else: 4876 latlon_label = latlon_labels["dms"] 4877 toggle_id = fieldname + "_latlon_toggle" 4878 components["latlon_toggle"] = ("", 4879 A(latlon_label, 4880 _id=toggle_id, 4881 _class="action-lnk", 4882 ), 4883 toggle_id, 4884 False, 4885 ) 4886 else: 4887 latlon_labels = None 4888 4889 # Already loaded? (to prevent duplicate JS injection) 4890 location_selector_loaded = s3.gis.location_selector_loaded 4891 4892 # Action labels i18n 4893 if not location_selector_loaded: 4894 global_append = s3.js_global.append 4895 global_append('''i18n.select="%s"''' % T("Select")) 4896 if multiselect == "search": 4897 global_append('''i18n.search="%s"''' % T("Search")) 4898 if latlon_labels: 4899 global_append('''i18n.latlon_mode=''' 4900 '''{decimal:"%(decimal)s",dms:"%(dms)s"}''' % 4901 latlon_labels) 4902 global_append('''i18n.latlon_error=''' 4903 '''{lat:"%s",lon:"%s",min:"%s",sec:"%s",format:"%s"}''' % 4904 (T("Latitude must be -90..90"), 4905 T("Longitude must be -180..180"), 4906 T("Minutes must be 0..59"), 4907 T("Seconds must be 0..59"), 4908 T("Unrecognized format"), 4909 )) 4910 4911 # If we need to show the map since we have an existing lat/lon/wkt 4912 # then we need to launch the client-side JS as a callback to the 4913 # MapJS loader 4914 wkt = values.get("wkt") 4915 radius = values.get("radius") 4916 if lat is not None or lon is not None or wkt is not None: 4917 use_callback = True 4918 else: 4919 use_callback = False 4920 4921 # Widget JS options 4922 options = {"hideLx": self.hide_lx, 4923 "reverseLx": self.reverse_lx, 4924 "locations": location_dict, 4925 "labels": labels_compact, 4926 "showLabels": self.labels, 4927 "featureRequired": self.feature_required, 4928 "latlonMode": self.latlon_mode, 4929 "latlonModeToggle": self.latlon_mode_toggle, 4930 } 4931 if self.min_bbox: 4932 options["minBBOX"] = self.min_bbox 4933 if self.open_map_on_load: 4934 options["openMapOnLoad"] = True 4935 script = '''$('#%s').locationselector(%s)''' % \ 4936 (fieldname, json.dumps(options, separators=SEPARATORS)) 4937 4938 show_map = self.show_map 4939 callback = None 4940 if show_map and use_callback: 4941 callback = script 4942 elif not location_selector_loaded or \ 4943 not location_selector_loaded.get(fieldname): 4944 s3.jquery_ready.append(script) 4945 4946 # Inject LocationSelector JS 4947 if s3.debug: 4948 script = "s3.ui.locationselector.js" 4949 else: 4950 script = "s3.ui.locationselector.min.js" 4951 script = "/%s/static/scripts/S3/%s" % (request.application, script) 4952 4953 scripts = s3.scripts 4954 if script not in scripts: 4955 scripts.append(script) 4956 4957 # Should we use the Geocoder? 4958 geocoder = config.geocoder and show_address 4959 4960 # Inject map 4961 if show_map: 4962 map_icon = self._map(field, 4963 fieldname, 4964 lat, 4965 lon, 4966 wkt, 4967 radius, 4968 callback = callback, 4969 geocoder = geocoder, 4970 tablename = field.tablename, 4971 ) 4972 else: 4973 map_icon = None 4974 4975 # LocationSelector is now loaded! (=prevent duplicate JS injection) 4976 if location_selector_loaded: 4977 location_selector_loaded[fieldname] = True 4978 else: 4979 s3.gis.location_selector_loaded = {fieldname: True} 4980 4981 # Real input 4982 classes = ["location-selector"] 4983 if fieldname.startswith("sub_"): 4984 is_inline = True 4985 classes.append("inline-locationselector-widget") 4986 else: 4987 is_inline = False 4988 real_input = self.inputfield(field, values, classes, **attributes) 4989 4990 # The overall layout of the components 4991 visible_components = self._layout(components, 4992 map_icon=map_icon, 4993 inline=is_inline, 4994 ) 4995 4996 return TAG[""](DIV(_class="throbber"), 4997 real_input, 4998 visible_components, 4999 )
5000 5001 # -------------------------------------------------------------------------
5002 - def _labels(self, levels, country=None):
5003 """ 5004 Extract the hierarchy labels 5005 5006 @param levels: the exposed hierarchy levels 5007 @param country: the country (gis_location record ID) for which 5008 to read the hierarchy labels 5009 5010 @return: tuple (labels, compact) where labels is for 5011 internal use with _lx_selectors, and compact 5012 the version ready for JSON output 5013 5014 @ToDo: Country-specific Translations of Labels 5015 """ 5016 5017 T = current.T 5018 table = current.s3db.gis_hierarchy 5019 5020 fields = [table[level] for level in levels if level != "L0"] 5021 5022 query = (table.uuid == "SITE_DEFAULT") 5023 if country: 5024 # Read both country-specific and default 5025 fields.append(table.uuid) 5026 query |= (table.location_id == country) 5027 limit = 2 5028 else: 5029 # Default only 5030 limit = 1 5031 5032 rows = current.db(query).select(*fields, limitby=(0, limit)) 5033 5034 labels = {} 5035 compact = {} 5036 5037 if "L0" in levels: 5038 labels["L0"] = current.messages.COUNTRY 5039 5040 if country: 5041 for row in rows: 5042 if row.uuid == "SITE_DEFAULT": 5043 d = compact["d"] = {} 5044 for level in levels: 5045 if level == "L0": 5046 continue 5047 d[int(level[1:])] = row[level] 5048 else: 5049 d = compact[country] = {} 5050 for level in levels: 5051 if level == "L0": 5052 continue 5053 labels[level] = d[int(level[1:])] = s3_unicode(T(row[level])) 5054 else: 5055 row = rows.first() 5056 d = compact["d"] = {} 5057 for level in levels: 5058 if level == "L0": 5059 continue 5060 d[int(level[1:])] = s3_unicode(T(row[level])) 5061 5062 return labels, compact
5063 5064 # -------------------------------------------------------------------------
5065 - def _locations(self, 5066 levels, 5067 values, 5068 default_bounds = None, 5069 lowest_lx = None, 5070 config = None):
5071 """ 5072 Build initial location dict (to populate Lx dropdowns) 5073 5074 @param levels: the exposed levels 5075 @param values: the current values 5076 @param default_bounds: the default bounds (if already known, e.g. 5077 single-country deployment) 5078 @param lowest_lx: the lowest un-selectable Lx level (to determine 5079 default bounds if not passed in) 5080 @param config: the current GIS config 5081 5082 @return: dict of location data, ready for JSON output 5083 5084 @ToDo: DRY with controllers/gis.py ldata() 5085 """ 5086 5087 db = current.db 5088 s3db = current.s3db 5089 settings = current.deployment_settings 5090 5091 L0 = values.get("L0") 5092 L1 = values.get("L1") 5093 L2 = values.get("L2") 5094 L3 = values.get("L3") 5095 L4 = values.get("L4") 5096 #L5 = values.get("L5") 5097 5098 # Read all visible levels 5099 # NB (level != None) is to handle Missing Levels 5100 gtable = s3db.gis_location 5101 5102 # @todo: DRY this: 5103 if "L0" in levels: 5104 query = (gtable.level == "L0") 5105 countries = settings.get_gis_countries() 5106 if len(countries): 5107 ttable = s3db.gis_location_tag 5108 query &= ((ttable.tag == "ISO2") & \ 5109 (ttable.value.belongs(countries)) & \ 5110 (ttable.location_id == gtable.id)) 5111 if L0 and "L1" in levels: 5112 query |= (gtable.level != None) & \ 5113 (gtable.parent == L0) 5114 if L1 and "L2" in levels: 5115 query |= (gtable.level != None) & \ 5116 (gtable.parent == L1) 5117 if L2 and "L3" in levels: 5118 query |= (gtable.level != None) & \ 5119 (gtable.parent == L2) 5120 if L3 and "L4" in levels: 5121 query |= (gtable.level != None) & \ 5122 (gtable.parent == L3) 5123 if L4 and "L5" in levels: 5124 query |= (gtable.level != None) & \ 5125 (gtable.parent == L4) 5126 elif L0 and "L1" in levels: 5127 query = (gtable.level != None) & \ 5128 (gtable.parent == L0) 5129 if L1 and "L2" in levels: 5130 query |= (gtable.level != None) & \ 5131 (gtable.parent == L1) 5132 if L2 and "L3" in levels: 5133 query |= (gtable.level != None) & \ 5134 (gtable.parent == L2) 5135 if L3 and "L4" in levels: 5136 query |= (gtable.level != None) & \ 5137 (gtable.parent == L3) 5138 if L4 and "L5" in levels: 5139 query |= (gtable.level != None) & \ 5140 (gtable.parent == L4) 5141 elif L1 and "L2" in levels: 5142 query = (gtable.level != None) & \ 5143 (gtable.parent == L1) 5144 if L2 and "L3" in levels: 5145 query |= (gtable.level != None) & \ 5146 (gtable.parent == L2) 5147 if L3 and "L4" in levels: 5148 query |= (gtable.level != None) & \ 5149 (gtable.parent == L3) 5150 if L4 and "L5" in levels: 5151 query |= (gtable.level != None) & \ 5152 (gtable.parent == L4) 5153 elif L2 and "L3" in levels: 5154 query = (gtable.level != None) & \ 5155 (gtable.parent == L2) 5156 if L3 and "L4" in levels: 5157 query |= (gtable.level != None) & \ 5158 (gtable.parent == L3) 5159 if L4 and "L5" in levels: 5160 query |= (gtable.level != None) & \ 5161 (gtable.parent == L4) 5162 elif L3 and "L4" in levels: 5163 query = (gtable.level != None) & \ 5164 (gtable.parent == L3) 5165 if L4 and "L5" in levels: 5166 query |= (gtable.level != None) & \ 5167 (gtable.parent == L4) 5168 elif L4 and "L5" in levels: 5169 query = (gtable.level != None) & \ 5170 (gtable.parent == L4) 5171 else: 5172 query = None 5173 5174 # Translate options using gis_location_name? 5175 language = current.session.s3.language 5176 if language in ("en", "en-gb"): 5177 # We assume that Location names default to the English version 5178 translate = False 5179 else: 5180 translate = settings.get_L10n_translate_gis_location() 5181 5182 if query is None: 5183 locations = [] 5184 if levels != []: 5185 # Misconfigured (e.g. no default for a hidden Lx level) 5186 current.log.warning("S3LocationSelector: no default for hidden Lx level?") 5187 else: 5188 query &= (gtable.deleted == False) & \ 5189 (gtable.end_date == None) 5190 fields = [gtable.id, 5191 gtable.name, 5192 gtable.level, 5193 gtable.parent, 5194 gtable.inherited, 5195 gtable.lat_min, 5196 gtable.lon_min, 5197 gtable.lat_max, 5198 gtable.lon_max, 5199 ] 5200 5201 if translate: 5202 ntable = s3db.gis_location_name 5203 fields.append(ntable.name_l10n) 5204 left = ntable.on((ntable.deleted == False) & \ 5205 (ntable.language == language) & \ 5206 (ntable.location_id == gtable.id)) 5207 else: 5208 left = None 5209 locations = db(query).select(*fields, left=left) 5210 5211 location_dict = {} 5212 if default_bounds: 5213 5214 # Only L0s get set before here 5215 location_dict["d"] = dict(id=L0, b=default_bounds) 5216 location_dict[L0] = dict(b=default_bounds, l=0) 5217 5218 elif lowest_lx: 5219 # What is the lowest-level un-selectable Lx? 5220 lx = values.get(lowest_lx) 5221 record = db(gtable.id == lx).select(gtable.lat_min, 5222 gtable.lon_min, 5223 gtable.lat_max, 5224 gtable.lon_max, 5225 cache=s3db.cache, 5226 limitby=(0, 1)).first() 5227 try: 5228 bounds = [record.lon_min, 5229 record.lat_min, 5230 record.lon_max, 5231 record.lat_max 5232 ] 5233 except: 5234 # Record not found! 5235 raise ValueError 5236 5237 location_dict["d"] = dict(id=lx, b=bounds) 5238 location_dict[lx] = dict(b=bounds, l=int(lowest_lx[1:])) 5239 else: 5240 fallback = None 5241 default_location = config.default_location_id 5242 if default_location: 5243 query = (gtable.id == default_location) 5244 record = db(query).select(gtable.level, 5245 gtable.lat_min, 5246 gtable.lon_min, 5247 gtable.lat_max, 5248 gtable.lon_max, 5249 cache=s3db.cache, 5250 limitby=(0, 1)).first() 5251 if record and record.level: 5252 bounds = [record.lon_min, 5253 record.lat_min, 5254 record.lon_max, 5255 record.lat_max, 5256 ] 5257 if any(bounds): 5258 fallback = {"id": default_location, "b": bounds} 5259 if fallback is None: 5260 fallback = {"b": [config.lon_min, 5261 config.lat_min, 5262 config.lon_max, 5263 config.lat_max, 5264 ] 5265 } 5266 location_dict["d"] = fallback 5267 5268 if translate: 5269 for location in locations: 5270 l = location["gis_location"] 5271 name = location["gis_location_name.name_l10n"] or l.name 5272 data = dict(n=name, 5273 l=int(l.level[1]), 5274 ) 5275 if l.parent: 5276 data["f"] = int(l.parent) 5277 if not l.inherited: 5278 data["b"] = [l.lon_min, 5279 l.lat_min, 5280 l.lon_max, 5281 l.lat_max, 5282 ] 5283 location_dict[int(l.id)] = data 5284 else: 5285 for l in locations: 5286 level = l.level 5287 if level: 5288 level = int(level[1]) 5289 else: 5290 current.log.warning("S3LocationSelector", 5291 "Location Hierarchy not setup properly") 5292 continue 5293 data = dict(n=l.name, 5294 l=level) 5295 if l.parent: 5296 data["f"] = int(l.parent) 5297 if not l.inherited: 5298 data["b"] = [l.lon_min, 5299 l.lat_min, 5300 l.lon_max, 5301 l.lat_max, 5302 ] 5303 location_dict[int(l.id)] = data 5304 5305 return location_dict
5306 5307 # -------------------------------------------------------------------------
5308 - def _layout(self, 5309 components, 5310 map_icon=None, 5311 formstyle=None, 5312 inline=False):
5313 """ 5314 Overall layout for visible components 5315 5316 @param components: the components as dict 5317 {name: (label, widget, id, hidden)} 5318 @param map icon: the map icon 5319 @param formstyle: the formstyle (falls back to CRUD formstyle) 5320 """ 5321 5322 if formstyle is None: 5323 formstyle = current.response.s3.crud.formstyle 5324 5325 # Test the formstyle 5326 row = formstyle("test", "test", "test", "test") 5327 if isinstance(row, tuple): 5328 # Formstyle with separate row for label 5329 # (e.g. old default Eden formstyle) 5330 tuple_rows = True 5331 table_style = inline and row[0].tag == "tr" 5332 else: 5333 # Formstyle with just a single row 5334 # (e.g. Bootstrap, Foundation or DRRPP) 5335 tuple_rows = False 5336 table_style = False 5337 5338 selectors = DIV() if not table_style else TABLE() 5339 for name in ("L0", "L1", "L2", "L3", "L4", "L5"): 5340 if name in components: 5341 label, widget, input_id, hidden = components[name] 5342 formrow = formstyle("%s__row" % input_id, 5343 label, 5344 widget, 5345 "", 5346 hidden=hidden, 5347 ) 5348 if tuple_rows: 5349 selectors.append(formrow[0]) 5350 selectors.append(formrow[1]) 5351 else: 5352 selectors.append(formrow) 5353 5354 inputs = TAG[""]() if not table_style else TABLE() 5355 for name in ("address", "postcode", "lat", "lon", "latlon_toggle"): 5356 if name in components: 5357 label, widget, input_id, hidden = components[name] 5358 formrow = formstyle("%s__row" % input_id, 5359 label, 5360 widget, 5361 "", 5362 hidden=hidden, 5363 ) 5364 if tuple_rows: 5365 inputs.append(formrow[0]) 5366 inputs.append(formrow[1]) 5367 else: 5368 inputs.append(formrow) 5369 5370 output = TAG[""](selectors, inputs) 5371 if map_icon: 5372 output.append(map_icon) 5373 return output
5374 5375 # -------------------------------------------------------------------------
5376 - def _lx_selectors(self, 5377 field, 5378 fieldname, 5379 levels, 5380 labels, 5381 multiselect=False, 5382 required=False):
5383 """ 5384 Render the Lx-dropdowns 5385 5386 @param field: the field (to construct the HTML Names) 5387 @param fieldname: the fieldname (to construct the HTML IDs) 5388 @param levels: tuple of levels in order, like ("L0", "L1", ...) 5389 @param labels: the labels for the hierarchy levels as dict {level:label} 5390 @param multiselect: Use multiselect-dropdowns (specify "search" to 5391 make the dropdowns searchable) 5392 @param required: whether selection is required 5393 5394 @return: a dict of components 5395 {name: (label, widget, id, hidden)} 5396 """ 5397 5398 # Use multiselect widget? 5399 if multiselect == "search": 5400 _class = "lx-select multiselect search" 5401 elif multiselect: 5402 _class = "lx-select multiselect" 5403 else: 5404 _class = None 5405 5406 # Initialize output 5407 selectors = {} 5408 5409 # 1st level is always hidden until populated 5410 hidden = True 5411 5412 _fieldname = fieldname.split("%s_" % field.tablename)[1] 5413 5414 #T = current.T 5415 required_levels = self.required_levels 5416 for level in levels: 5417 5418 _name = "%s_%s" % (_fieldname, level) 5419 5420 _id = "%s_%s" % (fieldname, level) 5421 5422 label = labels.get(level, level) 5423 5424 # Widget (options to be populated client-side) 5425 #placeholder = T("Select %(level)s") % {"level": label} 5426 placeholder = "" 5427 widget = SELECT(OPTION(placeholder, _value=""), 5428 _name = _name, 5429 _id = _id, 5430 _class = _class, 5431 ) 5432 5433 # Mark as required? 5434 if required or level in required_levels: 5435 widget.add_class("required") 5436 label = s3_required_label(label) 5437 5438 if required and ("L%s" % (int(level[1:]) - 1)) not in levels: 5439 # This is the highest level, treat subsequent levels 5440 # as optional unless they are explicitly configured 5441 # as required 5442 required = False 5443 5444 # Throbber 5445 throbber = DIV(_id="%s__throbber" % _id, 5446 _class="throbber hide", 5447 ) 5448 5449 if self.labels: 5450 label = LABEL(label, _for=_id) 5451 else: 5452 label = "" 5453 selectors[level] = (label, TAG[""](widget, throbber), _id, hidden) 5454 5455 # Follow hide-setting for all subsequent levels (default: True), 5456 # client-side JS will open when-needed 5457 hidden = self.hide_lx 5458 5459 return selectors
5460 5461 # -------------------------------------------------------------------------
5462 - def _input(self, 5463 fieldname, 5464 name, 5465 value, 5466 label, 5467 hidden=False, 5468 _class="string"):
5469 """ 5470 Render a text input (e.g. address or postcode field) 5471 5472 @param fieldname: the field name (for ID construction) 5473 @param name: the name for the input field 5474 @param value: the initial value for the input 5475 @param label: the label for the input 5476 @param hidden: render hidden 5477 5478 @return: a tuple (label, widget, id, hidden) 5479 """ 5480 5481 input_id = "%s_%s" % (fieldname, name) 5482 5483 if label and self.labels: 5484 _label = LABEL("%s:" % label, _for=input_id) 5485 else: 5486 _label = "" 5487 if label and self.placeholders: 5488 _placeholder = label 5489 else: 5490 _placeholder = None 5491 if isinstance(value, unicode): 5492 value = value.encode("utf-8") 5493 widget = INPUT(_name=name, 5494 _id=input_id, 5495 _class=_class, 5496 _placeholder=_placeholder, 5497 value=value, 5498 ) 5499 5500 return (_label, widget, input_id, hidden)
5501 5502 # -------------------------------------------------------------------------
5503 - def _map(self, 5504 field, 5505 fieldname, 5506 lat, 5507 lon, 5508 wkt, 5509 radius, 5510 callback = None, 5511 geocoder = False, 5512 tablename = None):
5513 """ 5514 Initialize the map 5515 5516 @param field: the field 5517 @param fieldname: the field name (to construct HTML IDs) 5518 @param lat: the Latitude of the current point location 5519 @param lon: the Longitude of the current point location 5520 @param wkt: the WKT 5521 @param radius: the radius of the location 5522 @param callback: the script to initialize the widget, if to be 5523 initialized as callback of the MapJS loader 5524 @param geocoder: use a geocoder 5525 @param tablename: tablename to determine the controller/function 5526 for custom colorpicker style 5527 5528 @return: the HTML components for the map (including the map icon row) 5529 5530 @ToDo: handle multiple LocationSelectors in 1 page 5531 (=> multiple callbacks, as well as the need to 5532 migrate options from globals to a parameter) 5533 """ 5534 5535 lines = self.lines 5536 points = self.points 5537 polygons = self.polygons 5538 circles = self.circles 5539 5540 # Toolbar options 5541 add_points_active = add_polygon_active = add_line_active = add_circle_active = False 5542 if points and lines: 5543 # Allow selection between drawing a point or a line 5544 toolbar = True 5545 if wkt: 5546 if not polygons or wkt.startswith("LINE"): 5547 add_line_active = True 5548 elif polygons: 5549 add_polygon_active = True 5550 else: 5551 add_line_active = True 5552 else: 5553 add_points_active = True 5554 elif points and polygons: 5555 # Allow selection between drawing a point or a polygon 5556 toolbar = True 5557 if wkt: 5558 add_polygon_active = True 5559 else: 5560 add_points_active = True 5561 elif points and circles: 5562 # Allow selection between drawing a point or a circle 5563 toolbar = True 5564 if wkt: 5565 add_circle_active = True 5566 else: 5567 add_points_active = True 5568 elif points: 5569 # No toolbar needed => always drawing points 5570 toolbar = False 5571 add_points_active = True 5572 elif lines and polygons: 5573 # Allow selection between drawing a line or a polygon 5574 toolbar = True 5575 if wkt: 5576 if wkt.startswith("LINE"): 5577 add_line_active = True 5578 else: 5579 add_polygon_active = True 5580 else: 5581 add_polygon_active = True 5582 elif lines and circles: 5583 # Allow selection between drawing a line or a circle 5584 toolbar = True 5585 if wkt: 5586 if wkt.startswith("LINE"): 5587 add_line_active = True 5588 else: 5589 add_circle_active = True 5590 else: 5591 add_circle_active = True 5592 elif lines: 5593 # No toolbar needed => always drawing lines 5594 toolbar = False 5595 add_line_active = True 5596 elif polygons and circles: 5597 # Allow selection between drawing a polygon or a circle 5598 toolbar = True 5599 if wkt: 5600 if radius is not None: 5601 add_circle_active = True 5602 else: 5603 add_polygon_active = True 5604 else: 5605 add_polygon_active = True 5606 elif polygons: 5607 # No toolbar needed => always drawing polygons 5608 toolbar = False 5609 add_polygon_active = True 5610 elif circles: 5611 # No toolbar needed => always drawing circles 5612 toolbar = False 5613 add_circle_active = True 5614 else: 5615 # No Valid options! 5616 raise SyntaxError 5617 5618 s3 = current.response.s3 5619 5620 # ColorPicker options 5621 color_picker = self.color_picker 5622 if color_picker: 5623 toolbar = True 5624 # Requires the custom controller to store this before calling the widget 5625 # - a bit hacky, but can't think of a better option currently without 5626 # rewriting completely as an S3SQLSubForm 5627 record_id = s3.record_id 5628 if not record_id: 5629 # Show Color Picker with default Style 5630 color_picker = True 5631 else: 5632 # Do we have a style defined for this record? 5633 # @ToDo: Support Layers using alternate controllers/functions 5634 db = current.db 5635 s3db = current.s3db 5636 c, f = field.tablename.split("_", 1) 5637 ftable = s3db.gis_layer_feature 5638 query = (ftable.deleted == False) & \ 5639 (ftable.controller == c) & \ 5640 (ftable.function == f) & \ 5641 (ftable.individual == True) 5642 rows = db(query).select(ftable.layer_id) 5643 if not rows: 5644 # Show Color Picker with default Style 5645 color_picker = True 5646 else: 5647 # @ToDo: Handle multiple rows? 5648 layer_id = rows.first().layer_id 5649 stable = s3db.gis_style 5650 query = (stable.deleted == False) & \ 5651 (stable.layer_id == layer_id) & \ 5652 (stable.record_id == record_id) 5653 rows = db(query).select(stable.style) 5654 row = rows.first() 5655 if row: 5656 color_picker = row.style 5657 else: 5658 # Show Color Picker with default Style 5659 color_picker = True 5660 else: 5661 color_picker = False 5662 5663 settings = current.deployment_settings 5664 5665 # Create the map 5666 _map = current.gis.show_map(id = "location_selector_%s" % fieldname, 5667 collapsed = True, 5668 height = settings.get_gis_map_selector_height(), 5669 width = settings.get_gis_map_selector_width(), 5670 add_feature = points, 5671 add_feature_active = add_points_active, 5672 add_line = lines, 5673 add_line_active = add_line_active, 5674 add_polygon = polygons, 5675 add_polygon_active = add_polygon_active, 5676 add_circle = circles, 5677 add_circle_active = add_circle_active, 5678 catalogue_layers = self.catalog_layers, 5679 color_picker = color_picker, 5680 toolbar = toolbar, 5681 # Hide controls from toolbar 5682 clear_layers = False, 5683 nav = False, 5684 print_control = False, 5685 area = False, 5686 zoomWheelEnabled = False, 5687 # Don't use normal callback (since we postpone rendering Map until DIV unhidden) 5688 # but use our one if we need to display a map by default 5689 callback = callback, 5690 ) 5691 5692 # Inject map icon labels 5693 if polygons or lines: 5694 show_map_add = settings.get_ui_label_locationselector_map_polygon_add() 5695 show_map_view = settings.get_ui_label_locationselector_map_polygon_view() 5696 if wkt is not None: 5697 label = show_map_view 5698 else: 5699 label = show_map_add 5700 else: 5701 show_map_add = settings.get_ui_label_locationselector_map_point_add() 5702 show_map_view = settings.get_ui_label_locationselector_map_point_view() 5703 if lat is not None or lon is not None: 5704 label = show_map_view 5705 else: 5706 label = show_map_add 5707 5708 T = current.T 5709 global_append = s3.js_global.append 5710 location_selector_loaded = s3.gis.location_selector_loaded 5711 5712 if not location_selector_loaded: 5713 global_append('''i18n.show_map_add="%s" 5714 i18n.show_map_view="%s" 5715 i18n.hide_map="%s" 5716 i18n.map_feature_required="%s"''' % (show_map_add, 5717 show_map_view, 5718 T("Hide Map"), 5719 T("Map Input Required"), 5720 )) 5721 5722 # Generate map icon 5723 icon_id = "%s_map_icon" % fieldname 5724 row_id = "%s_map_icon__row" % fieldname 5725 _formstyle = settings.ui.formstyle 5726 if not _formstyle or \ 5727 isinstance(_formstyle, basestring) and "foundation" in _formstyle: 5728 # Default: Foundation 5729 # Need to add custom classes to core HTML markup 5730 map_icon = DIV(DIV(BUTTON(ICON("globe"), 5731 SPAN(label), 5732 _type="button", # defaults to 'submit' otherwise! 5733 _id=icon_id, 5734 _class="btn tiny button gis_loc_select_btn", 5735 ), 5736 _class="small-12 columns", 5737 ), 5738 _id = row_id, 5739 _class = "form-row row hide", 5740 ) 5741 elif _formstyle == "bootstrap": 5742 # Need to add custom classes to core HTML markup 5743 map_icon = DIV(DIV(BUTTON(ICON("icon-map"), 5744 SPAN(label), 5745 _type="button", # defaults to 'submit' otherwise! 5746 _id=icon_id, 5747 _class="btn gis_loc_select_btn", 5748 ), 5749 _class="controls", 5750 ), 5751 _id = row_id, 5752 _class = "control-group hide", 5753 ) 5754 else: 5755 # Old default 5756 map_icon = DIV(DIV(BUTTON(ICON("globe"), 5757 SPAN(label), 5758 _type="button", # defaults to 'submit' otherwise! 5759 _id=icon_id, 5760 _class="btn gis_loc_select_btn", 5761 ), 5762 _class="w2p_fl", 5763 ), 5764 _id = row_id, 5765 _class = "hide", 5766 ) 5767 5768 # Geocoder? 5769 if geocoder: 5770 5771 if not location_selector_loaded: 5772 global_append('''i18n.address_mapped="%s" 5773 i18n.address_not_mapped="%s" 5774 i18n.location_found="%s" 5775 i18n.location_not_found="%s"''' % (T("Address Mapped"), 5776 T("Address NOT Mapped"), 5777 T("Address Found"), 5778 T("Address NOT Found"), 5779 )) 5780 5781 map_icon.append(DIV(DIV(_class="throbber hide"), 5782 DIV(_class="geocode_success hide"), 5783 DIV(_class="geocode_fail hide"), 5784 BUTTON(T("Geocode"), 5785 _type="button", # defaults to 'submit' otherwise! 5786 _class="hide", 5787 ), 5788 _id="%s_geocode" % fieldname, 5789 _class="controls geocode", 5790 )) 5791 5792 # Inject map directly behind map icon 5793 map_icon.append(_map) 5794 5795 return map_icon
5796 5797 # -------------------------------------------------------------------------
5798 - def extract(self, record_id, values=None):
5799 """ 5800 Load record data from database and update the values dict 5801 5802 @param record_id: the location record ID 5803 @param values: the values dict 5804 """ 5805 5806 # Initialize the values dict 5807 if values is None: 5808 values = {} 5809 for key in ("L0", "L1", "L2", "L3", "L4", "L5", "specific", "parent", "radius"): 5810 if key not in values: 5811 values[key] = None 5812 5813 values["id"] = record_id 5814 5815 if not record_id: 5816 return values 5817 5818 db = current.db 5819 table = current.s3db.gis_location 5820 5821 levels = self.load_levels 5822 5823 lat = values.get("lat") 5824 lon = values.get("lon") 5825 wkt = values.get("wkt") 5826 radius = values.get("radius") 5827 address = values.get("address") 5828 postcode = values.get("postcode") 5829 5830 # Load the record 5831 record = db(table.id == record_id).select(table.id, 5832 table.path, 5833 table.parent, 5834 table.level, 5835 table.gis_feature_type, 5836 table.inherited, 5837 table.lat, 5838 table.lon, 5839 table.wkt, 5840 table.radius, 5841 table.addr_street, 5842 table.addr_postcode, 5843 limitby=(0, 1)).first() 5844 if not record: 5845 raise ValueError 5846 5847 level = record.level 5848 5849 # Parse the path 5850 path = record.path 5851 if path is None: 5852 # Not updated yet? => do it now 5853 try: 5854 path = current.gis.update_location_tree({"id": record_id}) 5855 except ValueError: 5856 pass 5857 path = [] if path is None else path.split("/") 5858 5859 path_ok = True 5860 if level: 5861 # Lx location 5862 values["level"] = level 5863 values["specific"] = None 5864 5865 if len(path) != (int(level[1:]) + 1): 5866 # We don't have a full path 5867 path_ok = False 5868 5869 else: 5870 # Specific location 5871 values["parent"] = record.parent 5872 values["specific"] = record.id 5873 5874 if len(path) < (len(levels) + 1): 5875 # We don't have a full path 5876 path_ok = False 5877 5878 # Only use a specific Lat/Lon when they are not inherited 5879 if not record.inherited: 5880 if self.points: 5881 if lat is None or lat == "": 5882 if record.gis_feature_type == 1: 5883 # Only use Lat for Points 5884 lat = record.lat 5885 else: 5886 lat = None 5887 if lon is None or lon == "": 5888 if record.gis_feature_type == 1: 5889 # Only use Lat for Points 5890 lon = record.lon 5891 else: 5892 lon = None 5893 else: 5894 lat = None 5895 lon = None 5896 if self.lines or self.polygons or self.circles: 5897 if not wkt: 5898 if record.gis_feature_type != 1: 5899 # Only use WKT for non-Points 5900 wkt = record.wkt 5901 if record.radius is not None: 5902 radius = record.radius 5903 else: 5904 wkt = None 5905 else: 5906 wkt = None 5907 if address is None: 5908 address = record.addr_street 5909 if postcode is None: 5910 postcode = record.addr_postcode 5911 5912 # Path 5913 if path_ok: 5914 for level in levels: 5915 idx = int(level[1:]) 5916 if len(path) > idx: 5917 values[level] = int(path[idx]) 5918 else: 5919 # Retrieve all records in the path to match them up to their Lx 5920 rows = db(table.id.belongs(path)).select(table.id, table.level) 5921 for row in rows: 5922 if row.level: 5923 values[row.level] = row.id 5924 5925 # Address data 5926 values["address"] = address 5927 values["postcode"] = postcode 5928 5929 # Lat/Lon/WKT/Radius 5930 values["lat"] = lat 5931 values["lon"] = lon 5932 values["wkt"] = wkt 5933 values["radius"] = radius 5934 5935 return values
5936 5937 # -------------------------------------------------------------------------
5938 - def represent(self, value):
5939 """ 5940 Representation of a new/updated location row (before DB commit). 5941 5942 NB: Using a fake path here in order to prevent 5943 gis_LocationRepresent.represent_row() from running 5944 update_location_tree as that would change DB status which 5945 is an invalid action at this point (row not committed yet). 5946 5947 This method is called during S3CRUD.validate for inline components 5948 5949 @param values: the values dict 5950 5951 @return: string representation for the values dict 5952 """ 5953 5954 if not value or not any(value.get(key) for key in self.keys): 5955 # No data 5956 return current.messages["NONE"] 5957 5958 lat = value.get("lat") 5959 lon = value.get("lon") 5960 wkt = value.get("wkt") 5961 #radius = value.get("radius") 5962 address = value.get("address") 5963 postcode = value.get("postcode") 5964 5965 record = Storage(name = value.get("name"), 5966 lat = lat, 5967 lon = lon, 5968 addr_street = address, 5969 addr_postcode = postcode, 5970 parent = value.get("parent"), 5971 ) 5972 5973 # Is this a specific location? 5974 specific = value.get("specific") 5975 if specific: 5976 record_id = specific 5977 elif address or postcode or lat or lon or wkt: 5978 specific = True 5979 record_id = value.get("id") 5980 else: 5981 record_id = None 5982 if not record_id: 5983 record_id = 0 5984 record.id = record_id 5985 5986 lx_ids = {} 5987 5988 # Construct the path (must have a path to prevent update_location_tree) 5989 path = [str(record_id)] 5990 level = None 5991 append = None 5992 for l in xrange(5, -1, -1): 5993 lx = value.get("L%s" % l) 5994 if lx: 5995 if not level and not specific and l < 5: 5996 level = l 5997 elif level and not record.parent: 5998 record.parent = lx 5999 lx_ids[l] = lx 6000 if append is None: 6001 append = path.append 6002 if append: 6003 append(str(lx)) 6004 path.reverse() 6005 record.path = "/".join(path) 6006 6007 # Determine the Lx level 6008 if specific or level is None: 6009 record.level = None 6010 else: 6011 record.level = "L%s" % level 6012 6013 # Get the Lx names 6014 s3db = current.s3db 6015 ltable = s3db.gis_location 6016 6017 if lx_ids: 6018 query = ltable.id.belongs(lx_ids.values()) 6019 limitby = (0, len(lx_ids)) 6020 lx_names = current.db(query).select(ltable.id, 6021 ltable.name, 6022 limitby=limitby).as_dict() 6023 for l in xrange(0, 6): 6024 if l in lx_ids: 6025 lx_name = lx_names.get(lx_ids[l])["name"] 6026 else: 6027 lx_name = None 6028 if not lx_name: 6029 lx_name = "" 6030 record["L%s" % l] = lx_name 6031 if level == l: 6032 record["name"] = lx_name 6033 6034 # Call standard location represent 6035 represent = self._represent 6036 if represent is None: 6037 # Fall back to default 6038 represent = s3db.gis_location_id().represent 6039 6040 if hasattr(represent, "alt_represent_row"): 6041 text = represent.alt_represent_row(record) 6042 else: 6043 text = represent(record) 6044 6045 return s3_str(text)
6046 6047 # -------------------------------------------------------------------------
6048 - def validate(self, value, requires=None):
6049 """ 6050 Parse and validate the input value, but don't create or update 6051 any location data 6052 6053 @param value: the value from the form 6054 @param requires: the field validator 6055 @returns: tuple (values, error) with values being the parsed 6056 value dict, and error any validation errors 6057 """ 6058 6059 values = self.parse(value) 6060 6061 if not values or not any(values.get(key) for key in self.keys): 6062 # No data 6063 if requires and not isinstance(requires, IS_EMPTY_OR): 6064 return values, current.T("Location data required") 6065 return values, None 6066 6067 table = current.s3db.gis_location 6068 errors = {} 6069 feature = None 6070 onvalidation = None 6071 6072 msg = self.error_message 6073 6074 # Check for valid Lat/Lon/WKT/Radius (if any) 6075 lat = values.get("lat") 6076 if lat: 6077 try: 6078 lat = float(lat) 6079 except ValueError: 6080 errors["lat"] = current.T("Latitude is Invalid!") 6081 elif lat == "": 6082 lat = None 6083 6084 lon = values.get("lon") 6085 if lon: 6086 try: 6087 lon = float(lon) 6088 except ValueError: 6089 errors["lon"] = current.T("Longitude is Invalid!") 6090 elif lon == "": 6091 lon = None 6092 6093 wkt = values.get("wkt") 6094 if wkt: 6095 try: 6096 from shapely.wkt import loads as wkt_loads 6097 wkt_loads(wkt) 6098 except: 6099 errors["wkt"] = current.T("WKT is Invalid!") 6100 elif wkt == "": 6101 wkt = None 6102 6103 radius = values.get("radius") 6104 if radius: 6105 try: 6106 radius = float(radius) 6107 except ValueError: 6108 errors["radius"] = current.T("Radius is Invalid!") 6109 elif radius == "": 6110 radius = None 6111 6112 if errors: 6113 error = "\n".join(s3_str(errors[fn]) for fn in errors) 6114 return (values, error) 6115 6116 specific = values.get("specific") 6117 location_id = values.get("id") 6118 6119 if specific and location_id and location_id != specific: 6120 # Reset from a specific location to an Lx 6121 # Currently not possible 6122 # => widget always retains specific 6123 # => must take care of orphaned specific locations otherwise 6124 lat = lon = wkt = radius = None 6125 else: 6126 # Read other details 6127 parent = values.get("parent") 6128 address = values.get("address") 6129 postcode = values.get("postcode") 6130 6131 if parent or address or postcode or \ 6132 wkt is not None or \ 6133 lat is not None or \ 6134 lon is not None or \ 6135 radius is not None: 6136 6137 # Specific location with details 6138 if specific: 6139 values["id"] = specific 6140 6141 # Would-be update => get original record 6142 query = (table.id == specific) & \ 6143 (table.deleted == False) & \ 6144 (table.level == None) # specific Locations only 6145 location = current.db(query).select(table.lat, 6146 table.lon, 6147 table.wkt, 6148 table.addr_street, 6149 table.addr_postcode, 6150 table.parent, 6151 limitby=(0, 1)).first() 6152 if not location: 6153 return (values, msg or current.T("Invalid Location!")) 6154 6155 # Check for changes 6156 changed = False 6157 lparent = location.parent 6158 if parent and lparent: 6159 if int(parent) != int(lparent): 6160 changed = True 6161 elif parent or lparent: 6162 changed = True 6163 if not changed: 6164 laddress = location.addr_street 6165 if (address or laddress) and \ 6166 address != laddress: 6167 changed = True 6168 else: 6169 lpostcode = location.addr_postcode 6170 if (postcode or lpostcode) and \ 6171 postcode != lpostcode: 6172 changed = True 6173 else: 6174 lwkt = location.wkt 6175 if (wkt or lwkt) and \ 6176 wkt != lwkt: 6177 changed = True 6178 else: 6179 # Float comparisons need care 6180 # - just check the 1st 5 decimal points, as 6181 # that's all we care about 6182 llat = location.lat 6183 if lat is not None and llat is not None: 6184 if round(lat, 5) != round(llat, 5): 6185 changed = True 6186 elif lat is not None or llat is not None: 6187 changed = True 6188 if not changed: 6189 llon = location.lon 6190 if lon is not None and llon is not None: 6191 if round(lon, 5) != round(llon, 5): 6192 changed = True 6193 elif lon is not None or llon is not None: 6194 changed = True 6195 6196 if changed: 6197 # Update specific location (indicated by id=specific) 6198 6199 # Permission to update? 6200 if not current.auth.s3_has_permission("update", table, 6201 record_id=specific): 6202 return (values, current.auth.messages.access_denied) 6203 6204 # Schedule for onvalidation 6205 feature = Storage(addr_street=address, 6206 addr_postcode=postcode, 6207 parent=parent, 6208 ) 6209 if any(detail is not None for detail in (lat, lon, wkt, radius)): 6210 feature.lat = lat 6211 feature.lon = lon 6212 feature.wkt = wkt 6213 feature.radius = radius 6214 feature.inherited = False 6215 onvalidation = current.s3db.gis_location_onvalidation 6216 6217 else: 6218 # No changes => skip (indicated by specific=0) 6219 values["specific"] = 0 6220 6221 else: 6222 # Create new specific location (indicate by id=0) 6223 values["id"] = 0 6224 6225 # Permission to create? 6226 if not current.auth.s3_has_permission("create", table): 6227 return (values, current.auth.messages.access_denied) 6228 6229 # Check for duplicate address 6230 if self.prevent_duplicate_addresses: 6231 query = (table.addr_street == address) & \ 6232 (table.parent == parent) & \ 6233 (table.deleted != True) 6234 duplicate = current.db(query).select(table.id, 6235 limitby=(0, 1) 6236 ).first() 6237 if duplicate: 6238 return (values, current.T("Duplicate Address")) 6239 6240 # Schedule for onvalidation 6241 feature = Storage(addr_street=address, 6242 addr_postcode=postcode, 6243 parent=parent, 6244 inherited=True, 6245 ) 6246 if any(detail is not None for detail in (lat, lon, wkt, radius)): 6247 feature.lat = lat 6248 feature.lon = lon 6249 feature.wkt = wkt 6250 feature.radius = radius 6251 feature.inherited = False 6252 onvalidation = current.s3db.gis_location_onvalidation 6253 6254 elif specific: 6255 # Update specific location (indicated by id=specific) 6256 values["id"] = specific 6257 6258 # Permission to update? 6259 if not current.auth.s3_has_permission("update", table, 6260 record_id=specific): 6261 return (values, current.auth.messages.access_denied) 6262 6263 # Make sure parent/address are properly removed 6264 values["parent"] = None 6265 values["address"] = None 6266 values["postcode"] = None 6267 6268 else: 6269 # Lx location => check level 6270 ## @todo: 6271 #if not location_id: 6272 ## Get lowest selected Lx 6273 6274 if location_id: 6275 levels = self.levels 6276 if levels == []: 6277 # Widget set to levels=False 6278 # No Street Address specified, so skip 6279 return (None, None) 6280 query = (table.id == location_id) & \ 6281 (table.deleted == False) 6282 location = current.db(query).select(table.level, 6283 limitby=(0, 1)).first() 6284 if not location: 6285 return (values, msg or current.T("Invalid Location!")) 6286 6287 level = location.level 6288 if level: 6289 # Accept all levels above and including the lowest selectable level 6290 for i in xrange(5,-1,-1): 6291 if "L%s" % i in levels: 6292 accepted_levels = set("L%s" % l for l in xrange(i,-1,-1)) 6293 break 6294 if level not in accepted_levels: 6295 return (values, msg or \ 6296 current.T("Location is of incorrect level!")) 6297 # Do not update (indicate by specific = None) 6298 values["specific"] = None 6299 6300 if feature and onvalidation: 6301 6302 form = Storage(errors = errors, 6303 vars = feature, 6304 ) 6305 try: 6306 # @todo: should use callback() 6307 onvalidation(form) 6308 except: 6309 if current.response.s3.debug: 6310 raise 6311 else: 6312 error = "onvalidation failed: %s (%s)" % ( 6313 onvalidation, sys.exc_info()[1]) 6314 current.log.error(error) 6315 if form.errors: 6316 errors = form.errors 6317 error = "\n".join(s3_str(errors[fn]) for fn in errors) 6318 return (values, error) 6319 elif feature: 6320 # gis_location_onvalidation adds/updates form vars (e.g. 6321 # gis_feature_type, the_geom) => must also update values 6322 values.update(feature) 6323 6324 # Success 6325 return (values, None)
6326 6327 # -------------------------------------------------------------------------
6328 - def postprocess(self, value):
6329 """ 6330 Takes the JSON from the real input and returns a location ID 6331 for it. Creates or updates the location if necessary. 6332 6333 @param value: the JSON from the real input 6334 @return: tuple (location_id, error) 6335 6336 @ToDo: Audit 6337 """ 6338 6339 # Convert and validate 6340 values, error = self.validate(value) 6341 if values: 6342 location_id = values.get("id") 6343 else: 6344 location_id = None 6345 6346 # Return on validation error 6347 if error: 6348 # Make sure to return None to not override the field values 6349 # @todo: consider a custom INPUT subclass without 6350 # _postprocessing() to prevent _value override 6351 # after successful POST 6352 return None, error 6353 6354 # Skip if location_id is None 6355 if location_id is None: 6356 return location_id, None 6357 6358 db = current.db 6359 table = current.s3db.gis_location 6360 6361 # Read the values 6362 lat = values.get("lat") 6363 lon = values.get("lon") 6364 lat_min = values.get("lat_min") # Values brought in by onvalidation 6365 lon_min = values.get("lon_min") 6366 lat_max = values.get("lat_max") 6367 lon_max = values.get("lon_max") 6368 wkt = values.get("wkt") 6369 radius = values.get("radius") 6370 the_geom = values.get("the_geom") 6371 address = values.get("address") 6372 postcode = values.get("postcode") 6373 parent = values.get("parent") 6374 gis_feature_type = values.get("gis_feature_type") 6375 6376 if location_id == 0: 6377 # Create new location 6378 if wkt is not None or (lat is not None and lon is not None): 6379 inherited = False 6380 else: 6381 inherited = True 6382 6383 feature = Storage(lat=lat, 6384 lon=lon, 6385 lat_min=lat_min, 6386 lon_min=lon_min, 6387 lat_max=lat_max, 6388 lon_max=lon_max, 6389 wkt=wkt, 6390 radius=radius, 6391 inherited=inherited, 6392 addr_street=address, 6393 addr_postcode=postcode, 6394 parent=parent, 6395 ) 6396 6397 # These could have been added during validate: 6398 if gis_feature_type: 6399 feature.gis_feature_type = gis_feature_type 6400 if the_geom: 6401 feature.the_geom = the_geom 6402 6403 location_id = table.insert(**feature) 6404 feature.id = location_id 6405 current.gis.update_location_tree(feature) 6406 6407 else: 6408 specific = values.get("specific") 6409 # specific is 0 to skip update (unchanged) 6410 # specific is None for Lx locations 6411 if specific and specific == location_id: 6412 # Update specific location 6413 feature = Storage(addr_street=address, 6414 addr_postcode=postcode, 6415 parent=parent, 6416 ) 6417 if any(detail is not None for detail in (lat, lon, wkt, radius)): 6418 feature.lat = lat 6419 feature.lon = lon 6420 feature.lat_min = lat_min 6421 feature.lon_min = lon_min 6422 feature.lat_max = lat_max 6423 feature.lon_max = lon_max 6424 feature.wkt = wkt 6425 feature.radius = radius 6426 feature.inherited = False 6427 6428 # These could have been added during validate: 6429 if gis_feature_type: 6430 feature.gis_feature_type = gis_feature_type 6431 if the_geom: 6432 feature.the_geom = the_geom 6433 6434 db(table.id == location_id).update(**feature) 6435 feature.id = location_id 6436 current.gis.update_location_tree(feature) 6437 6438 return location_id, None
6439
6440 # ============================================================================= 6441 -class S3SelectWidget(OptionsWidget):
6442 """ 6443 Standard OptionsWidget, but using the jQuery UI SelectMenu: 6444 http://jqueryui.com/selectmenu/ 6445 6446 Useful for showing Icons against the Options. 6447 """ 6448
6449 - def __init__(self, 6450 icons = False 6451 ):
6452 """ 6453 Constructor 6454 6455 @param icons: show icons next to options, 6456 can be: 6457 - False (don't show icons) 6458 - function (function to call add Icon URLs, height and width to the options) 6459 """ 6460 6461 self.icons = icons
6462
6463 - def __call__(self, field, value, **attr):
6464 6465 if isinstance(field, Field): 6466 selector = str(field).replace(".", "_") 6467 else: 6468 selector = field.name.replace(".", "_") 6469 6470 # Widget 6471 _class = attr.get("_class", None) 6472 if _class: 6473 if "select-widget" not in _class: 6474 attr["_class"] = "%s select-widget" % _class 6475 else: 6476 attr["_class"] = "select-widget" 6477 6478 widget = TAG[""](self.widget(field, value, **attr), 6479 requires = field.requires) 6480 6481 if self.icons: 6482 # Use custom subclass in S3.js 6483 fn = "iconselectmenu().iconselectmenu('menuWidget').addClass('customicons')" 6484 else: 6485 # Use default 6486 fn = "selectmenu()" 6487 script = '''$('#%s').%s''' % (selector, fn) 6488 6489 jquery_ready = current.response.s3.jquery_ready 6490 if script not in jquery_ready: # Prevents loading twice when form has errors 6491 jquery_ready.append(script) 6492 6493 return widget
6494 6495 # -------------------------------------------------------------------------
6496 - def widget(self, field, value, **attributes):
6497 """ 6498 Generates a SELECT tag, including OPTIONs (only 1 option allowed) 6499 see also: `FormWidget.widget` 6500 """ 6501 6502 default = dict(value=value) 6503 attr = self._attributes(field, default, 6504 **attributes) 6505 requires = field.requires 6506 if not isinstance(requires, (list, tuple)): 6507 requires = [requires] 6508 if requires: 6509 if hasattr(requires[0], "options"): 6510 options = requires[0].options() 6511 else: 6512 raise SyntaxError( 6513 "widget cannot determine options of %s" % field) 6514 icons = self.icons 6515 if icons: 6516 # Options including Icons 6517 # Add the Icons to the Options 6518 options = icons(options) 6519 opts = [] 6520 oappend = opts.append 6521 for (k, v, i) in options: 6522 oattr = {"_value": k, 6523 #"_data-class": "select-widget-icon", 6524 } 6525 if i: 6526 oattr["_data-style"] = "background-image:url('%s');height:%spx;width:%spx" % \ 6527 (i[0], i[1], i[2]) 6528 opt = OPTION(v, **oattr) 6529 oappend(opt) 6530 else: 6531 # Standard Options 6532 opts = [OPTION(v, _value=k) for (k, v) in options] 6533 6534 return SELECT(*opts, **attr)
6535
6536 # ============================================================================= 6537 -class S3MultiSelectWidget(MultipleOptionsWidget):
6538 """ 6539 Standard MultipleOptionsWidget, but using the jQuery UI: 6540 http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ 6541 static/scripts/ui/multiselect.js 6542 """ 6543
6544 - def __init__(self, 6545 search = "auto", 6546 header = True, 6547 multiple = True, 6548 selectedList = 3, 6549 noneSelectedText = "Select", 6550 columns = None, 6551 create = None, 6552 ):
6553 """ 6554 Constructor 6555 6556 @param search: show an input field in the widget to search for options, 6557 can be: 6558 - True (always show search field) 6559 - False (never show the search field) 6560 - "auto" (show search if more than 10 options) 6561 - <number> (show search if more than <number> options) 6562 @param header: show a header for the options list, can be: 6563 - True (show the default Select All/Deselect All header) 6564 - False (don't show a header unless required for search field) 6565 @param selectedList: maximum number of individual selected options to show 6566 on the widget button (before collapsing into "<number> 6567 selected") 6568 @param noneSelectedText: text to show on the widget button when no option is 6569 selected (automatic l10n, no T() required) 6570 @param columns: set the columns width class for Foundation forms 6571 @param create: options to create a new record {c: 'controller', 6572 f: 'function', 6573 label: 'label', 6574 parent: 'parent', (optional: which function to lookup options from) 6575 child: 'child', (optional: which field to lookup options for) 6576 } 6577 @ToDo: Complete the 'create' feature: 6578 * Ensure the Create option doesn't get filtered out when searching for items 6579 * Style option to make it clearer that it's an Action item 6580 """ 6581 6582 self.search = search 6583 self.header = header 6584 self.multiple = multiple 6585 self.selectedList = selectedList 6586 self.noneSelectedText = noneSelectedText 6587 self.columns = columns 6588 self.create = create
6589
6590 - def __call__(self, field, value, **attr):
6591 6592 T = current.T 6593 6594 if isinstance(field, Field): 6595 selector = str(field).replace(".", "_") 6596 else: 6597 selector = field.name.replace(".", "_") 6598 6599 # Widget 6600 _class = attr.get("_class", None) 6601 if _class: 6602 if "multiselect-widget" not in _class: 6603 attr["_class"] = "%s multiselect-widget" % _class 6604 else: 6605 attr["_class"] = "multiselect-widget" 6606 6607 multiple_opt = self.multiple 6608 if multiple_opt: 6609 w = MultipleOptionsWidget 6610 else: 6611 w = OptionsWidget 6612 if value: 6613 # Base widget requires single value, so enforce that 6614 # if necessary, and convert to string to match options 6615 value = str(value[0] if type(value) is list else value) 6616 6617 # Set explicit columns width for the formstyle 6618 if self.columns: 6619 attr["s3cols"] = self.columns 6620 6621 widget = w.widget(field, value, **attr) 6622 options_len = len(widget) 6623 6624 # Search field and header for multiselect options list 6625 search_opt = self.search 6626 header_opt = self.header 6627 if not multiple_opt and header_opt is True: 6628 # Select All / Unselect All doesn't make sense if multiple == False 6629 header_opt = False 6630 if not isinstance(search_opt, bool) and \ 6631 (search_opt == "auto" or isinstance(search_opt, (int, long))): 6632 max_options = 10 if search_opt == "auto" else search_opt 6633 if options_len > max_options: 6634 search_opt = True 6635 else: 6636 search_opt = False 6637 if search_opt is True and header_opt is False: 6638 # Must have at least "" as header to show the search field 6639 header_opt = "" 6640 6641 # Other options: 6642 # * Show Selected List 6643 if header_opt is True: 6644 header = '''checkAllText:'%s',uncheckAllText:"%s"''' % \ 6645 (T("Select All"), 6646 T("Clear All")) 6647 elif header_opt is False: 6648 header = '''header:false''' 6649 else: 6650 header = '''header:"%s"''' % header_opt 6651 noneSelectedText = self.noneSelectedText 6652 if not isinstance(noneSelectedText, lazyT): 6653 noneSelectedText = T(noneSelectedText) 6654 create = self.create or "" 6655 if create: 6656 tablename = "%s_%s" % (create["c"], create["f"]) 6657 if current.auth.s3_has_permission("create", tablename): 6658 create = ",create:%s" % json.dumps(create, separators=SEPARATORS) 6659 else: 6660 create = "" 6661 script = '''$('#%s').multiselect({allSelectedText:'%s',selectedText:'%s',%s,height:300,minWidth:0,selectedList:%s,noneSelectedText:'%s',multiple:%s%s})''' % \ 6662 (selector, 6663 T("All selected"), 6664 T("# selected"), 6665 header, 6666 self.selectedList, 6667 noneSelectedText, 6668 "true" if multiple_opt else "false", 6669 create 6670 ) 6671 6672 if search_opt: 6673 script = '''%s.multiselectfilter({label:'',placeholder:'%s'})''' % \ 6674 (script, T("Search")) 6675 jquery_ready = current.response.s3.jquery_ready 6676 if script not in jquery_ready: # Prevents loading twice when form has errors 6677 jquery_ready.append(script) 6678 6679 return widget
6680
6681 # ============================================================================= 6682 -class S3CascadeSelectWidget(FormWidget):
6683 """ Cascade Selector for Hierarchies """ 6684
6685 - def __init__(self, 6686 lookup=None, 6687 formstyle=None, 6688 levels=None, 6689 multiple=False, 6690 filter=None, 6691 leafonly=True, 6692 cascade=None, 6693 represent=None, 6694 inline=False, 6695 ):
6696 """ 6697 Constructor 6698 6699 @param lookup: the name of the hierarchical lookup-table 6700 @param formstyle: the formstyle to use for the inline-selectors 6701 (defaults to s3.crud.formstyle) 6702 @param levels: list of labels for the hierarchy levels, in 6703 top-down order 6704 @param multiple: allow selection of multiple options 6705 @param filter: resource filter expression to filter the 6706 selectable options 6707 @param leafonly: allow only leaf-nodes to be selected 6708 @param cascade: automatically select child-nodes when a 6709 parent node is selected (override option, 6710 implied by leafonly if not set explicitly) 6711 @param represent: representation function for the nodes 6712 (defaults to the represent of the field) 6713 @param inline: formstyle uses inline-labels, so add a colon 6714 """ 6715 6716 self.lookup = lookup 6717 self.formstyle = formstyle 6718 6719 self.levels = levels 6720 self.multiple = multiple 6721 6722 self.filter = filter 6723 self.leafonly = leafonly 6724 self.cascade = cascade 6725 6726 self.represent = represent 6727 self.inline = inline
6728 6729 # -------------------------------------------------------------------------
6730 - def __call__(self, field, value, **attr):
6731 """ 6732 Widget renderer 6733 6734 @param field: the Field 6735 @param value: the current value(s) 6736 @param attr: additional HTML attributes for the widget 6737 """ 6738 6739 # Get the lookup table 6740 lookup = self.lookup 6741 if not lookup: 6742 lookup = s3_get_foreign_key(field)[0] 6743 if not lookup: 6744 raise SyntaxError("No lookup table known for %s" % field) 6745 6746 # Get the representation 6747 represent = self.represent 6748 if not represent: 6749 represent = field.represent 6750 6751 # Get the hierarchy 6752 leafonly = self.leafonly 6753 from s3hierarchy import S3Hierarchy 6754 h = S3Hierarchy(tablename = lookup, 6755 represent = represent, 6756 filter = self.filter, 6757 leafonly = leafonly, 6758 ) 6759 if not h.config: 6760 raise AttributeError("No hierarchy configured for %s" % lookup) 6761 6762 # Get the cascade levels 6763 levels = self.levels 6764 if not levels: 6765 levels = current.s3db.get_config(lookup, "hierarchy_levels") 6766 if not levels: 6767 levels = [field.label] 6768 6769 # Get the hierarchy nodes 6770 nodes = h.json(max_depth=len(levels)-1) 6771 6772 # Intended DOM-ID of the input field 6773 if isinstance(field, Field): 6774 input_id = str(field).replace(".", "_") 6775 else: 6776 input_id = field.name.replace(".", "_") 6777 6778 # Prepare labels and selectors 6779 selectors = [] 6780 multiple = "multiple" if self.multiple else None 6781 T = current.T 6782 for depth, level in enumerate(levels): 6783 # The selector for this level 6784 selector = SELECT(data = {"level": depth}, 6785 _class = "s3-cascade-select", 6786 _disabled = "disabled", 6787 _multiple = multiple, 6788 ) 6789 6790 # The label for the selector 6791 row_id = "%s_level_%s" % (input_id, depth) 6792 label = T(level) if isinstance(level, basestring) else level 6793 if self.inline: 6794 label = "%s:" % label 6795 label = LABEL(label, _for=row_id, _id="%s__label" % row_id) 6796 selectors.append((row_id, label, selector, None)) 6797 6798 # Build inline-rows from labels+selectors 6799 formstyle = self.formstyle 6800 if not formstyle: 6801 formstyle = current.response.s3.crud.formstyle 6802 selector_rows = formstyle(None, selectors) 6803 6804 # Construct the widget 6805 widget_id = attr.get("_id") 6806 if not widget_id: 6807 widget_id = "%s-cascade" % input_id 6808 widget = DIV(self.hidden_input(input_id, field, value, **attr), 6809 INPUT(_type = "hidden", 6810 _class = "s3-cascade", 6811 _value = json.dumps(nodes), 6812 ), 6813 selector_rows, 6814 _class = "s3-cascade-select", 6815 _id = widget_id, 6816 ) 6817 6818 # Inject static JS and instantiate UI widget 6819 cascade = self.cascade 6820 if leafonly and cascade is not False: 6821 cascade = True 6822 6823 widget_opts = {"multiple": True if multiple else False, 6824 "leafonly": leafonly, 6825 "cascade": cascade, 6826 } 6827 self.inject_script(widget_id, widget_opts) 6828 6829 return widget
6830 6831 # -------------------------------------------------------------------------
6832 - def hidden_input(self, input_id, field, value, **attr):
6833 """ 6834 Construct the hidden (real) input and populate it with the 6835 current field value 6836 6837 @param input_id: the DOM-ID for the input 6838 @param field: the Field 6839 @param value: the current value 6840 @param attr: widget attributes from caller 6841 """ 6842 6843 # Currently selected values 6844 selected = [] 6845 append = selected.append 6846 if isinstance(value, basestring) and value and not value.isdigit(): 6847 value = self.parse(value)[0] 6848 if not isinstance(value, (list, tuple, set)): 6849 values = [value] 6850 else: 6851 values = value 6852 for v in values: 6853 if isinstance(v, (int, long)) or str(v).isdigit(): 6854 append(v) 6855 6856 # Prepend value parser to field validator 6857 requires = field.requires 6858 if isinstance(requires, (list, tuple)): 6859 requires = [self.parse] + requires 6860 elif requires is not None: 6861 requires = [self.parse, requires] 6862 else: 6863 requires = self.parse 6864 6865 # The hidden input field 6866 hidden_input = INPUT(_type = "hidden", 6867 _name = attr.get("_name") or field.name, 6868 _id = input_id, 6869 _class = "s3-cascade-input", 6870 requires = requires, 6871 value = json.dumps(selected), 6872 ) 6873 6874 return hidden_input
6875 6876 # ------------------------------------------------------------------------- 6877 @staticmethod
6878 - def inject_script(widget_id, options):
6879 """ 6880 Inject static JS and instantiate client-side UI widget 6881 6882 @param widget_id: the widget ID 6883 @param options: JSON-serializable dict with UI widget options 6884 """ 6885 6886 request = current.request 6887 s3 = current.response.s3 6888 6889 # Static script 6890 if s3.debug: 6891 script = "/%s/static/scripts/S3/s3.ui.cascadeselect.js" % \ 6892 request.application 6893 else: 6894 script = "/%s/static/scripts/S3/s3.ui.cascadeselect.min.js" % \ 6895 request.application 6896 scripts = s3.scripts 6897 if script not in scripts: 6898 scripts.append(script) 6899 6900 # Widget options 6901 opts = {} 6902 if options: 6903 opts.update(options) 6904 6905 # Widget instantiation 6906 script = '''$('#%(widget_id)s').cascadeSelect(%(options)s)''' % \ 6907 {"widget_id": widget_id, 6908 "options": json.dumps(opts), 6909 } 6910 jquery_ready = s3.jquery_ready 6911 if script not in jquery_ready: 6912 jquery_ready.append(script)
6913 6914 # -------------------------------------------------------------------------
6915 - def parse(self, value):
6916 """ 6917 Value parser for the hidden input field of the widget 6918 6919 @param value: the value received from the client, JSON string 6920 6921 @return: a list (if multiple=True) or the value 6922 """ 6923 6924 default = [] if self.multiple else None 6925 6926 if value is None: 6927 return None, None 6928 try: 6929 value = json.loads(value) 6930 except ValueError: 6931 return default, None 6932 if not self.multiple and isinstance(value, list): 6933 value = value[0] if value else None 6934 6935 return value, None
6936
6937 # ============================================================================= 6938 -class S3HierarchyWidget(FormWidget):
6939 """ Selector Widget for Hierarchies """ 6940
6941 - def __init__(self, 6942 lookup = None, 6943 represent = None, 6944 multiple = True, 6945 leafonly = True, 6946 cascade = False, 6947 bulk_select = False, 6948 filter = None, 6949 columns = None, 6950 none = None, 6951 ):
6952 """ 6953 Constructor 6954 6955 @param lookup: name of the lookup table (must have a hierarchy 6956 configured) 6957 @param represent: alternative representation method (falls back 6958 to the field's represent-method) 6959 @param multiple: allow selection of multiple options 6960 @param leafonly: True = only leaf nodes can be selected (with 6961 multiple=True: selection of a parent node will 6962 automatically select all leaf nodes of that 6963 branch) 6964 False = any nodes can be selected independently 6965 @param cascade: automatic selection of children when selecting 6966 a parent node (if leafonly=False, otherwise 6967 this is the standard behavior!), requires 6968 multiple=True 6969 @param bulk_select: provide option to select/deselect all nodes 6970 @param filter: filter query for the lookup table 6971 @param columns: set the columns width class for Foundation forms 6972 @param none: label for an option that delivers "None" as value 6973 (useful for HierarchyFilters with explicit none-selection) 6974 """ 6975 6976 self.lookup = lookup 6977 self.represent = represent 6978 self.filter = filter 6979 6980 self.multiple = multiple 6981 self.leafonly = leafonly 6982 self.cascade = cascade 6983 6984 self.columns = columns 6985 self.bulk_select = bulk_select 6986 6987 self.none = none
6988 6989 # -------------------------------------------------------------------------
6990 - def __call__(self, field, value, **attr):
6991 """ 6992 Widget renderer 6993 6994 @param field: the Field 6995 @param value: the current value(s) 6996 @param attr: additional HTML attributes for the widget 6997 """ 6998 6999 if isinstance(field, Field): 7000 selector = str(field).replace(".", "_") 7001 else: 7002 selector = field.name.replace(".", "_") 7003 7004 # Widget ID 7005 widget_id = attr.get("_id") 7006 if widget_id == None: 7007 widget_id = attr["_id"] = "%s-hierarchy" % selector 7008 7009 # Field name 7010 name = attr.get("_name") 7011 if not name: 7012 name = field.name 7013 7014 # Get the lookup table 7015 lookup = self.lookup 7016 if not lookup: 7017 lookup = s3_get_foreign_key(field)[0] 7018 if not lookup: 7019 raise SyntaxError("No lookup table known for %s" % field) 7020 7021 # Get the representation 7022 represent = self.represent 7023 if not represent: 7024 represent = field.represent 7025 7026 # Instantiate the hierarchy 7027 leafonly = self.leafonly 7028 from s3hierarchy import S3Hierarchy 7029 h = S3Hierarchy(tablename = lookup, 7030 represent = represent, 7031 leafonly = leafonly, 7032 filter = self.filter, 7033 ) 7034 if not h.config: 7035 raise AttributeError("No hierarchy configured for %s" % lookup) 7036 7037 # Set explicit columns width for the formstyle 7038 if self.columns: 7039 attr["s3cols"] = self.columns 7040 7041 # Generate the widget 7042 settings = current.deployment_settings 7043 cascade_option_in_tree = settings.get_ui_hierarchy_cascade_option_in_tree() 7044 7045 if self.multiple and self.bulk_select and \ 7046 not cascade_option_in_tree: 7047 # Render bulk-select options as separate header 7048 header = DIV(SPAN(A("Select All", 7049 _class="s3-hierarchy-select-all", 7050 ), 7051 " | ", 7052 A("Deselect All", 7053 _class="s3-hierarchy-deselect-all", 7054 ), 7055 _class="s3-hierarchy-bulkselect", 7056 ), 7057 _class="s3-hierarchy-header", 7058 ) 7059 else: 7060 header = "" 7061 7062 # Currently selected values 7063 selected = [] 7064 append = selected.append 7065 if isinstance(value, basestring) and value and not value.isdigit(): 7066 value = self.parse(value)[0] 7067 if not isinstance(value, (list, tuple, set)): 7068 values = [value] 7069 else: 7070 values = value 7071 for v in values: 7072 if isinstance(v, (int, long)) or str(v).isdigit(): 7073 append(v) 7074 7075 # Prepend value parser to field validator 7076 requires = field.requires 7077 if isinstance(requires, (list, tuple)): 7078 requires = [self.parse] + requires 7079 elif requires is not None: 7080 requires = [self.parse, requires] 7081 else: 7082 requires = self.parse 7083 7084 # The hidden input field 7085 hidden_input = INPUT(_type = "hidden", 7086 _multiple = "multiple", 7087 _name = name, 7088 _id = selector, 7089 _class = "s3-hierarchy-input", 7090 requires = requires, 7091 value = json.dumps(selected), 7092 ) 7093 7094 # The widget 7095 widget = DIV(hidden_input, 7096 DIV(header, 7097 DIV(h.html("%s-tree" % widget_id, 7098 none=self.none, 7099 ), 7100 _class = "s3-hierarchy-tree", 7101 ), 7102 _class = "s3-hierarchy-wrapper", 7103 ), 7104 **attr) 7105 widget.add_class("s3-hierarchy-widget") 7106 7107 s3 = current.response.s3 7108 scripts = s3.scripts 7109 script_dir = "/%s/static/scripts" % current.request.application 7110 7111 # Custom theme 7112 theme = settings.get_ui_hierarchy_theme() 7113 7114 if s3.debug: 7115 script = "%s/jstree.js" % script_dir 7116 if script not in scripts: 7117 scripts.append(script) 7118 script = "%s/S3/s3.ui.hierarchicalopts.js" % script_dir 7119 if script not in scripts: 7120 scripts.append(script) 7121 style = "%s/jstree.css" % theme.get("css", "plugins") 7122 if style not in s3.stylesheets: 7123 s3.stylesheets.append(style) 7124 else: 7125 script = "%s/S3/s3.jstree.min.js" % script_dir 7126 if script not in scripts: 7127 scripts.append(script) 7128 style = "%s/jstree.min.css" % theme.get("css", "plugins") 7129 if style not in s3.stylesheets: 7130 s3.stylesheets.append(style) 7131 7132 T = current.T 7133 7134 widget_opts = {"selected": selected, 7135 "selectedText": str(T("# selected")), 7136 "noneSelectedText": str(T("Select")), 7137 "noOptionsText": str(T("No options available")), 7138 "selectAllText": str(T("Select All")), 7139 "deselectAllText": str(T("Deselect All")), 7140 } 7141 7142 # Only include non-default options 7143 if not self.multiple: 7144 widget_opts["multiple"] = False 7145 if not leafonly: 7146 widget_opts["leafonly"] = False 7147 if self.cascade: 7148 widget_opts["cascade"] = True 7149 if self.bulk_select: 7150 widget_opts["bulkSelect"] = True 7151 if not cascade_option_in_tree: 7152 widget_opts["cascadeOptionInTree"] = False 7153 icons = theme.get("icons", False) 7154 if icons: 7155 widget_opts["icons"] = icons 7156 stripes = theme.get("stripes", True) 7157 if not stripes: 7158 widget_opts["stripes"] = stripes 7159 7160 7161 script = '''$('#%(widget_id)s').hierarchicalopts(%(widget_opts)s)''' % \ 7162 {"widget_id": widget_id, 7163 "widget_opts": json.dumps(widget_opts, separators=SEPARATORS), 7164 } 7165 7166 s3.jquery_ready.append(script) 7167 7168 return widget
7169 7170 # -------------------------------------------------------------------------
7171 - def parse(self, value):
7172 """ 7173 Value parser for the hidden input field of the widget 7174 7175 @param value: the value received from the client, JSON string 7176 7177 @return: a list (if multiple=True) or the value 7178 """ 7179 7180 default = [] if self.multiple else None 7181 7182 if value is None: 7183 return None, None 7184 try: 7185 value = json.loads(value) 7186 except ValueError: 7187 return default, None 7188 if not self.multiple and isinstance(value, list): 7189 value = value[0] if value else None 7190 7191 return value, None
7192
7193 # ============================================================================= 7194 -class S3OptionsMatrixWidget(FormWidget):
7195 """ 7196 Constructs a two dimensional array/grid of checkboxes 7197 with row and column headers. 7198 """ 7199
7200 - def __init__(self, rows, cols):
7201 """ 7202 @type rows: tuple 7203 @param rows: 7204 A tuple of tuples. 7205 The nested tuples will have the row label followed by a value 7206 for each checkbox in that row. 7207 7208 @type cols: tuple 7209 @param cols: 7210 A tuple containing the labels to use in the column headers 7211 """ 7212 self.rows = rows 7213 self.cols = cols
7214
7215 - def __call__(self, field, value, **attributes):
7216 """ 7217 Returns the grid/matrix of checkboxes as a web2py TABLE object and 7218 adds references to required Javascript files. 7219 7220 @type field: Field 7221 @param field: 7222 This gets passed in when the widget is rendered or used. 7223 7224 @type value: list 7225 @param value: 7226 A list of the values matching those of the checkboxes. 7227 7228 @param attributes: 7229 HTML attributes to assign to the table. 7230 """ 7231 7232 if isinstance(value, (list, tuple)): 7233 values = [str(v) for v in value] 7234 else: 7235 values = [str(value)] 7236 7237 # Create the table header 7238 header_cells = [] 7239 for col in self.cols: 7240 header_cells.append(TH(col, _scope="col")) 7241 header = THEAD(TR(header_cells)) 7242 7243 # Create the table body cells 7244 grid_rows = [] 7245 for row in self.rows: 7246 # Create a list to hold our table cells 7247 # the first cell will hold the row label 7248 row_cells = [TH(row[0], _scope="row")] 7249 for option in row[1:]: 7250 # This determines if the checkbox should be checked 7251 if option in values: 7252 checked = True 7253 else: 7254 checked = False 7255 7256 row_cells.append(TD( 7257 INPUT(_type="checkbox", 7258 _name=field.name, 7259 _value=option, 7260 value=checked 7261 ) 7262 )) 7263 grid_rows.append(TR(row_cells)) 7264 7265 s3 = current.response.s3 7266 s3.scripts.append("/%s/static/scripts/S3/s3.optionsmatrix.js" % current.request.application) 7267 7268 # If the table has an id attribute, activate the jQuery plugin for it. 7269 if "_id" in attributes: 7270 s3.jquery_ready.append('''$('#{0}').s3optionsmatrix()'''.format(attributes.get("_id"))) 7271 7272 return TABLE(header, TBODY(grid_rows), **attributes)
7273
7274 # ============================================================================= 7275 -class S3OrganisationAutocompleteWidget(FormWidget):
7276 """ 7277 Renders an org_organisation SELECT as an INPUT field with AJAX Autocomplete. 7278 Differs from the S3AutocompleteWidget in that it can default to the setting in the profile. 7279 7280 @ToDo: Add an option to hide the widget completely when using the Org from the Profile 7281 - i.e. prevent user overrides 7282 """ 7283
7284 - def __init__(self, 7285 post_process = "", 7286 default_from_profile = False, 7287 ):
7288 7289 self.post_process = post_process 7290 self.tablename = "org_organisation" 7291 self.default_from_profile = default_from_profile
7292
7293 - def __call__(self, field, value, **attributes):
7294 7295 def transform_value(value): 7296 if not value and self.default_from_profile: 7297 auth = current.session.auth 7298 if auth and auth.user: 7299 value = auth.user.organisation_id 7300 return value
7301 7302 settings = current.deployment_settings 7303 delay = settings.get_ui_autocomplete_delay() 7304 min_length = settings.get_ui_autocomplete_min_chars() 7305 7306 return S3GenericAutocompleteTemplate(self.post_process, 7307 delay, 7308 min_length, 7309 field, 7310 value, 7311 attributes, 7312 transform_value = transform_value, 7313 tablename = "org_organisation", 7314 )
7315
7316 # ============================================================================= 7317 -class S3OrganisationHierarchyWidget(OptionsWidget):
7318 """ Renders an organisation_id SELECT as a menu """ 7319 7320 _class = "widget-org-hierarchy" 7321
7322 - def __init__(self, primary_options=None):
7323 """ 7324 [{"id":12, "pe_id":4, "name":"Organisation Name"}] 7325 """ 7326 self.primary_options = primary_options
7327
7328 - def __call__(self, field, value, **attributes):
7329 7330 options = self.primary_options 7331 name = attributes.get("_name", field.name) 7332 7333 if options is None: 7334 requires = field.requires 7335 if isinstance(requires, (list, tuple)) and \ 7336 len(requires): 7337 requires = requires[0] 7338 if requires is not None: 7339 if isinstance(requires, IS_EMPTY_OR): 7340 requires = requires.other 7341 if hasattr(requires, "options"): 7342 table = current.s3db.org_organisation 7343 options = requires.options() 7344 ids = [option[0] for option in options if option[0]] 7345 rows = current.db(table.id.belongs(ids)).select(table.id, 7346 table.pe_id, 7347 table.name, 7348 orderby=table.name) 7349 options = [] 7350 for row in rows: 7351 options.append(row.as_dict()) 7352 else: 7353 raise SyntaxError, "widget cannot determine options of %s" % field 7354 7355 javascript_array = '''%s_options=%s''' % (name, 7356 json.dumps(options, separators=SEPARATORS)) 7357 s3 = current.response.s3 7358 s3.js_global.append(javascript_array) 7359 s3.scripts.append("/%s/static/scripts/S3/s3.orghierarchy.js" % \ 7360 current.request.application) 7361 7362 return self.widget(field, value, **attributes)
7363
7364 # ============================================================================= 7365 -class S3PersonAutocompleteWidget(FormWidget):
7366 """ 7367 Renders a pr_person SELECT as an INPUT field with AJAX Autocomplete. 7368 Differs from the S3AutocompleteWidget in that it uses 3 name fields 7369 7370 To make this widget use the HR table, set the controller to "hrm" 7371 7372 @ToDo: Migrate to template (initial attempt failed) 7373 """ 7374
7375 - def __init__(self, 7376 controller = "pr", 7377 function = "person_search", 7378 post_process = "", 7379 hideerror = False, 7380 ajax_filter = "", 7381 ):
7382 7383 self.post_process = post_process 7384 self.c = controller 7385 self.f = function 7386 self.hideerror = hideerror 7387 self.ajax_filter = ajax_filter
7388
7389 - def __call__(self, field, value, **attributes):
7390 7391 default = dict( 7392 _type = "text", 7393 value = (value is not None and str(value)) or "", 7394 ) 7395 attr = StringWidget._attributes(field, default, **attributes) 7396 7397 # Hide the real field 7398 attr["_class"] = "%s hide" % attr["_class"] 7399 7400 if "_id" in attr: 7401 real_input = attr["_id"] 7402 else: 7403 real_input = str(field).replace(".", "_") 7404 7405 dummy_input = "dummy_%s" % real_input 7406 7407 if value: 7408 try: 7409 value = long(value) 7410 except ValueError: 7411 pass 7412 # Provide the representation for the current/default Value 7413 text = s3_unicode(field.represent(value)) 7414 if "<" in text: 7415 text = s3_strip_markup(text) 7416 represent = text.encode("utf-8") 7417 else: 7418 represent = "" 7419 7420 script = '''S3.autocomplete.person('%(controller)s','%(fn)s',"%(input)s"''' % \ 7421 dict(controller = self.c, 7422 fn = self.f, 7423 input = real_input, 7424 ) 7425 options = "" 7426 post_process = self.post_process 7427 7428 settings = current.deployment_settings 7429 delay = settings.get_ui_autocomplete_delay() 7430 min_length = settings.get_ui_autocomplete_min_chars() 7431 7432 if self.ajax_filter: 7433 options = ''',"%(ajax_filter)s"''' % \ 7434 dict(ajax_filter = self.ajax_filter) 7435 7436 if min_length != 2: 7437 options += ''',"%(postprocess)s",%(delay)s,%(min_length)s''' % \ 7438 dict(postprocess = post_process, 7439 delay = delay, 7440 min_length = min_length) 7441 elif delay != 800: 7442 options += ''',"%(postprocess)s",%(delay)s''' % \ 7443 dict(postprocess = post_process, 7444 delay = delay) 7445 elif post_process: 7446 options += ''',"%(postprocess)s"''' % \ 7447 dict(postprocess = post_process) 7448 7449 script = '''%s%s)''' % (script, options) 7450 current.response.s3.jquery_ready.append(script) 7451 7452 return TAG[""](INPUT(_id=dummy_input, 7453 _class="string", 7454 _value=represent), 7455 DIV(_id="%s_throbber" % dummy_input, 7456 _class="throbber input_throbber hide"), 7457 INPUT(hideerror=self.hideerror, **attr), 7458 requires = field.requires 7459 )
7460
7461 # ============================================================================= 7462 -class S3PentityAutocompleteWidget(FormWidget):
7463 """ 7464 Renders a pr_pentity SELECT as an INPUT field with AJAX Autocomplete. 7465 Differs from the S3AutocompleteWidget in that it can filter by type & 7466 also represents results with the type 7467 """ 7468
7469 - def __init__(self, 7470 controller = "pr", 7471 function = "pentity", 7472 types = None, 7473 post_process = "", 7474 hideerror = False, 7475 ):
7476 7477 self.post_process = post_process 7478 self.c = controller 7479 self.f = function 7480 self.types = None 7481 self.hideerror = hideerror
7482
7483 - def __call__(self, field, value, **attributes):
7484 7485 default = dict( 7486 _type = "text", 7487 value = (value is not None and str(value)) or "", 7488 ) 7489 attr = StringWidget._attributes(field, default, **attributes) 7490 7491 # Hide the real field 7492 attr["_class"] = "%s hide" % attr["_class"] 7493 7494 if "_id" in attr: 7495 real_input = attr["_id"] 7496 else: 7497 real_input = str(field).replace(".", "_") 7498 7499 dummy_input = "dummy_%s" % real_input 7500 7501 if value: 7502 try: 7503 value = long(value) 7504 except ValueError: 7505 pass 7506 # Provide the representation for the current/default Value 7507 text = s3_unicode(field.represent(value)) 7508 if "<" in text: 7509 text = s3_strip_markup(text) 7510 represent = text.encode("utf-8") 7511 else: 7512 represent = "" 7513 7514 T = current.T 7515 s3 = current.response.s3 7516 script = \ 7517 '''i18n.person="%s"\ni18n.group="%s"\ni18n.none_of_the_above="%s"''' % \ 7518 (T("Person"), T("Group"), T("None of the above")) 7519 s3.js_global.append(script) 7520 7521 if self.types: 7522 # Something other than default: ("pr_person", "pr_group") 7523 types = json.dumps(self.types, separators=SEPARATORS) 7524 else: 7525 types = "" 7526 7527 script = '''S3.autocomplete.pentity('%(controller)s','%(fn)s',"%(input)s"''' % \ 7528 dict(controller = self.c, 7529 fn = self.f, 7530 input = real_input) 7531 7532 options = "" 7533 post_process = self.post_process 7534 7535 settings = current.deployment_settings 7536 delay = settings.get_ui_autocomplete_delay() 7537 min_length = settings.get_ui_autocomplete_min_chars() 7538 7539 if types: 7540 options = ''',"%(postprocess)s",%(delay)s,%(min_length)s,%(types)s''' % \ 7541 dict(postprocess = post_process, 7542 delay = delay, 7543 min_length = min_length, 7544 types = types) 7545 elif min_length != 2: 7546 options = ''',"%(postprocess)s",%(delay)s,%(min_length)s''' % \ 7547 dict(postprocess = post_process, 7548 delay = delay, 7549 min_length = min_length) 7550 elif delay != 800: 7551 options = ''',"%(postprocess)s",%(delay)s''' % \ 7552 dict(postprocess = post_process, 7553 delay = delay) 7554 elif post_process: 7555 options = ''',"%(postprocess)s"''' % \ 7556 dict(postprocess = post_process) 7557 7558 script = '''%s%s)''' % (script, options) 7559 s3.jquery_ready.append(script) 7560 return TAG[""](INPUT(_id=dummy_input, 7561 _class="string", 7562 _value=represent), 7563 DIV(_id="%s_throbber" % dummy_input, 7564 _class="throbber input_throbber hide"), 7565 INPUT(hideerror=self.hideerror, **attr), 7566 requires = field.requires 7567 )
7568
7569 # ============================================================================= 7570 -class S3PriorityListWidget(StringWidget):
7571 """ 7572 Widget to broadcast facility needs 7573 """ 7574
7575 - def __call__(self, field, value, **attributes):
7576 7577 s3 = current.response.s3 7578 7579 default = dict( 7580 _type = "text", 7581 value = (value is not None and str(value)) or "", 7582 ) 7583 attr = StringWidget._attributes(field, default, **attributes) 7584 7585 # @ToDo: i18n strings in JS 7586 #T = current.T 7587 7588 selector = str(field).replace(".", "_") 7589 s3.jquery_ready.append(''' 7590 $('#%s').removeClass('list').addClass('prioritylist').prioritylist()''' % \ 7591 (selector)) 7592 7593 # @ToDo: minify 7594 s3.scripts.append("/%s/static/scripts/S3/s3.prioritylist.js" % current.request.application) 7595 s3.stylesheets.append("S3/s3.prioritylist.css") 7596 7597 return TAG[""](INPUT(**attr), 7598 requires = field.requires 7599 )
7600
7601 # ============================================================================= 7602 -class S3SiteAutocompleteWidget(FormWidget):
7603 """ 7604 Renders an org_site SELECT as an INPUT field with AJAX Autocomplete. 7605 Differs from the S3AutocompleteWidget in that it uses name & type fields 7606 in the represent 7607 """ 7608
7609 - def __init__(self, 7610 post_process = "", 7611 ):
7612 7613 self.auth = current.auth 7614 self.post_process = post_process
7615
7616 - def __call__(self, field, value, **attributes):
7617 7618 default = dict( 7619 _type = "text", 7620 value = (value is not None and str(value)) or "", 7621 ) 7622 attr = StringWidget._attributes(field, default, **attributes) 7623 7624 # Hide the real field 7625 attr["_class"] = "%s hide" % attr["_class"] 7626 7627 if "_id" in attr: 7628 real_input = attr["_id"] 7629 else: 7630 real_input = str(field).replace(".", "_") 7631 dummy_input = "dummy_%s" % real_input 7632 7633 if value: 7634 try: 7635 value = long(value) 7636 except ValueError: 7637 pass 7638 # Provide the representation for the current/default Value 7639 represent = field.represent 7640 if hasattr(represent, "link"): 7641 # S3Represent, so don't generate HTML 7642 text = s3_unicode(represent(value, show_link=False)) 7643 else: 7644 # Custom represent, so filter out HTML later 7645 text = s3_unicode(represent(value)) 7646 if "<" in text: 7647 text = s3_strip_markup(text) 7648 represent = text.encode("utf-8") 7649 else: 7650 represent = "" 7651 7652 7653 7654 s3 = current.response.s3 7655 site_types = current.auth.org_site_types 7656 for instance_type in site_types: 7657 # Change from T() 7658 site_types[instance_type] = s3_unicode(site_types[instance_type]) 7659 site_types = '''S3.org_site_types=%s''' % json.dumps(site_types, separators=SEPARATORS) 7660 7661 settings = current.deployment_settings 7662 delay = settings.get_ui_autocomplete_delay() 7663 min_length = settings.get_ui_autocomplete_min_chars() 7664 7665 js_global = s3.js_global 7666 if site_types not in js_global: 7667 js_global.append(site_types) 7668 script = '''S3.autocomplete.site('%(input)s',"%(postprocess)s"''' % \ 7669 dict(input = real_input, 7670 postprocess = self.post_process, 7671 ) 7672 if delay != 800: 7673 script = "%s,%s" % (script, delay) 7674 if min_length != 2: 7675 script = "%s,%s" % (script, min_length) 7676 elif min_length != 2: 7677 script = "%s,,%s" % (script, min_length) 7678 script = "%s)" % script 7679 7680 s3.jquery_ready.append(script) 7681 return TAG[""](INPUT(_id=dummy_input, 7682 _class="string", 7683 _value=represent), 7684 DIV(_id="%s_throbber" % dummy_input, 7685 _class="throbber input_throbber hide"), 7686 INPUT(**attr), 7687 requires = field.requires 7688 )
7689
7690 # ============================================================================= 7691 -class S3SliderWidget(FormWidget):
7692 """ 7693 Standard Slider Widget 7694 7695 The range of the Slider is derived from the Validator 7696 """ 7697
7698 - def __init__(self, 7699 step = 1, 7700 type = "int", 7701 ):
7702 self.step = step 7703 self.type = type
7704
7705 - def __call__(self, field, value, **attributes):
7706 7707 validator = field.requires 7708 field = str(field) 7709 fieldname = field.replace(".", "_") 7710 input_field = INPUT(_name = field.split(".")[1], 7711 _disabled = True, 7712 _id = fieldname, 7713 _style = "border:0", 7714 _value = value, 7715 ) 7716 slider = DIV(_id="%s_slider" % fieldname, **attributes) 7717 7718 s3 = current.response.s3 7719 7720 if isinstance(validator, IS_EMPTY_OR): 7721 validator = validator.other 7722 7723 self.min = validator.minimum 7724 7725 # Max Value depends upon validator type 7726 if isinstance(validator, IS_INT_IN_RANGE): 7727 self.max = validator.maximum - 1 7728 elif isinstance(validator, IS_FLOAT_IN_RANGE): 7729 self.max = validator.maximum 7730 7731 if value is None: 7732 # JSONify 7733 value = "null" 7734 script = '''i18n.slider_help="%s"''' % \ 7735 current.T("Click on the slider to choose a value") 7736 s3.js_global.append(script) 7737 7738 if self.type == "int": 7739 script = '''S3.slider('%s',%i,%i,%i,%s)''' % (fieldname, 7740 self.min, 7741 self.max, 7742 self.step, 7743 value) 7744 else: 7745 # Float 7746 script = '''S3.slider('%s',%f,%f,%f,%s)''' % (fieldname, 7747 self.min, 7748 self.max, 7749 self.step, 7750 value) 7751 s3.jquery_ready.append(script) 7752 7753 return TAG[""](input_field, slider)
7754
7755 # ============================================================================= 7756 -class S3StringWidget(StringWidget):
7757 """ 7758 Extend the default Web2Py widget to include a Placeholder 7759 """ 7760
7761 - def __init__(self, 7762 columns = 10, 7763 placeholder = None, 7764 prefix = None, 7765 textarea = False, 7766 ):
7767 """ 7768 Constructor 7769 7770 @param columns: number of grid columns to span (Foundation-themes) 7771 @param placeholder: placeholder text for the input field 7772 @param prefix: text for prefix button (Foundation-themes) 7773 @param textarea: render as textarea rather than string input 7774 """ 7775 7776 self.columns = columns 7777 self.placeholder = placeholder 7778 self.prefix = prefix 7779 self.textarea = textarea
7780
7781 - def __call__(self, field, value, **attributes):
7782 7783 default = dict( 7784 value = (value is not None and str(value)) or "", 7785 ) 7786 7787 if self.textarea: 7788 attr = TextWidget._attributes(field, default, **attributes) 7789 else: 7790 attr = StringWidget._attributes(field, default, **attributes) 7791 7792 placeholder = self.placeholder 7793 if placeholder: 7794 attr["_placeholder"] = placeholder 7795 7796 if self.textarea: 7797 widget = TEXTAREA(**attr) 7798 else: 7799 widget = INPUT(**attr) 7800 7801 # NB These classes target Foundation Themes 7802 prefix = self.prefix 7803 if prefix: 7804 widget = DIV(DIV(SPAN(prefix, _class="prefix"), 7805 _class="small-1 columns", 7806 ), 7807 DIV(widget, 7808 _class="small-11 columns", 7809 ), 7810 _class="row collapse", 7811 ) 7812 7813 # Set explicit columns width for the formstyle 7814 columns = self.columns 7815 if columns: 7816 widget["s3cols"] = columns 7817 7818 return widget
7819
7820 # ============================================================================= 7821 -class S3TimeIntervalWidget(FormWidget):
7822 """ 7823 Simple time interval widget for the scheduler task table 7824 """ 7825 7826 multipliers = (("weeks", 604800), 7827 ("days", 86400), 7828 ("hours", 3600), 7829 ("minutes", 60), 7830 ("seconds", 1)) 7831 7832 # ------------------------------------------------------------------------- 7833 @staticmethod
7834 - def widget(field, value, **attributes):
7835 7836 multipliers = S3TimeIntervalWidget.multipliers 7837 7838 if value is None: 7839 value = 0 7840 elif isinstance(value, basestring): 7841 try: 7842 value = int(value) 7843 except ValueError: 7844 value = 0 7845 7846 if value == 0: 7847 multiplier = 1 7848 else: 7849 for m in multipliers: 7850 multiplier = m[1] 7851 if int(value) % multiplier == 0: 7852 break 7853 7854 options = [] 7855 for i in xrange(1, len(multipliers) + 1): 7856 title, opt = multipliers[-i] 7857 if opt == multiplier: 7858 option = OPTION(title, _value=opt, _selected="selected") 7859 else: 7860 option = OPTION(title, _value=opt) 7861 options.append(option) 7862 7863 val = value / multiplier 7864 inp = DIV(INPUT(value = val, 7865 requires = field.requires, 7866 _id = ("%s" % field).replace(".", "_"), 7867 _name = field.name), 7868 SELECT(options, 7869 _name=("%s_multiplier" % field).replace(".", "_"))) 7870 return inp
7871 7872 # ------------------------------------------------------------------------- 7873 @staticmethod
7874 - def represent(value):
7875 7876 multipliers = S3TimeIntervalWidget.multipliers 7877 7878 try: 7879 val = int(value) 7880 except ValueError: 7881 val = 0 7882 7883 if val == 0: 7884 multiplier = multipliers[-1] 7885 else: 7886 for m in multipliers: 7887 if val % m[1] == 0: 7888 multiplier = m 7889 break 7890 7891 val = val / multiplier[1] 7892 return "%s %s" % (val, current.T(multiplier[0]))
7893
7894 # ============================================================================= 7895 -class S3UploadWidget(UploadWidget):
7896 """ 7897 Subclass for use in inline-forms 7898 7899 - always renders all widget elements (even when empty), so that 7900 they can be updated from JavaScript 7901 - adds CSS selectors for widget elements 7902 """ 7903 7904 @classmethod
7905 - def widget(cls, field, value, download_url=None, **attributes):
7906 """ 7907 generates a INPUT file tag. 7908 7909 Optionally provides an A link to the file, including a checkbox so 7910 the file can be deleted. 7911 All is wrapped in a DIV. 7912 7913 @see: :meth:`FormWidget.widget` 7914 @param download_url: Optional URL to link to the file (default = None) 7915 7916 """ 7917 7918 T = current.T 7919 7920 # File input 7921 default = {"_type": "file", 7922 } 7923 attr = cls._attributes(field, default, **attributes) 7924 7925 # File URL 7926 base_url = "/default/download" 7927 if download_url and value: 7928 if callable(download_url): 7929 url = download_url(value) 7930 else: 7931 base_url = download_url 7932 url = download_url + "/" + value 7933 else: 7934 url = None 7935 7936 # Download-link 7937 link = SPAN("[", 7938 A(T(cls.GENERIC_DESCRIPTION), 7939 _href = url, 7940 ), 7941 _class = "s3-upload-link", 7942 _style = "white-space:nowrap", 7943 ) 7944 7945 # Delete-checkbox 7946 requires = attr["requires"] 7947 if requires == [] or isinstance(requires, IS_EMPTY_OR): 7948 name = field.name + cls.ID_DELETE_SUFFIX 7949 delete_checkbox = TAG[""]("|", 7950 INPUT(_type = "checkbox", 7951 _name = name, 7952 _id = name, 7953 ), 7954 LABEL(T(cls.DELETE_FILE), 7955 _for = name, 7956 _style = "display:inline", 7957 ), 7958 ) 7959 link.append(delete_checkbox) 7960 7961 # Complete link-element 7962 link.append("]") 7963 if not url: 7964 link.add_class("hide") 7965 7966 # Image preview 7967 preview_class = "s3-upload-preview" 7968 if value and cls.is_image(value): 7969 preview_url = url 7970 else: 7971 preview_url = None 7972 preview_class = "%s hide" % preview_class 7973 image = DIV(IMG(_alt = T("Loading"), 7974 _src = preview_url, 7975 _width = cls.DEFAULT_WIDTH, 7976 ), 7977 _class = preview_class, 7978 ) 7979 7980 # Construct the widget 7981 inp = DIV(INPUT(**attr), 7982 link, 7983 image, 7984 _class="s3-upload-widget", 7985 data = {"base": base_url, 7986 }, 7987 ) 7988 7989 return inp
7990
7991 # ============================================================================= 7992 -class S3FixedOptionsWidget(OptionsWidget):
7993 """ Non-introspective options widget """ 7994
7995 - def __init__(self, options, translate=False, sort=True, empty=True):
7996 """ 7997 Constructor 7998 7999 @param options: the options for the widget, either as iterable of 8000 tuples (value, representation) or as dict 8001 {value:representation}, or as iterable of strings 8002 if value is the same as representation 8003 @param translate: automatically translate the representation 8004 @param sort: alpha-sort options (by representation) 8005 @param empty: add an empty-option (to select none of the options) 8006 """ 8007 8008 self.options = options 8009 self.translate = translate 8010 self.sort = sort 8011 self.empty = empty
8012
8013 - def __call__(self, field, value, **attributes):
8014 8015 default = dict(value=value) 8016 attr = self._attributes(field, default, **attributes) 8017 8018 options = self.options 8019 8020 if isinstance(options, dict): 8021 options = options.items() 8022 8023 opts = [] 8024 translate = self.translate 8025 T = current.T 8026 has_none = False 8027 for option in options: 8028 if isinstance(option, tuple): 8029 k, v = option 8030 else: 8031 k, v = option, option 8032 if v is None: 8033 v = current.messages["NONE"] 8034 elif translate: 8035 v = T(v) 8036 if k in (None, ""): 8037 k = "" 8038 has_none = True 8039 opts.append((k, v)) 8040 8041 sort = self.sort 8042 if callable(sort): 8043 opts = sorted(opts, key=sort) 8044 elif sort: 8045 opts = sorted(opts, key=lambda item: item[1]) 8046 if self.empty and not has_none: 8047 opts.insert(0, ("", current.messages["NONE"])) 8048 8049 opts = [OPTION(v, _value=k) for (k, v) in opts] 8050 return SELECT(*opts, **attr)
8051
8052 # ============================================================================= 8053 -class CheckboxesWidgetS3(OptionsWidget):
8054 """ 8055 S3 version of gluon.sqlhtml.CheckboxesWidget: 8056 - configurable number of columns 8057 - supports also integer-type keys in option sets 8058 - has an identifiable class for styling 8059 8060 Used in Sync, Projects, Assess, Facilities 8061 """ 8062 8063 @classmethod
8064 - def widget(cls, field, value, **attributes):
8065 """ 8066 generates a TABLE tag, including INPUT checkboxes (multiple allowed) 8067 8068 see also: :meth:`FormWidget.widget` 8069 """ 8070 8071 #values = re.compile("[\w\-:]+").findall(str(value)) 8072 values = not isinstance(value, (list, tuple)) and [value] or value 8073 values = [str(v) for v in values] 8074 8075 attr = OptionsWidget._attributes(field, {}, **attributes) 8076 attr["_class"] = "checkboxes-widget-s3" 8077 8078 requires = field.requires 8079 if not isinstance(requires, (list, tuple)): 8080 requires = [requires] 8081 8082 if hasattr(requires[0], "options"): 8083 options = requires[0].options() 8084 else: 8085 raise SyntaxError, "widget cannot determine options of %s" \ 8086 % field 8087 8088 options = [(k, v) for k, v in options if k != ""] 8089 8090 options_help = attributes.get("options_help", {}) 8091 input_index = attributes.get("start_at", 0) 8092 8093 opts = [] 8094 cols = attributes.get("cols", 1) 8095 8096 totals = len(options) 8097 mods = totals % cols 8098 rows = totals / cols 8099 if mods: 8100 rows += 1 8101 8102 if totals == 0: 8103 T = current.T 8104 opts.append(TR(TD(SPAN(T("no options available"), 8105 _class="no-options-available"), 8106 INPUT(_name=field.name, 8107 _class="hide", 8108 _value=None)))) 8109 8110 for r_index in range(rows): 8111 tds = [] 8112 8113 for k, v in options[r_index * cols:(r_index + 1) * cols]: 8114 input_id = "id-%s-%s" % (field.name, input_index) 8115 option_help = options_help.get(str(k), "") 8116 if option_help: 8117 label = LABEL(v, _for=input_id, _title=option_help) 8118 else: 8119 # Don't provide empty client-side popups 8120 label = LABEL(v, _for=input_id) 8121 8122 tds.append(TD(INPUT(_type="checkbox", 8123 _name=field.name, 8124 _id=input_id, 8125 # Hide checkboxes without a label 8126 _class="" if v else "hide", 8127 requires=attr.get("requires", None), 8128 hideerror=True, 8129 _value=k, 8130 value=(str(k) in values)), 8131 label)) 8132 8133 input_index += 1 8134 opts.append(TR(tds)) 8135 8136 if opts: 8137 opts[-1][0][0]["hideerror"] = False 8138 return TABLE(*opts, **attr)
8139
8140 # ============================================================================= 8141 -class S3PasswordWidget(FormWidget):
8142 """ 8143 Widget for password fields, allows unmasking of passwords 8144 """ 8145
8146 - def __call__(self, field, value, **attributes):
8147 8148 T = current.T 8149 8150 tablename = field._tablename 8151 fieldname = field.name 8152 js_append = current.response.s3.js_global.append 8153 js_append('''i18n.password_view="%s"''' % T("View")) 8154 js_append('''i18n.password_mask="%s"''' % T("Mask")) 8155 8156 password_input = INPUT(_name = fieldname, 8157 _id = "%s_%s" % (tablename, fieldname), 8158 _type = "password", 8159 _value = value, 8160 requires = field.requires, 8161 ) 8162 password_unmask = A(T("View"), 8163 _class = "s3-unmask", 8164 _onclick = '''S3.unmask('%s','%s')''' % (tablename, 8165 fieldname), 8166 _id = "%s_%s_unmask" % (tablename, fieldname), 8167 ) 8168 return DIV(password_input, 8169 password_unmask, 8170 _class = "s3-password-widget", 8171 )
8172
8173 # ============================================================================= 8174 -class S3PhoneWidget(StringWidget):
8175 """ 8176 Extend the default Web2Py widget to ensure that the + is at the 8177 beginning not the end in RTL. 8178 Adds class to be acted upon by S3.js 8179 """ 8180
8181 - def __call__(self, field, value, **attributes):
8182 8183 default = dict( 8184 value = (value is not None and str(value)) or "", 8185 ) 8186 8187 attr = StringWidget._attributes(field, default, **attributes) 8188 attr["_class"] = "string phone-widget" 8189 8190 widget = INPUT(**attr) 8191 8192 return widget
8193
8194 # ============================================================================= 8195 -def s3_comments_widget(field, value, **attr):
8196 """ 8197 A smaller-than-normal textarea 8198 to be used by the s3.comments() & gis.desc_field Reusable fields 8199 """ 8200 8201 _id = attr.get("_id", "%s_%s" % (field._tablename, field.name)) 8202 8203 _name = attr.get("_name", field.name) 8204 8205 return TEXTAREA(_name = _name, 8206 _id = _id, 8207 _class = "comments %s" % (field.type), 8208 _placeholder = attr.get("_placeholder"), 8209 value = value, 8210 requires = field.requires)
8211
8212 # ============================================================================= 8213 -def s3_richtext_widget(field, value):
8214 """ 8215 A Rich Text field to be used by the CMS Post Body, etc 8216 - uses CKEditor 8217 - requires doc module loaded to be able to upload/browse Images 8218 """ 8219 8220 s3 = current.response.s3 8221 widget_id = "%s_%s" % (field._tablename, field.name) 8222 8223 # Load the scripts 8224 sappend = s3.scripts.append 8225 ckeditor = URL(c="static", f="ckeditor", args="ckeditor.js") 8226 sappend(ckeditor) 8227 adapter = URL(c="static", f="ckeditor", args=["adapters", 8228 "jquery.js"]) 8229 sappend(adapter) 8230 8231 table = current.s3db.table("doc_ckeditor") 8232 if table: 8233 # Doc module enabled: can upload/browse images 8234 url = '''filebrowserUploadUrl:'/%(appname)s/doc/ck_upload',filebrowserBrowseUrl:'/%(appname)s/doc/ck_browse',''' \ 8235 % dict(appname=current.request.application) 8236 else: 8237 # Doc module not enabled: cannot upload/browse images 8238 url = "" 8239 8240 # Toolbar options: http://docs.ckeditor.com/#!/guide/dev_toolbar 8241 js = '''var ck_config={toolbar:[['Format','Bold','Italic','-','NumberedList','BulletedList','-','Link','Unlink','-','Image','Table','-','PasteFromWord','-','Source','Maximize']],toolbarCanCollapse:false,%sremovePlugins:'elementspath'}''' \ 8242 % url 8243 s3.js_global.append(js) 8244 8245 js = '''$('#%s').ckeditor(ck_config)''' % widget_id 8246 s3.jquery_ready.append(js) 8247 8248 return TEXTAREA(_name=field.name, 8249 _id=widget_id, 8250 _class="richtext %s" % (field.type), 8251 value=value, 8252 requires=field.requires)
8253
8254 # ============================================================================= 8255 -def search_ac(r, **attr):
8256 """ 8257 JSON search method for S3AutocompleteWidget 8258 8259 @param r: the S3Request 8260 @param attr: request attributes 8261 """ 8262 8263 _vars = current.request.get_vars 8264 8265 # JQueryUI Autocomplete uses "term" instead of "value" 8266 # (old JQuery Autocomplete uses "q" instead of "value") 8267 value = _vars.term or _vars.value or _vars.q or None 8268 8269 # We want to do case-insensitive searches 8270 # (default anyway on MySQL/SQLite, but not PostgreSQL) 8271 value = value.lower().strip() 8272 8273 fieldname = _vars.get("field", "name") 8274 fieldname = str.lower(fieldname) 8275 filter = _vars.get("filter", "~") 8276 8277 resource = r.resource 8278 table = resource.table 8279 8280 limit = int(_vars.limit or 0) 8281 8282 from s3query import FS 8283 field = FS(fieldname) 8284 8285 # Default fields to return 8286 fields = ["id", fieldname] 8287 # Now using custom method 8288 #if resource.tablename == "org_site": 8289 # # Simpler to provide an exception case than write a whole new class 8290 # fields.append("instance_type") 8291 8292 if filter == "~": 8293 # Normal single-field Autocomplete 8294 query = (field.lower().like(value + "%")) 8295 8296 elif filter == "=": 8297 if field.type.split(" ")[0] in \ 8298 ["reference", "id", "float", "integer"]: 8299 # Numeric, e.g. Organizations' offices_by_org 8300 query = (field == value) 8301 else: 8302 # Text 8303 query = (field.lower() == value) 8304 8305 elif filter == "<": 8306 query = (field < value) 8307 8308 elif filter == ">": 8309 query = (field > value) 8310 8311 else: 8312 output = current.xml.json_message(False, 400, 8313 "Unsupported filter! Supported filters: ~, =, <, >") 8314 raise HTTP(400, body=output) 8315 8316 if "link" in _vars: 8317 link_filter = S3EmbeddedComponentWidget.link_filter_query(table, 8318 _vars.link, 8319 ) 8320 if link_filter: 8321 query &= link_filter 8322 8323 # Select only or exclude template records: 8324 # to only select templates: 8325 # ?template=<fieldname>.<value>, 8326 # e.g. ?template=template.true 8327 # to exclude templates: 8328 # ?template=~<fieldname>.<value> 8329 # e.g. ?template=~template.true 8330 if "template" in _vars: 8331 try: 8332 flag, val = _vars.template.split(".", 1) 8333 if flag[0] == "~": 8334 exclude = True 8335 flag = flag[1:] 8336 else: 8337 exclude = False 8338 ffield = table[flag] 8339 except: 8340 pass # ignore 8341 else: 8342 if str(ffield.type) == "boolean": 8343 if val.lower() == "true": 8344 val = True 8345 else: 8346 val = False 8347 if exclude: 8348 templates = (ffield != val) 8349 else: 8350 templates = (ffield == val) 8351 resource.add_filter(templates) 8352 8353 resource.add_filter(query) 8354 8355 output = None 8356 if filter == "~": 8357 MAX_SEARCH_RESULTS = current.deployment_settings.get_search_max_results() 8358 if (not limit or limit > MAX_SEARCH_RESULTS) and \ 8359 resource.count() > MAX_SEARCH_RESULTS: 8360 output = [ 8361 dict(label=str(current.T("There are more than %(max)s results, please input more characters.") % \ 8362 dict(max=MAX_SEARCH_RESULTS))) 8363 ] 8364 8365 if output is None: 8366 rows = resource.select(fields, 8367 start=0, 8368 limit=limit, 8369 orderby=field, 8370 as_rows=True) 8371 output = [] 8372 append = output.append 8373 for row in rows: 8374 record = {"id": row.id, 8375 fieldname: row[fieldname], 8376 } 8377 append(record) 8378 8379 current.response.headers["Content-Type"] = "application/json" 8380 return json.dumps(output, separators=SEPARATORS)
8381
8382 # ============================================================================= 8383 -class S3XMLContents(object):
8384 """ 8385 Renderer for db-stored XML contents (e.g. CMS) 8386 8387 Replaces {{page}} expressions inside the contents with local URLs. 8388 8389 {{page}} - gives the URL of the current page 8390 {{name:example}} - gives the URL of the current page with 8391 a query ?name=example (can add any number 8392 of query variables) 8393 {{c:org,f:organisation}} - c and f tokens override controller and 8394 function of the current page, in this 8395 example like /org/organisation 8396 {{args:arg,arg}} - override the current request's URL args 8397 (this should come last in the expression) 8398 {{noargs}} - strip all URL args 8399 8400 @note: does not check permissions for the result URLs 8401 """ 8402
8403 - def __init__(self, contents):
8404 """ 8405 Constructor 8406 8407 @param contents: the contents (string) 8408 """ 8409 8410 self.contents = contents
8411 8412 # ------------------------------------------------------------------------- 8457 8458 # -------------------------------------------------------------------------
8459 - def xml(self):
8460 """ Render the output """ 8461 8462 return re.sub(r"\{\{(.+?)\}\}", self.link, self.contents)
8463
8464 # ============================================================================= 8465 -class S3QuestionWidget(FormWidget):
8466 """ 8467 A Question widget which takes attributes 8468 of a typical question as input and converts 8469 it into a JSON 8470 """ 8471 8472 # -------------------------------------------------------------------------
8473 - def __call__(self, field, value, **attr):
8474 """ 8475 Widget builder 8476 8477 @param field: the Field 8478 @param value: the current value 8479 @param attributes: the HTML attributes for the widget 8480 """ 8481 8482 selector = attr.get("id") 8483 if not selector: 8484 if isinstance(field, Field): 8485 selector = str(field).replace(".", "_") 8486 else: 8487 selector = field.name.replace(".", "_") 8488 8489 # Field name 8490 name = attr.get("_name") 8491 8492 if not name: 8493 name = field.name 8494 8495 T = current.T 8496 request = current.request 8497 s3 = current.response.s3 8498 8499 # The actual hidden input containing the JSON of the fields 8500 real_input = INPUT(_id=selector, 8501 _name=name, 8502 _value=value, 8503 _type="hidden", 8504 ) 8505 8506 formstyle = s3.crud.formstyle 8507 8508 if value is None: 8509 value = "{}" 8510 8511 value = eval(value) 8512 8513 type_options = (("string", "String"), 8514 ("integer", "Integer"), 8515 ("float", "Float"), 8516 ("text", "Text"), 8517 ("object", "Object"), 8518 ("date", "Date"), 8519 ("time", "Time"), 8520 ("datetime", "DateTime"), 8521 ("reference", "Reference"), 8522 ("location", "Location"), 8523 ) 8524 8525 type_id = "%s_type" % selector 8526 8527 select_field = Field("type", requires=IS_IN_SET(type_options)) 8528 select_value = value.get("type", "") 8529 8530 # Used by OptionsWidget for creating DOM id for select input 8531 select_field.tablename = "dc_question_model" 8532 select = OptionsWidget.widget(select_field, select_value) 8533 8534 # Retrieve value of checkboxes 8535 multiple = value.get("multiple", False) 8536 if multiple == "true": 8537 multiple = True 8538 else: 8539 multiple = False 8540 8541 is_required = value.get("is_required", False) 8542 if is_required == "true": 8543 is_required = True 8544 else: 8545 is_required = False 8546 8547 # Render visual components 8548 components = {} 8549 manual_input = self._input 8550 8551 components["type"] = ("Type: ", select, type_id) 8552 8553 components["is_required"] = manual_input(selector, 8554 "is_required", 8555 is_required, 8556 T("Is Required"), 8557 "checkbox") 8558 8559 components["description"] = manual_input(selector, 8560 "description", 8561 value.get("description", ""), 8562 T("Description")) 8563 8564 components["default_answer"] = manual_input(selector, 8565 "defaultanswer", 8566 value.get("defaultanswer", ""), 8567 T("Default Answer")) 8568 8569 components["max"] = manual_input(selector, 8570 "max", 8571 value.get("max", ""), 8572 T("Maximum")) 8573 8574 components["min"] = manual_input(selector, 8575 "min", 8576 value.get("min", ""), 8577 T("Minimum")) 8578 8579 components["filter"] = manual_input(selector, 8580 "filter", 8581 value.get("filter", ""), 8582 T("Filter")) 8583 8584 components["reference"] = manual_input(selector, 8585 "reference", 8586 value.get("reference", ""), 8587 T("Reference")) 8588 8589 components["represent"] = manual_input(selector, 8590 "represent", 8591 value.get("represent", ""), 8592 T("Represent")) 8593 8594 components["location"] = manual_input(selector, 8595 "location", 8596 value.get("location", "[]"), 8597 T("Location Fields")) 8598 8599 components["options"] = manual_input(selector, 8600 "options", 8601 value.get("options", "[]"), 8602 T("Options")) 8603 8604 components["multiple"] = manual_input(selector, 8605 "multiple", 8606 multiple, 8607 T("Multiple Options"), 8608 "checkbox") 8609 8610 # Load the widget script 8611 scripts = s3.scripts 8612 script_dir = "/%s/static/scripts" % request.application 8613 8614 script = "%s/S3/s3.ui.question.js" % script_dir 8615 if script not in scripts: 8616 scripts.append(script) 8617 8618 # Call the widget 8619 script = '''$('#%(widget_id)s').addQuestion()''' % \ 8620 {"widget_id": "dc_question_model"} 8621 8622 s3.jquery_ready.append(script) 8623 8624 # Get the layout for visible components 8625 visible_components = self._layout(components, formstyle=formstyle) 8626 8627 return TAG[""](real_input, 8628 visible_components)
8629 8630 # -------------------------------------------------------------------------
8631 - def _layout(self, components, formstyle=None):
8632 """ 8633 Overall layout for visible components 8634 8635 @param components: the components as dict 8636 @param formstyle: the formstyle (falls back to CRUD formstyle) 8637 """ 8638 8639 if formstyle is None: 8640 formstyle = current.response.s3.crud.formstyle 8641 8642 # Test the formstyle 8643 row = formstyle("test", "test", "test", "test") 8644 8645 tuple_rows = isinstance(row, tuple) 8646 8647 inputs = TAG[""]() 8648 for name in ("type", "is_required", "description", "default_answer", 8649 "max", "min", "filter", "reference", "represent", 8650 "location", "options", "multiple"): 8651 if name in components: 8652 label, widget, input_id = components[name] 8653 formrow = formstyle("%s__row" % input_id, 8654 label, 8655 widget, 8656 "") 8657 8658 if tuple_rows: 8659 inputs.append(formrow[0]) 8660 inputs.append(formrow[1]) 8661 else: 8662 inputs.append(formrow) 8663 return inputs
8664 8665 # -------------------------------------------------------------------------
8666 - def _input(self, 8667 fieldname, 8668 name, 8669 value, 8670 label, 8671 _type="text"):
8672 """ 8673 Render a text input with given attributes 8674 8675 @param fieldname: the field name (for ID construction) 8676 @param name: the name for the input field 8677 @param value: the initial value for the input 8678 @param label: the label for the input 8679 @param hidden: render hidden 8680 8681 @return: a tuple (label, widget, id, hidden) 8682 """ 8683 8684 input_id = "%s_%s" % (fieldname, name) 8685 8686 _label = LABEL("%s: " % label, _for=input_id) 8687 8688 if isinstance(value, unicode): 8689 value = value.encode("utf-8") 8690 8691 # If the input is of type checkbox 8692 if name in ("is_required", "multiple"): 8693 widget = INPUT(_type=_type, 8694 _id=input_id, 8695 value=value) 8696 else: 8697 widget = INPUT(_type=_type, 8698 _id=input_id, 8699 _value=value) 8700 8701 return (_label, widget, input_id)
8702
8703 # ============================================================================= 8704 -class S3TagCheckboxWidget(FormWidget):
8705 """ 8706 Simple widget to use a checkbox to toggle a string-type Field 8707 between two values (default "Y"|"N"). 8708 Like an S3BooleanWidget but real Booleans cannot be stored in strings. 8709 Designed for use with tag.value 8710 8711 NB it is usually better to use a boolean Field with a context-specific 8712 representation function than this. 8713 8714 NB make sure the field validator accepts the configured on/off values, 8715 e.g. IS_IN_SET(("Y", "N")) (also for consistency with imports) 8716 8717 NB when using this with a filtered key-value-component (e.g. 8718 pr_person_tag), make the filtered component multiple=False and 8719 embed *.value as subtable-field (do not use S3SQLInlineComponent) 8720 """ 8721
8722 - def __init__(self, on="Y", off="N"):
8723 """ 8724 Constructor 8725 8726 @param on: the value of the tag for checkbox=on 8727 @param off: the value of the tag for checkbox=off 8728 """ 8729 8730 self.on = on 8731 self.off = off
8732 8733 # -------------------------------------------------------------------------
8734 - def __call__(self, field, value, **attributes):
8735 """ 8736 Widget construction 8737 8738 @param field: the Field 8739 @param value: the current (or default) value 8740 @param attributes: overrides for default attributes 8741 """ 8742 8743 defaults = {"_type": "checkbox", 8744 "value": str(value) == self.on, 8745 "requires": self.requires, 8746 } 8747 attr = self._attributes(field, defaults, **attributes) 8748 return INPUT(**attr)
8749 8750 # -------------------------------------------------------------------------
8751 - def requires(self, value):
8752 """ 8753 Input-validator to convert the checkbox value into the 8754 corresponding tag value 8755 8756 @param value: the checkbox value ("on" if checked) 8757 """ 8758 8759 v = self.on if value == "on" else self.off 8760 return v, None
8761
8762 # ============================================================================= 8763 -class ICON(I):
8764 """ 8765 Helper class to render <i> tags for icons, mapping abstract 8766 icon names to theme-specific CSS classes. The standard icon 8767 set can be configured using settings.ui.icons 8768 8769 e.g. ICON("book"), gives: 8770 - font-awesome: <i class="icon icon-book"> 8771 - foundation: <i class="fi-book"> 8772 8773 Standard sets are defined below. 8774 8775 Additional icons (beyond the standard set) can be configured 8776 per deployment (settings.ui.custom_icons). 8777 8778 If <i class=""> is not suitable for the CSS, a custom HTML 8779 layout can be configured as settings.ui.icon_layout. See 8780 S3Config for more details. 8781 8782 @todo: apply in widgets/crud/profile+datalist layouts etc. 8783 @todo: better abstract names for the icons to indicate what they 8784 symbolize rather than what they depict, e.g. "sitemap" is 8785 typically used to symbolize an organisation => rename into 8786 "organisation". 8787 """ 8788 8789 # ------------------------------------------------------------------------- 8790 # Standard icon sets, 8791 # - "_base" can be used to define a common CSS class for all icons 8792 # 8793 icons = { 8794 # Font-Awesome 4 8795 # http://fontawesome.io/icons/ 8796 "font-awesome": { 8797 "_base": "fa", 8798 "active": "fa-check", 8799 "activity": "fa-cogs", 8800 "add": "fa-plus", 8801 "administration": "fa-cog", 8802 "alert": "fa-bell", 8803 "arrow-down": "fa-arrow-down", 8804 "assessment": "fa-bar-chart", 8805 "asset": "fa-fire-extinguisher", 8806 "attachment": "fa-paperclip", 8807 "bar-chart": "fa-bar-chart", 8808 "book": "fa-book", 8809 "bookmark": "fa-bookmark", 8810 "bookmark-empty": "fa-bookmark-o", 8811 "briefcase": "fa-briefcase", 8812 "calendar": "fa-calendar", 8813 "caret-right": "fa-caret-right", 8814 "certificate": "fa-certificate", 8815 "comment-alt": "fa-comment-o", 8816 "commit": "fa-check-square-o", 8817 "delete": "fa-trash", 8818 "delivery": "fa-thumbs-up", 8819 "deploy": "fa-plus", 8820 "deployed": "fa-check", 8821 "done": "fa-check", 8822 "down": "fa-caret-down", 8823 "edit": "fa-edit", 8824 "event": "fa-bolt", 8825 "exclamation": "fa-exclamation", 8826 "facebook": "fa-facebook", 8827 "facility": "fa-home", 8828 "file": "fa-file", 8829 "file-alt": "fa-file-alt", 8830 "flag": "fa-flag", 8831 "flag-alt": "fa-flag-o", 8832 "folder-open-alt": "fa-folder-open-o", 8833 "fullscreen": "fa-fullscreen", 8834 "globe": "fa-globe", 8835 "goods": "fa-cubes", 8836 "group": "fa-group", 8837 "hint": "fa-hand-o-right", 8838 "home": "fa-home", 8839 "inactive": "fa-check-empty", 8840 "incident": "fa-bolt", 8841 "link": "fa-external-link", 8842 "list": "fa-list", 8843 "location": "fa-globe", 8844 "mail": "fa-envelope-o", 8845 "map-marker": "fa-map-marker", 8846 "move": "fa-arrows", 8847 "news": "fa-info", 8848 "offer": "fa-truck", 8849 "organisation": "fa-institution", 8850 "org-network": "fa-umbrella", 8851 "other": "fa-circle", 8852 "paper-clip": "fa-paperclip", 8853 "pause": "fa-pause", 8854 "pencil": "fa-pencil", 8855 "phone": "fa-phone", 8856 "play": "fa-play", 8857 "plus": "fa-plus", 8858 "plus-sign": "fa-plus-sign", 8859 "print": "fa-print", 8860 "project": "fa-dashboard", 8861 "radio": "fa-microphone", 8862 "remove": "fa-remove", 8863 "request": "fa-flag", 8864 "responsibility": "fa-briefcase", 8865 "return": "fa-arrow-left", 8866 "rss": "fa-rss", 8867 "sent": "fa-check", 8868 "settings": "fa-wrench", 8869 "share": "fa-share-alt", 8870 "shipment": "fa-truck", 8871 "site": "fa-home", 8872 "skype": "fa-skype", 8873 "staff": "fa-user", 8874 "star": "fa-star", 8875 "stop": "fa-stop", 8876 "table": "fa-table", 8877 "tag": "fa-tag", 8878 "tags": "fa-tags", 8879 "tasks": "fa-tasks", 8880 "time": "fa-time", 8881 "truck": "fa-truck", 8882 "twitter": "fa-twitter", 8883 "unsent": "fa-times", 8884 "up": "fa-caret-up", 8885 "upload": "fa-upload", 8886 "user": "fa-user", 8887 "volunteer": "fa-hand-paper-o", 8888 "wrench": "fa-wrench", 8889 "zoomin": "fa-zoomin", 8890 "zoomout": "fa-zoomout", 8891 }, 8892 # Foundation Icon Fonts 3 8893 # http://zurb.com/playground/foundation-icon-fonts-3 8894 "foundation": { 8895 "active": "fi-check", 8896 "activity": "fi-price-tag", 8897 "add": "fi-plus", 8898 "arrow-down": "fi-arrow-down", 8899 "attachment": "fi-paperclip", 8900 "bar-chart": "fi-graph-bar", 8901 "book": "fi-book", 8902 "bookmark": "fi-bookmark", 8903 "bookmark-empty": "fi-bookmark-empty", 8904 "calendar": "fi-calendar", 8905 "caret-right": "fi-play", 8906 "certificate": "fi-burst", 8907 "comment-alt": "fi-comment", 8908 "commit": "fi-check", 8909 "delete": "fi-trash", 8910 "deploy": "fi-plus", 8911 "deployed": "fi-check", 8912 "edit": "fi-page-edit", 8913 "exclamation": "fi-alert", 8914 "facebook": "fi-social-facebook", 8915 "facility": "fi-home", 8916 "file": "fi-page-filled", 8917 "file-alt": "fi-page", 8918 "flag": "fi-flag", 8919 "flag-alt": "fi-flag", 8920 "folder-open-alt": "fi-folder", 8921 "fullscreen": "fi-arrows-out", 8922 "globe": "fi-map", 8923 "group": "fi-torsos-all", 8924 "home": "fi-home", 8925 "inactive": "fi-x", 8926 "link": "fi-web", 8927 "list": "fi-list", 8928 "location": "fi-map", 8929 "mail": "fi-mail", 8930 "map-marker": "fi-marker", 8931 "offer": "fi-burst", 8932 "organisation": "fi-torsos-all", 8933 "org-network": "fi-asterisk", 8934 "other": "fi-asterisk", 8935 "paper-clip": "fi-paperclip", 8936 "pause": "fi-pause", 8937 "pencil": "fi-pencil", 8938 "phone": "fi-telephone", 8939 "play": "fi-play", 8940 "plus": "fi-plus", 8941 "plus-sign": "fi-plus", 8942 "print": "fi-print", 8943 "radio": "fi-microphone", 8944 "remove": "fi-x", 8945 "request": "fi-flag", 8946 "responsibility": "fi-sheriff-badge", 8947 "return": "fi-arrow-left", 8948 "rss": "fi-rss", 8949 "sent": "fi-check", 8950 "settings": "fi-wrench", 8951 "share": "fi-share", 8952 "site": "fi-home", 8953 "skype": "fi-social-skype", 8954 "star": "fi-star", 8955 "stop": "fi-stop", 8956 "table": "fi-list-thumbnails", 8957 "tag": "fi-price-tag", 8958 "tags": "fi-pricetag-multiple", 8959 "tasks": "fi-clipboard-notes", 8960 "time": "fi-clock", 8961 "twitter": "fi-social-twitter", 8962 "unsent": "fi-x", 8963 "upload": "fi-upload", 8964 "user": "fi-torso", 8965 "zoomin": "fi-zoom-in", 8966 "zoomout": "fi-zoom-out", 8967 }, 8968 # Font-Awesome 3 8969 # http://fontawesome.io/3.2.1/icons/ 8970 "font-awesome3": { 8971 "_base": "icon", 8972 "active": "icon-check", 8973 "activity": "icon-tag", 8974 "add": "icon-plus", 8975 "administration": "icon-cog", 8976 "arrow-down": "icon-arrow-down", 8977 "attachment": "icon-paper-clip", 8978 "bar-chart": "icon-bar-chart", 8979 "book": "icon-book", 8980 "bookmark": "icon-bookmark", 8981 "bookmark-empty": "icon-bookmark-empty", 8982 "briefcase": "icon-briefcase", 8983 "calendar": "icon-calendar", 8984 "caret-right": "icon-caret-right", 8985 "certificate": "icon-certificate", 8986 "comment-alt": "icon-comment-alt", 8987 "commit": "icon-truck", 8988 "delete": "icon-trash", 8989 "deploy": "icon-plus", 8990 "deployed": "icon-ok", 8991 "down": "icon-caret-down", 8992 "edit": "icon-edit", 8993 "exclamation": "icon-exclamation", 8994 "facebook": "icon-facebook", 8995 "facility": "icon-home", 8996 "file": "icon-file", 8997 "file-alt": "icon-file-alt", 8998 "flag": "icon-flag", 8999 "flag-alt": "icon-flag-alt", 9000 "folder-open-alt": "icon-folder-open-alt", 9001 "fullscreen": "icon-fullscreen", 9002 "globe": "icon-globe", 9003 "group": "icon-group", 9004 "home": "icon-home", 9005 "inactive": "icon-check-empty", 9006 "link": "icon-external-link", 9007 "list": "icon-list", 9008 "location": "icon-globe", 9009 "mail": "icon-envelope-alt", 9010 "map-marker": "icon-map-marker", 9011 "offer": "icon-truck", 9012 "organisation": "icon-sitemap", 9013 "org-network": "icon-umbrella", 9014 "other": "icon-circle", 9015 "paper-clip": "icon-paper-clip", 9016 "pause": "icon-pause", 9017 "pencil": "icon-pencil", 9018 "phone": "icon-phone", 9019 "play": "icon-play", 9020 "plus": "icon-plus", 9021 "plus-sign": "icon-plus-sign", 9022 "print": "icon-print", 9023 "radio": "icon-microphone", 9024 "remove": "icon-remove", 9025 "request": "icon-flag", 9026 "responsibility": "icon-briefcase", 9027 "return": "icon-arrow-left", 9028 "rss": "icon-rss", 9029 "sent": "icon-ok", 9030 "settings": "icon-wrench", 9031 "share": "icon-share", 9032 "site": "icon-home", 9033 "skype": "icon-skype", 9034 "star": "icon-star", 9035 "stop": "icon-stop", 9036 "table": "icon-table", 9037 "tag": "icon-tag", 9038 "tags": "icon-tags", 9039 "tasks": "icon-tasks", 9040 "time": "icon-time", 9041 "truck": "icon-truck", 9042 "twitter": "icon-twitter", 9043 "unsent": "icon-remove", 9044 "up": "icon-caret-up", 9045 "upload": "icon-upload-alt", 9046 "user": "icon-user", 9047 "wrench": "icon-wrench", 9048 "zoomin": "icon-zoomin", 9049 "zoomout": "icon-zoomout", 9050 }, 9051 } 9052 9053 # -------------------------------------------------------------------------
9054 - def __init__(self, name, **attr):
9055 """ 9056 Constructor 9057 9058 @param name: the abstract icon name 9059 @param attr: additional HTML attributes (optional) 9060 """ 9061 9062 self.name = name 9063 super(ICON, self).__init__(" ", **attr)
9064 9065 # -------------------------------------------------------------------------
9066 - def xml(self):
9067 """ 9068 Render this instance as XML 9069 """ 9070 9071 # Custom layout? 9072 layout = current.deployment_settings.get_ui_icon_layout() 9073 if layout: 9074 return layout(self) 9075 9076 css_class = self.css_class(self.name) 9077 9078 if css_class: 9079 self.add_class(css_class) 9080 9081 return super(ICON, self).xml()
9082 9083 # ------------------------------------------------------------------------- 9084 @classmethod
9085 - def css_class(cls, name):
9086 9087 settings = current.deployment_settings 9088 fallback = "font-awesome" 9089 9090 # Lookup the default set 9091 icons = cls.icons 9092 default_set = settings.get_ui_icons() 9093 default = icons[fallback] 9094 if default_set != fallback: 9095 default.pop("_base", None) 9096 default.update(icons.get(default_set, {})) 9097 9098 # Custom set? 9099 custom = settings.get_ui_custom_icons() 9100 9101 if custom and name in custom: 9102 css = custom[name] 9103 base = custom.get("_base") 9104 elif name in default: 9105 css = default[name] 9106 base = default.get("_base") 9107 else: 9108 css = name 9109 base = None 9110 9111 return " ".join([c for c in (css, base) if c])
9112 9113 # END ========================================================================= 9114