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

Source Code for Module s3.s3validators

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ Custom Validators 
   4   
   5      @requires: U{B{I{gluon}} <http://web2py.com>} 
   6   
   7      @copyright: (c) 2010-2019 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  """ 
  32   
  33  __all__ = ("single_phone_number_pattern", 
  34             "multi_phone_number_pattern", 
  35             "s3_single_phone_requires", 
  36             "s3_phone_requires", 
  37             "IS_ACL", 
  38             "IS_COMBO_BOX", 
  39             "IS_DYNAMIC_FIELDNAME", 
  40             "IS_DYNAMIC_FIELDTYPE", 
  41             "IS_FLOAT_AMOUNT", 
  42             "IS_HTML_COLOUR", 
  43             "IS_INT_AMOUNT", 
  44             "IS_IN_SET_LAZY", 
  45             "IS_ISO639_2_LANGUAGE_CODE", 
  46             "IS_JSONS3", 
  47             "IS_LAT", 
  48             "IS_LON", 
  49             "IS_LAT_LON", 
  50             "IS_LOCATION", 
  51             "IS_ONE_OF", 
  52             "IS_ONE_OF_EMPTY", 
  53             "IS_ONE_OF_EMPTY_SELECT", 
  54             "IS_NOT_ONE_OF", 
  55             "IS_PERSON_GENDER", 
  56             "IS_PHONE_NUMBER", 
  57             "IS_PHONE_NUMBER_MULTI", 
  58             "IS_PROCESSED_IMAGE", 
  59             "IS_UTC_DATETIME", 
  60             "IS_UTC_DATE", 
  61             "IS_UTC_OFFSET", 
  62             "QUANTITY_INV_ITEM", 
  63             ) 
  64   
  65  import datetime 
  66  import json 
  67  import re 
  68   
  69  from gluon import current, IS_FLOAT_IN_RANGE, IS_INT_IN_RANGE, IS_IN_SET, \ 
  70                    IS_MATCH, IS_NOT_IN_DB 
  71  from gluon.storage import Storage 
  72  from gluon.validators import Validator 
  73   
  74  from s3datetime import S3DateTime 
  75  from s3utils import s3_orderby_fields, s3_str, s3_unicode 
  76   
  77  DEFAULT = lambda: None 
  78  JSONERRORS = (NameError, TypeError, ValueError, AttributeError, KeyError) 
  79  SEPARATORS = (",", ":") 
80 81 -def translate(text):
82 if text is None: 83 return None 84 elif isinstance(text, (str, unicode)): 85 if hasattr(current, "T"): 86 return str(current.T(text)) 87 return str(text)
88
89 -def options_sorter(x, y):
90 return (s3_unicode(x[1]).upper() > s3_unicode(y[1]).upper() and 1) or -1
91 92 # ----------------------------------------------------------------------------- 93 # Phone number requires 94 # Multiple phone numbers can be separated by comma, slash, semi-colon. 95 # (Semi-colon appears in Brazil OSM data.) 96 # @ToDo: Need to beware of separators used inside phone numbers 97 # (e.g. 555-1212, ext 9), so may need fancier validation if we see that. 98 # @ToDo: Add tooltip giving list syntax, and warning against above. 99 # (Current use is in importing OSM files, so isn't interactive.) 100 # @ToDo: Code that should only have a single # should use 101 # s3_single_phone_requires. Check what messaging assumes. 102 phone_number_pattern = r"\+?\s*[\s\-\.\(\)\d]+(?:(?: x| ext)\s?\d{1,5})?" 103 single_phone_number_pattern = "%s$" % phone_number_pattern 104 multi_phone_number_pattern = r"%s(\s*(,|/|;)\s*%s)*$" % (phone_number_pattern, 105 phone_number_pattern) 106 107 s3_single_phone_requires = IS_MATCH(single_phone_number_pattern) 108 s3_phone_requires = IS_MATCH(multi_phone_number_pattern, 109 error_message="Invalid phone number!")
110 111 # ============================================================================= 112 -class IS_JSONS3(Validator):
113 """ 114 Similar to web2py's IS_JSON validator, but extended to handle 115 single quotes in dict keys (=invalid JSON) from CSV imports. 116 117 Example: 118 119 INPUT(_type='text', _name='name', requires=IS_JSONS3()) 120 121 >>> IS_JSONS3()('{"a": 100}') 122 ({u'a': 100}, None) 123 124 >>> IS_JSONS3()('spam1234') 125 ('spam1234', 'invalid json') 126 """ 127
128 - def __init__(self, 129 native_json=False, 130 error_message="Invalid JSON"):
131 """ 132 Constructor 133 134 @param native_json: return the JSON string rather than 135 a Python object (e.g. when the field 136 is "string" type rather than "json") 137 @param error_message: the error message 138 """ 139 140 self.native_json = native_json 141 self.error_message = error_message
142 143 # -------------------------------------------------------------------------
144 - def __call__(self, value):
145 """ 146 Validator, validates a string and converts it into db format 147 """ 148 149 error = lambda v, e: (v, "%s: %s" % (current.T(self.error_message), e)) 150 151 if current.response.s3.bulk: 152 # CSV import produces invalid JSON (single quotes), 153 # which would still be valid Python though, so try 154 # using ast to decode, then re-dumps as valid JSON: 155 import ast 156 try: 157 value_ = json.dumps(ast.literal_eval(value), 158 separators = SEPARATORS, 159 ) 160 except JSONERRORS + (SyntaxError,), e: 161 return error(value, e) 162 if self.native_json: 163 return (value_, None) 164 else: 165 return (json.loads(value_), None) 166 else: 167 # Coming from UI, so expect valid JSON 168 try: 169 if self.native_json: 170 json.loads(value) # raises error in case of malformed JSON 171 return (value, None) # the serialized value is not passed 172 else: 173 return (json.loads(value), None) 174 except JSONERRORS, e: 175 return error(value, e)
176 177 # -------------------------------------------------------------------------
178 - def formatter(self, value):
179 """ 180 Formatter, converts the db format into a string 181 """ 182 183 if value is None or \ 184 self.native_json and isinstance(value, basestring): 185 return value 186 else: 187 return json.dumps(value, separators = SEPARATORS)
188
189 # ============================================================================= 190 -class IS_LAT(Validator):
191 """ 192 example: 193 194 INPUT(_type="text", _name="name", requires=IS_LAT()) 195 196 Latitude has to be in decimal degrees between -90 & 90 197 - we attempt to convert DMS format into decimal degrees 198 """ 199
200 - def __init__(self, 201 error_message = "Latitude/Northing should be between -90 & 90!" 202 ):
203 204 self.minimum = -90 205 self.maximum = 90 206 self.error_message = error_message 207 # Tell s3_mark_required that this validator doesn't accept NULL values 208 self.mark_required = True
209 210 # -------------------------------------------------------------------------
211 - def __call__(self, value):
212 try: 213 value = float(value) 214 except ValueError: 215 # DMS format 216 pass 217 else: 218 if self.minimum <= value <= self.maximum: 219 return (value, None) 220 else: 221 return (value, self.error_message) 222 223 pattern = re.compile(r"^[0-9]{,3}[\D\W][0-9]{,3}[\D\W][0-9]+$") 224 if not pattern.match(value): 225 return (value, self.error_message) 226 else: 227 val = [] 228 val.append(value) 229 sep = [] 230 count = 0 231 for i in val[0]: 232 try: 233 int(i) 234 except ValueError: 235 sep.append(count) 236 count += 1 237 sec = "" 238 posn = sep[1] 239 while posn != (count-1): 240 # join the numbers for seconds 241 sec = sec + val[0][posn+1] 242 posn += 1 243 posn2 = sep[0] 244 mins = "" 245 while posn2 != (sep[1]-1): 246 # join the numbers for minutes 247 mins = mins + val[0][posn2+1] 248 posn2 += 1 249 deg = "" 250 posn3 = 0 251 while posn3 != (sep[0]): 252 # join the numbers for degree 253 deg = deg + val[0][posn3] 254 posn3 += 1 255 e = int(sec) / 60 # formula to get back decimal degree 256 f = int(mins) + e # formula 257 g = int(f) / 60 # formula 258 value = int(deg) + g 259 return (value, None)
260
261 # ============================================================================= 262 -class IS_LON(IS_LAT):
263 """ 264 example: 265 266 INPUT(_type="text", _name="name", requires=IS_LON()) 267 268 Longitude has to be in decimal degrees between -180 & 180 269 - we attempt to convert DMS format into decimal degrees 270 """ 271
272 - def __init__(self, 273 error_message = "Longitude/Easting should be between -180 & 180!" 274 ):
275 276 super(IS_LON, self).__init__(error_message=error_message) 277 278 self.minimum = -180 279 self.maximum = 180
280
281 # ============================================================================= 282 -class IS_LAT_LON(Validator):
283 """ 284 Designed for use within the S3LocationLatLonWidget. 285 For Create forms, this will create a new location from the additional fields 286 For Update forms, this will check that we have a valid location_id FK and update any changes 287 288 @ToDo: Audit 289 """ 290
291 - def __init__(self, 292 field, 293 ):
294 295 self.field = field 296 # Tell s3_mark_required that this validator doesn't accept NULL values 297 self.mark_required = True
298 299 # -------------------------------------------------------------------------
300 - def __call__(self, value):
301 302 if current.response.s3.bulk: 303 # Pointless in imports 304 return (value, None) 305 306 selector = str(self.field).replace(".", "_") 307 post_vars = current.request.post_vars 308 lat = post_vars.get("%s_lat" % selector, None) 309 if lat == "": 310 lat = None 311 lon = post_vars.get("%s_lon" % selector, None) 312 if lon == "": 313 lon = None 314 315 if lat is None or lon is None: 316 # We don't accept None 317 return (value, current.T("Latitude and Longitude are required")) 318 319 # Check Lat 320 lat, error = IS_LAT()(lat) 321 if error: 322 return (value, error) 323 324 # Check Lon 325 lon, error = IS_LON()(lon) 326 if error: 327 return (value, error) 328 329 if value: 330 # update 331 db = current.db 332 db(db.gis_location.id == value).update(lat=lat, lon=lon) 333 else: 334 # create 335 value = current.db.gis_location.insert(lat=lat, lon=lon) 336 337 # OK 338 return (value, None)
339
340 # ============================================================================= 341 -class IS_NUMBER(object):
342 """ 343 Used by s3data.py to wrap IS_INT_AMOUNT & IS_FLOAT_AMOUNT 344 """ 345 346 # ------------------------------------------------------------------------- 347 @staticmethod
348 - def represent(number, precision=2):
349 if number is None: 350 return "" 351 352 if isinstance(number, int): 353 return IS_INT_AMOUNT.represent(number) 354 elif isinstance(number, float): 355 return IS_FLOAT_AMOUNT.represent(number, precision) 356 else: 357 return number
358
359 # ============================================================================= 360 -class IS_INT_AMOUNT(IS_INT_IN_RANGE):
361 """ 362 Validation, widget and representation of 363 integer-values with thousands-separators 364 """ 365
366 - def __init__(self, 367 minimum=None, 368 maximum=None, 369 error_message=None, 370 ):
371 372 IS_INT_IN_RANGE.__init__(self, 373 minimum=minimum, 374 maximum=maximum, 375 error_message=error_message, 376 )
377 378 # -------------------------------------------------------------------------
379 - def __call__(self, value):
380 381 thousands_sep = current.deployment_settings.get_L10n_thousands_separator() 382 if thousands_sep: 383 value = s3_str(value).replace(thousands_sep, "") 384 385 return IS_INT_IN_RANGE.__call__(self, value)
386 387 # ------------------------------------------------------------------------- 388 @staticmethod
389 - def represent(number):
390 """ 391 Change the format of the number depending on the language 392 Based on https://code.djangoproject.com/browser/django/trunk/django/utils/numberformat.py 393 """ 394 395 if number is None: 396 return "" 397 try: 398 intnumber = int(number) 399 except ValueError: 400 intnumber = number 401 402 settings = current.deployment_settings 403 THOUSAND_SEPARATOR = settings.get_L10n_thousands_separator() 404 NUMBER_GROUPING = settings.get_L10n_thousands_grouping() 405 406 # The negative/positive sign for the number 407 if float(number) < 0: 408 sign = "-" 409 else: 410 sign = "" 411 412 str_number = str(intnumber) 413 414 if str_number[0] == "-": 415 str_number = str_number[1:] 416 417 # Walk backwards over the integer part, inserting the separator as we go 418 int_part_gd = "" 419 for cnt, digit in enumerate(str_number[::-1]): 420 if cnt and not cnt % NUMBER_GROUPING: 421 int_part_gd += THOUSAND_SEPARATOR 422 int_part_gd += digit 423 int_part = int_part_gd[::-1] 424 425 return sign + int_part
426 427 # ------------------------------------------------------------------------- 428 @staticmethod
429 - def widget(f, v, **attributes):
430 from gluon.sqlhtml import StringWidget 431 attr = Storage(attributes) 432 classes = attr.get("_class", "").split(" ") 433 classes = " ".join([c for c in classes if c != "integer"]) 434 _class = "%s int_amount" % classes 435 attr.update(_class=_class) 436 return StringWidget.widget(f, v, **attr)
437
438 # ============================================================================= 439 -class IS_FLOAT_AMOUNT(IS_FLOAT_IN_RANGE):
440 """ 441 Validation, widget and representation of 442 float-values with thousands-separators 443 """ 444
445 - def __init__(self, 446 minimum=None, 447 maximum=None, 448 error_message=None, 449 dot=None, 450 ):
451 452 if dot is None: 453 dot = current.deployment_settings.get_L10n_decimal_separator() 454 455 IS_FLOAT_IN_RANGE.__init__(self, 456 minimum=minimum, 457 maximum=maximum, 458 error_message=error_message, 459 dot=dot, 460 )
461 462 # -------------------------------------------------------------------------
463 - def __call__(self, value):
464 465 thousands_sep = current.deployment_settings.get_L10n_thousands_separator() 466 if thousands_sep and isinstance(value, basestring): 467 value = s3_unicode(value).replace(thousands_sep, "") \ 468 .encode("utf-8") 469 470 return IS_FLOAT_IN_RANGE.__call__(self, value)
471 472 # ------------------------------------------------------------------------- 473 @staticmethod
474 - def represent(number, precision=None, fixed=False):
475 """ 476 Change the format of the number depending on the language 477 Based on https://code.djangoproject.com/browser/django/trunk/django/utils/numberformat.py 478 479 @param number: the number 480 @param precision: the number of decimal places to show 481 @param fixed: show decimal places even if the decimal part is 0 482 """ 483 484 if number is None: 485 return "" 486 487 DECIMAL_SEPARATOR = current.deployment_settings.get_L10n_decimal_separator() 488 489 if precision is not None: 490 str_number = format(number, ".0%df" % precision) 491 else: 492 # Default to any precision 493 str_number = format(number, "f").rstrip("0") \ 494 .rstrip(DECIMAL_SEPARATOR) 495 496 if "." in str_number: 497 int_part, dec_part = str_number.split(".") 498 else: 499 int_part, dec_part = str_number, "" 500 501 if dec_part and int(dec_part) == 0 and not fixed: 502 # Omit decimal part if zero 503 dec_part = "" 504 505 if dec_part: 506 dec_part = DECIMAL_SEPARATOR + dec_part 507 508 int_part = IS_INT_AMOUNT.represent(int(int_part)) 509 510 return int_part + dec_part
511 512 # ------------------------------------------------------------------------- 513 @staticmethod
514 - def widget(f, v, **attributes):
515 from gluon.sqlhtml import StringWidget 516 attr = Storage(attributes) 517 classes = attr.get("_class", "").split(" ") 518 classes = " ".join([c for c in classes if c != "double"]) 519 _class = "%s float_amount" % classes 520 attr.update(_class=_class) 521 return StringWidget.widget(f, v, **attr)
522
523 # ============================================================================= 524 -class IS_HTML_COLOUR(IS_MATCH):
525 """ 526 example:: 527 528 INPUT(_type="text", _name="name", requires=IS_HTML_COLOUR()) 529 """ 530
531 - def __init__(self, 532 error_message="must be a 6 digit hex code! (format: rrggbb)" 533 ):
534 IS_MATCH.__init__(self, "^[0-9a-fA-F]{6}$", error_message)
535 536 # ============================================================================= 537 regex1 = re.compile(r"[\w_]+\.[\w_]+") 538 regex2 = re.compile(r"%\((?P<name>[^\)]+)\)s")
539 540 -class IS_ONE_OF_EMPTY(Validator):
541 """ 542 Filtered version of IS_IN_DB(): 543 544 validates a given value as key of another table, filtered by the 545 'filterby' field for one of the 'filter_opts' options 546 (=a selective IS_IN_DB()) 547 548 NB Filtering isn't active in GQL. 549 550 For the dropdown representation: 551 552 'label' can be a string template for the record, or a set of field 553 names of the fields to be used as option labels, or a function or 554 lambda to create an option label from the respective record (which 555 has to return a string, of course). The function will take the 556 record as an argument. 557 558 No 'options' method as designed to be called next to an 559 Autocomplete field so don't download a large dropdown 560 unnecessarily. 561 """ 562
563 - def __init__(self, 564 dbset, 565 field, 566 label=None, 567 filterby=None, 568 filter_opts=None, 569 not_filterby=None, 570 not_filter_opts=None, 571 realms=None, 572 updateable=False, 573 instance_types=None, 574 error_message="invalid value!", 575 orderby=None, 576 groupby=None, 577 left=None, 578 multiple=False, 579 zero="", 580 sort=True, 581 _and=None, 582 ):
583 """ 584 Validator for foreign keys. 585 586 @param dbset: a Set of records like db(query), or db itself 587 @param field: the field in the referenced table 588 @param label: lookup method for the label corresponding a value, 589 alternatively a string template to be filled with 590 values from the record 591 @param filterby: a field in the referenced table to filter by 592 @param filter_opts: values for the filterby field which indicate 593 records to include 594 @param not_filterby: a field in the referenced table to filter by 595 @param not_filter_opts: values for not_filterby field which indicate 596 records to exclude 597 @param realms: only include records belonging to the listed realms 598 (if None, all readable records will be included) 599 @param updateable: only include records in the referenced table which 600 can be updated by the user (if False, all readable 601 records will be included) 602 @param instance_types: if the referenced table is a super-entity, then 603 only include these instance types (this parameter 604 is required for super entity lookups!) 605 @param error_message: the error message to return for failed validation 606 @param orderby: orderby for the options 607 @param groupby: groupby for the options 608 @param left: additional left joins required for the options lookup 609 (super-entity instance left joins will be included 610 automatically) 611 @param multiple: allow multiple values (for list:reference types) 612 @param zero: add this as label for the None-option (allow selection of "None") 613 @param sort: sort options alphabetically by their label 614 @param _and: internal use 615 """ 616 617 if hasattr(dbset, "define_table"): 618 self.dbset = dbset() 619 else: 620 self.dbset = dbset 621 (ktable, kfield) = str(field).split(".") 622 623 if not label: 624 label = "%%(%s)s" % kfield 625 626 if isinstance(label, str): 627 if regex1.match(str(label)): 628 label = "%%(%s)s" % str(label).split(".")[-1] 629 ks = regex2.findall(label) 630 if not kfield in ks: 631 ks += [kfield] 632 fields = ["%s.%s" % (ktable, k) for k in ks] 633 elif hasattr(label, "bulk"): 634 # S3Represent 635 ks = [kfield] 636 if label.custom_lookup: 637 # Represent uses a custom lookup, so we only 638 # retrieve the keys here 639 fields = [kfield] 640 if orderby is None: 641 orderby = field 642 else: 643 # Represent uses a standard field lookup, so 644 # we can do that right here 645 label._setup() 646 fields = list(label.fields) 647 if kfield not in fields: 648 fields.insert(0, kfield) 649 # Unlikely, but possible: represent and validator 650 # using different keys - commented for now for 651 # performance reasons (re-enable if ever necessary) 652 #key = label.key 653 #if key and key not in fields: 654 #fields.insert(0, key) 655 else: 656 ks = [kfield] 657 try: 658 table = current.s3db[ktable] 659 fields =[str(f) for f in table if f.name not in ("wkt", "the_geom")] 660 except RuntimeError: 661 fields = "all" 662 663 self.fields = fields 664 self.label = label 665 self.ktable = ktable 666 if not kfield or not len(kfield): 667 self.kfield = "id" 668 else: 669 self.kfield = kfield 670 self.ks = ks 671 self.error_message = error_message 672 self.theset = None 673 self.orderby = orderby 674 self.groupby = groupby 675 self.left = left 676 self.multiple = multiple 677 self.zero = zero 678 self.sort = sort 679 self._and = _and 680 681 self.filterby = filterby 682 self.filter_opts = filter_opts 683 self.not_filterby = not_filterby 684 self.not_filter_opts = not_filter_opts 685 686 self.realms = realms 687 self.updateable = updateable 688 self.instance_types = instance_types
689 690 # -------------------------------------------------------------------------
691 - def set_self_id(self, record_id):
692 if self._and: 693 self._and.record_id = record_id
694 695 # -------------------------------------------------------------------------
696 - def set_filter(self, 697 filterby = None, 698 filter_opts = None, 699 not_filterby = None, 700 not_filter_opts = None):
701 """ 702 This can be called from prep to apply a filter based on 703 data in the record or the primary resource id. 704 """ 705 706 if filterby: 707 self.filterby = filterby 708 if filter_opts: 709 self.filter_opts = filter_opts 710 if not_filterby: 711 self.not_filterby = not_filterby 712 if not_filter_opts: 713 self.not_filter_opts = not_filter_opts
714 715 # -------------------------------------------------------------------------
716 - def build_set(self):
717 """ 718 Look up selectable options from the database 719 """ 720 721 dbset = self.dbset 722 db = dbset._db 723 724 ktablename = self.ktable 725 if ktablename not in db: 726 table = current.s3db.table(ktablename, db_only=True) 727 else: 728 table = db[ktablename] 729 730 if table: 731 if self.fields == "all": 732 fields = [table[f] for f in table.fields if f not in ("wkt", "the_geom")] 733 else: 734 fieldnames = [f.split(".")[1] if "." in f else f for f in self.fields] 735 fields = [table[k] for k in fieldnames if k in table.fields] 736 737 if db._dbname not in ("gql", "gae"): 738 739 orderby = self.orderby or reduce(lambda a, b: a|b, fields) 740 groupby = self.groupby 741 742 left = self.left 743 744 dd = {"orderby": orderby, "groupby": groupby} 745 query, qleft = self.query(table, fields=fields, dd=dd) 746 if qleft is not None: 747 if left is not None: 748 if not isinstance(qleft, list): 749 qleft = [qleft] 750 ljoins = [str(join) for join in left] 751 for join in qleft: 752 ljoin = str(join) 753 if ljoin not in ljoins: 754 left.append(join) 755 ljoins.append(ljoin) 756 else: 757 left = qleft 758 if left is not None: 759 dd["left"] = left 760 761 # Make sure we have all ORDERBY fields in the query 762 # - required with distinct=True (PostgreSQL) 763 fieldnames = set(str(f) for f in fields) 764 for f in s3_orderby_fields(table, dd.get("orderby")): 765 fieldname = str(f) 766 if fieldname not in fieldnames: 767 fields.append(f) 768 fieldnames.add(fieldname) 769 770 records = dbset(query).select(distinct=True, *fields, **dd) 771 772 else: 773 # Note this does not support filtering. 774 orderby = self.orderby or \ 775 reduce(lambda a, b: a|b, (f for f in fields if f.type != "id")) 776 records = dbset.select(table.ALL, 777 # Caching breaks Colorbox dropdown refreshes 778 #cache=(current.cache.ram, 60), 779 orderby = orderby, 780 ) 781 782 self.theset = [str(r[self.kfield]) for r in records] 783 784 label = self.label 785 try: 786 # Is callable 787 if hasattr(label, "bulk"): 788 # S3Represent => use bulk option 789 d = label.bulk(None, 790 rows = records, 791 list_type = False, 792 show_link = False, 793 ) 794 labels = [d.get(r[self.kfield], d[None]) for r in records] 795 else: 796 # Other representation function 797 labels = [label(r) for r in records] 798 except TypeError: 799 if isinstance(label, str): 800 labels = [label % dict(r) for r in records] 801 elif isinstance(label, (list, tuple)): 802 labels = [" ".join([r[l] for l in label if l in r]) 803 for r in records 804 ] 805 elif "name" in table: 806 labels = [r.name for r in records] 807 else: 808 labels = [r[self.kfield] for r in records] 809 self.labels = labels 810 811 if labels and self.sort: 812 813 items = zip(self.theset, self.labels) 814 items.sort(key=lambda item: s3_unicode(item[1]).lower()) 815 self.theset, self.labels = zip(*items) 816 817 else: 818 self.theset = None 819 self.labels = None
820 821 # -------------------------------------------------------------------------
822 - def query(self, table, fields=None, dd=None):
823 """ 824 Construct the query to lookup the options (separated from 825 build_set so the query can be extracted and used in other 826 lookups, e.g. filter options). 827 828 @param table: the lookup table 829 @param fields: fields (updatable list) 830 @param dd: additional query options (updatable dict) 831 """ 832 833 # Accessible-query 834 method = "update" if self.updateable else "read" 835 query, left = self.accessible_query(method, table, 836 instance_types=self.instance_types) 837 838 # Available-query 839 if "deleted" in table: 840 query &= (table["deleted"] != True) 841 842 # Realms filter? 843 if self.realms: 844 auth = current.auth 845 if auth.is_logged_in() and \ 846 auth.get_system_roles().ADMIN in auth.user.realms: 847 # Admin doesn't filter 848 pass 849 else: 850 query &= auth.permission.realm_query(table, self.realms) 851 852 all_fields = [str(f) for f in fields] if fields is not None else [] 853 854 filterby = self.filterby 855 if filterby and filterby in table: 856 857 filter_opts = self.filter_opts 858 859 if filter_opts: 860 if None in filter_opts: 861 # Needs special handling (doesn't show up in 'belongs') 862 _query = (table[filterby] == None) 863 filter_opts = [f for f in filter_opts if f is not None] 864 if filter_opts: 865 _query = _query | (table[filterby].belongs(filter_opts)) 866 query &= _query 867 else: 868 query &= (table[filterby].belongs(filter_opts)) 869 870 if not self.orderby and \ 871 fields is not None and dd is not None: 872 filterby_field = table[filterby] 873 if dd is not None: 874 dd.update(orderby=filterby_field) 875 if str(filterby_field) not in all_fields: 876 fields.append(filterby_field) 877 all_fields.append(str(filterby_field)) 878 879 not_filterby = self.not_filterby 880 if not_filterby and not_filterby in table: 881 882 not_filter_opts = self.not_filter_opts 883 884 if not_filter_opts: 885 if None in not_filter_opts: 886 # Needs special handling (doesn't show up in 'belongs') 887 _query = (table[not_filterby] == None) 888 not_filter_opts = [f for f in not_filter_opts if f is not None] 889 if not_filter_opts: 890 _query = _query | (table[not_filterby].belongs(not_filter_opts)) 891 query &= (~_query) 892 else: 893 query &= (~(table[not_filterby].belongs(not_filter_opts))) 894 895 if not self.orderby and \ 896 fields is not None and dd is not None: 897 filterby_field = table[not_filterby] 898 if dd is not None: 899 dd.update(orderby=filterby_field) 900 if str(filterby_field) not in all_fields: 901 fields.append(filterby_field) 902 all_fields.append(str(filterby_field)) 903 904 return query, left
905 906 # ------------------------------------------------------------------------- 907 @classmethod
908 - def accessible_query(cls, method, table, instance_types=None):
909 """ 910 Returns an accessible query (and left joins, if necessary) for 911 records in table the user is permitted to access with method 912 913 @param method: the method (e.g. "read" or "update") 914 @param table: the table 915 @param instance_types: list of instance tablenames, if table is 916 a super-entity (required in this case!) 917 918 @return: tuple (query, left) where query is the query and left joins 919 is the list of left joins required for the query 920 921 @note: for higher security policies and super-entities with many 922 instance types this can give a very complex query. Try to 923 always limit the instance types to what is really needed 924 """ 925 926 DEFAULT = (table._id > 0) 927 928 left = None 929 930 if "instance_type" in table: 931 # Super-entity 932 if not instance_types: 933 return DEFAULT, left 934 query = None 935 auth = current.auth 936 s3db = current.s3db 937 for instance_type in instance_types: 938 itable = s3db.table(instance_type) 939 if itable is None: 940 continue 941 942 join = itable.on(itable[table._id.name] == table._id) 943 if left is None: 944 left = [join] 945 else: 946 left.append(join) 947 948 q = (itable._id != None) & \ 949 auth.s3_accessible_query(method, itable) 950 if "deleted" in itable: 951 q &= itable.deleted != True 952 if query is None: 953 query = q 954 else: 955 query |= q 956 957 if query is None: 958 query = DEFAULT 959 else: 960 query = current.auth.s3_accessible_query(method, table) 961 962 return query, left
963 964 # ------------------------------------------------------------------------- 965 # Removed as we don't want any options downloaded unnecessarily 966 #def options(self): 967 968 # -------------------------------------------------------------------------
969 - def __call__(self, value):
970 971 # Translate error message if string 972 error_message = self.error_message 973 if isinstance(error_message, basestring): 974 error_message = current.T(error_message) 975 976 try: 977 dbset = self.dbset 978 table = dbset._db[self.ktable] 979 deleted_q = ("deleted" in table) and (table["deleted"] == False) or False 980 filter_opts_q = False 981 filterby = self.filterby 982 if filterby and filterby in table: 983 filter_opts = self.filter_opts 984 if filter_opts: 985 if None in filter_opts: 986 # Needs special handling (doesn't show up in 'belongs') 987 filter_opts_q = (table[filterby] == None) 988 filter_opts = [f for f in filter_opts if f is not None] 989 if filter_opts: 990 filter_opts_q |= (table[filterby].belongs(filter_opts)) 991 else: 992 filter_opts_q = (table[filterby].belongs(filter_opts)) 993 994 if self.multiple: 995 if isinstance(value, list): 996 values = [str(v) for v in value] 997 elif isinstance(value, basestring) and \ 998 value[0] == "|" and value[-1] == "|": 999 values = value[1:-1].split("|") 1000 elif value: 1001 values = [value] 1002 else: 1003 values = [] 1004 1005 if self.theset: 1006 if not [x for x in values if not x in self.theset]: 1007 return (values, None) 1008 else: 1009 return (value, error_message) 1010 else: 1011 field = table[self.kfield] 1012 query = None 1013 for v in values: 1014 q = (field == v) 1015 query = query is not None and query | q or q 1016 if filter_opts_q != False: 1017 query = query is not None and \ 1018 (filter_opts_q & (query)) or filter_opts_q 1019 if deleted_q != False: 1020 query = query is not None and \ 1021 (deleted_q & (query)) or deleted_q 1022 if dbset(query).count() < 1: 1023 return (value, error_message) 1024 return (values, None) 1025 elif self.theset: 1026 if str(value) in self.theset: 1027 if self._and: 1028 return self._and(value) 1029 else: 1030 return (value, None) 1031 else: 1032 values = [value] 1033 query = None 1034 for v in values: 1035 q = (table[self.kfield] == v) 1036 query = query is not None and query | q or q 1037 if filter_opts_q != False: 1038 query = query is not None and \ 1039 (filter_opts_q & (query)) or filter_opts_q 1040 if deleted_q != False: 1041 query = query is not None and \ 1042 (deleted_q & (query)) or deleted_q 1043 if dbset(query).count(): 1044 if self._and: 1045 return self._and(value) 1046 else: 1047 return (value, None) 1048 except: 1049 pass 1050 1051 return (value, error_message)
1052
1053 1054 # ============================================================================= 1055 -class IS_ONE_OF(IS_ONE_OF_EMPTY):
1056 """ 1057 Extends IS_ONE_OF_EMPTY by restoring the 'options' method. 1058 """ 1059
1060 - def options(self, zero=True):
1061 1062 self.build_set() 1063 theset, labels = self.theset, self.labels 1064 if theset is None or labels is None: 1065 items = [] 1066 else: 1067 items = zip(theset, labels) 1068 if zero and self.zero is not None and not self.multiple: 1069 items.insert(0, ("", self.zero)) 1070 return items
1071
1072 # ============================================================================= 1073 -class IS_ONE_OF_EMPTY_SELECT(IS_ONE_OF_EMPTY):
1074 1075 """ 1076 Extends IS_ONE_OF_EMPTY by displaying an empty SELECT (instead of INPUT) 1077 """ 1078
1079 - def options(self, zero=True):
1080 return [("", "")]
1081
1082 # ============================================================================= 1083 -class IS_NOT_ONE_OF(IS_NOT_IN_DB):
1084 """ 1085 Filtered version of IS_NOT_IN_DB() 1086 - understands the 'deleted' field. 1087 - makes the field unique (amongst non-deleted field) 1088 1089 Example: 1090 - INPUT(_type="text", _name="name", requires=IS_NOT_ONE_OF(db, db.table)) 1091 """ 1092
1093 - def __call__(self, value):
1094 1095 value = str(value) 1096 if not value.strip(): 1097 # Empty => error 1098 return (value, translate(self.error_message)) 1099 1100 if value in self.allowed_override: 1101 # Uniqueness-requirement overridden 1102 return (value, None) 1103 1104 # Establish table and field 1105 (tablename, fieldname) = str(self.field).split(".") 1106 dbset = self.dbset 1107 table = dbset.db[tablename] 1108 field = table[fieldname] 1109 1110 # Does the table allow archiving ("soft-delete")? 1111 archived = "deleted" in table 1112 1113 # Does the table use multiple columns as key? 1114 record_id = self.record_id 1115 keys = record_id.keys() if isinstance(record_id, dict) else None 1116 1117 # Build duplicate query 1118 # => if the field has a unique-constraint, we must include 1119 # archived ("soft-deleted") records, otherwise the 1120 # validator will pass, but the DB-write will crash 1121 query = (field == value) 1122 if not field.unique and archived: 1123 query = (table["deleted"] == False) & query 1124 1125 # Limit the fields we extract to just keys+deleted 1126 fields = [] 1127 if keys: 1128 fields = [table[k] for k in keys] 1129 else: 1130 fields = [table._id] 1131 if archived: 1132 fields.append(table.deleted) 1133 1134 # Find conflict 1135 row = dbset(query).select(limitby=(0, 1), *fields).first() 1136 if row: 1137 if keys: 1138 # Keyed table 1139 for f in keys: 1140 if str(getattr(row, f)) != str(record_id[f]): 1141 return (value, translate(self.error_message)) 1142 1143 elif str(row[table._id.name]) != str(record_id): 1144 1145 if archived and row.deleted and field.type in ("string", "text"): 1146 # Table supports archiving, and the conflicting 1147 # record is "deleted" => try updating the archived 1148 # record by appending a random tag to the field value 1149 import random 1150 tagged = "%s.[%s]" % (value, 1151 "".join(random.choice("abcdefghijklmnopqrstuvwxyz") 1152 for _ in range(8)) 1153 ) 1154 try: 1155 row.update_record(**{fieldname: tagged}) 1156 except: 1157 # Failed => nothing else we can try 1158 return (value, translate(self.error_message)) 1159 else: 1160 return (value, translate(self.error_message)) 1161 1162 return (value, None)
1163
1164 # ============================================================================= 1165 -class IS_LOCATION(Validator):
1166 """ 1167 Allow all locations, or locations by level. 1168 """ 1169
1170 - def __init__(self, 1171 level = None, 1172 error_message = None 1173 ):
1174 1175 self.level = level # can be a List or a single element 1176 self.error_message = error_message 1177 # Make it like IS_ONE_OF to support AddResourceLink 1178 self.ktable = "gis_location" 1179 self.kfield = "id" 1180 # Tell s3_mark_required that this validator doesn't accept NULL values 1181 self.mark_required = True
1182 1183 # -------------------------------------------------------------------------
1184 - def __call__(self, value):
1185 1186 level = self.level 1187 if level == "L0": 1188 # Use cached countries. This returns name if id is for a country. 1189 try: 1190 location_id = int(value) 1191 except ValueError: 1192 ok = False 1193 else: 1194 ok = current.gis.get_country(location_id) 1195 else: 1196 db = current.db 1197 table = db.gis_location 1198 query = (table.id == value) & (table.deleted == False) 1199 if level: 1200 if isinstance(level, (tuple, list)): 1201 if None in level: 1202 # None needs special handling 1203 level = [l for l in level if l is not None] 1204 query &= ((table.level.belongs(level)) | \ 1205 (table.level == None)) 1206 else: 1207 query &= (table.level.belongs(level)) 1208 else: 1209 query &= (table.level == level) 1210 ok = db(query).select(table.id, limitby=(0, 1)).first() 1211 if ok: 1212 return (value, None) 1213 else: 1214 return (value, self.error_message or current.T("Invalid Location!"))
1215
1216 # ============================================================================= 1217 -class IS_PROCESSED_IMAGE(Validator):
1218 """ 1219 Uses an S3ImageCropWidget to allow the user to crop/scale images and 1220 processes the results sent by the browser. 1221 1222 @param file_cb: callback that returns the file for this field 1223 1224 @param error_message: the error message to be returned 1225 1226 @param image_bounds: the boundaries for the processed image 1227 1228 @param upload_path: upload path for the image 1229 """ 1230
1231 - def __init__(self, 1232 field_name, 1233 file_cb, 1234 error_message="No image was specified!", 1235 image_bounds=(300, 300), 1236 upload_path=None, 1237 ):
1238 1239 self.field_name = field_name 1240 self.file_cb = file_cb 1241 self.error_message = error_message 1242 self.image_bounds = image_bounds 1243 self.upload_path = upload_path
1244
1245 - def __call__(self, value):
1246 1247 if current.response.s3.bulk: 1248 # Pointless in imports 1249 return (value, None) 1250 1251 r = current.request 1252 post_vars = r.post_vars 1253 1254 if r.env.request_method == "GET": 1255 return (value, None) 1256 1257 # If there's a newly uploaded file, accept it. It'll be processed in 1258 # the update form. 1259 # NOTE: A FieldStorage with data evaluates as False (odd!) 1260 uploaded_image = post_vars.get(self.field_name) 1261 if uploaded_image not in ("", None): 1262 return (uploaded_image, None) 1263 1264 cropped_image = post_vars.get("imagecrop-data") 1265 uploaded_image = self.file_cb() 1266 1267 if not (cropped_image or uploaded_image): 1268 return value, current.T(self.error_message) 1269 1270 # Decode the base64-encoded image from the client side image crop 1271 # process if, that worked. 1272 if cropped_image: 1273 import base64 1274 import uuid 1275 try: 1276 from cStringIO import StringIO 1277 except ImportError: 1278 from StringIO import StringIO 1279 1280 metadata, cropped_image = cropped_image.split(",") 1281 #filename, datatype, enctype = metadata.split(";") 1282 filename = metadata.split(";", 1)[0] 1283 1284 f = Storage() 1285 f.filename = uuid.uuid4().hex + filename 1286 1287 f.file = StringIO(base64.decodestring(cropped_image)) 1288 1289 return (f, None) 1290 1291 # Crop the image, if we've got the crop points. 1292 points = post_vars.get("imagecrop-points") 1293 if points and uploaded_image: 1294 import os 1295 points = map(float, points.split(",")) 1296 1297 if not self.upload_path: 1298 path = os.path.join(r.folder, "uploads", "images", uploaded_image) 1299 else: 1300 path = os.path.join(self.upload_path, uploaded_image) 1301 1302 current.s3task.async("crop_image", 1303 args=[path] + points + [self.image_bounds[0]]) 1304 1305 return (None, None)
1306
1307 # ============================================================================= 1308 -class IS_UTC_OFFSET(Validator):
1309 """ 1310 Validates a given string value as UTC offset in the format +/-HHMM 1311 1312 @param error_message: the error message to be returned 1313 1314 @note: 1315 all leading parts of the string (before the trailing offset specification) 1316 will be ignored and replaced by 'UTC ' in the return value, if the string 1317 passes through. 1318 """ 1319
1320 - def __init__(self, error_message="invalid UTC offset!"):
1321 1322 self.error_message = error_message
1323 1324 # -------------------------------------------------------------------------
1325 - def __call__(self, value):
1326 1327 if value and isinstance(value, str): 1328 1329 offset = S3DateTime.get_offset_value(value) 1330 if offset is not None: 1331 hours, seconds = divmod(abs(offset), 3600) 1332 minutes = int(seconds / 60) 1333 sign = "-" if offset < 0 else "+" 1334 return ("%s%02d%02d" % (sign, hours, minutes), None) 1335 1336 return (value, self.error_message)
1337
1338 # ============================================================================= 1339 -class IS_UTC_DATETIME(Validator):
1340 """ 1341 Validates a given date/time and returns it as timezone-naive 1342 datetime object in UTC. Accepted input types are strings (in 1343 local format), datetime.datetime and datetime.date. 1344 1345 Example: 1346 - INPUT(_type="text", _name="name", requires=IS_UTC_DATETIME()) 1347 1348 @note: a date/time string must be in local format, and can have 1349 an optional trailing UTC offset specified as +/-HHMM 1350 (+ for eastern, - for western timezones) 1351 @note: dates stretch 8 hours West and 16 hours East of the current 1352 time zone, i.e. the most Eastern timezones are on the next 1353 day. 1354 """ 1355
1356 - def __init__(self, 1357 format=None, 1358 error_message=None, 1359 offset_error=None, 1360 utc_offset=None, 1361 calendar=None, 1362 minimum=None, 1363 maximum=None):
1364 """ 1365 Constructor 1366 1367 @param format: strptime/strftime format template string, for 1368 directives refer to your strptime implementation 1369 @param error_message: error message for invalid date/times 1370 @param offset_error: error message for invalid UTC offset 1371 @param utc_offset: offset to UTC in hours, defaults to the 1372 current session's UTC offset 1373 @param calendar: calendar to use for string evaluation, defaults 1374 to current.calendar 1375 @param minimum: the minimum acceptable date/time 1376 @param maximum: the maximum acceptable date/time 1377 """ 1378 1379 if format is None: 1380 self.format = str(current.deployment_settings.get_L10n_datetime_format()) 1381 else: 1382 self.format = str(format) 1383 1384 if isinstance(calendar, basestring): 1385 # Instantiate calendar by name 1386 from s3datetime import S3Calendar 1387 calendar = S3Calendar(calendar) 1388 elif calendar == None: 1389 calendar = current.calendar 1390 self.calendar = calendar 1391 1392 self.utc_offset = utc_offset 1393 1394 self.minimum = minimum 1395 self.maximum = maximum 1396 1397 # Default error messages 1398 T = current.T 1399 if error_message is None: 1400 if minimum is None and maximum is None: 1401 error_message = T("Date/Time is required!") 1402 elif minimum is None: 1403 error_message = T("Date/Time must be %(max)s or earlier!") 1404 elif maximum is None: 1405 error_message = T("Date/Time must be %(min)s or later!") 1406 else: 1407 error_message = T("Date/Time must be between %(min)s and %(max)s!") 1408 if offset_error is None: 1409 offset_error = T("Invalid UTC offset!") 1410 1411 # Localized minimum/maximum 1412 mindt = self.formatter(minimum) if minimum else "" 1413 maxdt = self.formatter(maximum) if maximum else "" 1414 1415 # Store error messages 1416 self.error_message = error_message % {"min": mindt, "max": maxdt} 1417 self.offset_error = offset_error
1418 1419 # -------------------------------------------------------------------------
1420 - def delta(self, utc_offset=None):
1421 """ 1422 Compute the delta in seconds for the current UTC offset 1423 1424 @param utc_offset: the offset (override defaults) 1425 @return: the offset in seconds 1426 """ 1427 1428 if utc_offset is None: 1429 # Fall back to validator default 1430 utc_offset = self.utc_offset 1431 1432 if utc_offset is None: 1433 # Fall back to session default 1434 utc_offset = current.session.s3.utc_offset 1435 1436 # Convert into offset seconds 1437 return S3DateTime.get_offset_value(utc_offset)
1438 1439 # -------------------------------------------------------------------------
1440 - def __call__(self, value):
1441 """ 1442 Validate a value, and convert it into a timezone-naive 1443 datetime.datetime object as necessary 1444 1445 @param value: the value to validate 1446 @return: tuple (value, error) 1447 """ 1448 1449 if isinstance(value, basestring): 1450 1451 val = value.strip() 1452 1453 # Split date/time and UTC offset 1454 if len(val) > 5 and val[-5] in ("+", "-") and val[-4:].isdigit(): 1455 dtstr, utc_offset = val[0:-5].strip(), val[-5:] 1456 else: 1457 dtstr, utc_offset = val, None 1458 1459 # Convert into datetime object 1460 dt = self.calendar.parse_datetime(dtstr, 1461 dtfmt=self.format, 1462 local=True, 1463 ) 1464 if dt is None: 1465 # Try parsing as date 1466 dt_ = self.calendar.parse_date(dtstr) 1467 if dt_ is None: 1468 return(value, self.error_message) 1469 dt = datetime.datetime.combine(dt_, datetime.datetime.min.time()) 1470 elif isinstance(value, datetime.datetime): 1471 dt = value 1472 utc_offset = None 1473 elif isinstance(value, datetime.date): 1474 # Default to 8:00 hours in the current timezone 1475 dt = datetime.datetime.combine(value, datetime.time(8, 0, 0)) 1476 utc_offset = None 1477 else: 1478 # Invalid type 1479 return value, self.error_message 1480 1481 # Convert to UTC and make tz-naive 1482 if dt.tzinfo: 1483 offset = dt.tzinfo.utcoffset(dt) 1484 dt = dt.replace(tzinfo=None) 1485 else: 1486 offset = self.delta(utc_offset=utc_offset) 1487 # Offset must be in range -2359 to +2359 1488 if not -86340 < offset < 86340: 1489 return (val, self.offset_error) 1490 offset = datetime.timedelta(seconds=offset) 1491 dt_utc = dt - offset 1492 1493 # Validate 1494 if self.minimum and dt_utc < self.minimum or \ 1495 self.maximum and dt_utc > self.maximum: 1496 return (dt_utc, self.error_message) 1497 1498 return (dt_utc, None)
1499 1500 # -------------------------------------------------------------------------
1501 - def formatter(self, value):
1502 """ 1503 Format a datetime as string. 1504 1505 @param value: the value 1506 """ 1507 1508 if not value: 1509 return current.messages["NONE"] 1510 1511 offset = self.delta() 1512 if offset: 1513 value += datetime.timedelta(seconds=offset) 1514 result = self.calendar.format_datetime(value, 1515 dtfmt=self.format, 1516 local=True, 1517 ) 1518 return result
1519
1520 # ============================================================================= 1521 -class IS_UTC_DATE(IS_UTC_DATETIME):
1522 """ 1523 Validates a given date and returns the corresponding datetime.date 1524 object in UTC. Accepted input types are strings (in local format), 1525 datetime.datetime and datetime.date. 1526 1527 Example: 1528 - INPUT(_type="text", _name="name", requires=IS_UTC_DATE()) 1529 1530 @note: dates stretch 8 hours West and 16 hours East of the current 1531 time zone, i.e. the most Eastern timezones are on the next 1532 day. 1533 """ 1534
1535 - def __init__(self, 1536 format=None, 1537 error_message=None, 1538 offset_error=None, 1539 calendar=None, 1540 utc_offset=None, 1541 minimum=None, 1542 maximum=None):
1543 """ 1544 Constructor 1545 1546 @param format: strptime/strftime format template string, for 1547 directives refer to your strptime implementation 1548 @param error_message: error message for invalid date/times 1549 @param offset_error: error message for invalid UTC offset 1550 @param calendar: calendar to use for string evaluation, defaults 1551 to current.calendar 1552 @param utc_offset: offset to UTC in seconds, defaults to the 1553 current session's UTC offset 1554 @param minimum: the minimum acceptable date (datetime.date) 1555 @param maximum: the maximum acceptable date (datetime.date) 1556 """ 1557 1558 if format is None: 1559 self.format = str(current.deployment_settings.get_L10n_date_format()) 1560 else: 1561 self.format = str(format) 1562 1563 if isinstance(calendar, basestring): 1564 # Instantiate calendar by name 1565 from s3datetime import S3Calendar 1566 calendar = S3Calendar(calendar) 1567 elif calendar == None: 1568 calendar = current.calendar 1569 self.calendar = calendar 1570 1571 self.utc_offset = utc_offset 1572 1573 self.minimum = minimum 1574 self.maximum = maximum 1575 1576 # Default error messages 1577 T = current.T 1578 if error_message is None: 1579 if minimum is None and maximum is None: 1580 error_message = T("Date is required!") 1581 elif minimum is None: 1582 error_message = T("Date must be %(max)s or earlier!") 1583 elif maximum is None: 1584 error_message = T("Date must be %(min)s or later!") 1585 else: 1586 error_message = T("Date must be between %(min)s and %(max)s!") 1587 if offset_error is None: 1588 offset_error = T("Invalid UTC offset!") 1589 1590 # Localized minimum/maximum 1591 mindt = self.formatter(minimum) if minimum else "" 1592 maxdt = self.formatter(maximum) if maximum else "" 1593 1594 # Store error messages 1595 self.error_message = error_message % {"min": mindt, "max": maxdt} 1596 self.offset_error = offset_error
1597 1598 # -------------------------------------------------------------------------
1599 - def __call__(self, value):
1600 """ 1601 Validate a value, and convert it into a datetime.date object 1602 as necessary 1603 1604 @param value: the value to validate 1605 @return: tuple (value, error) 1606 """ 1607 1608 is_datetime = False 1609 1610 if isinstance(value, basestring): 1611 # Convert into date object 1612 dt = self.calendar.parse_date(value.strip(), 1613 dtfmt=self.format, 1614 local=True, 1615 ) 1616 if dt is None: 1617 return(value, self.error_message) 1618 elif isinstance(value, datetime.datetime): 1619 dt = value 1620 #utc_offset = None 1621 is_datetime = True 1622 elif isinstance(value, datetime.date): 1623 # Default to 0:00 hours in the current timezone 1624 dt = value 1625 #utc_offset = None 1626 else: 1627 # Invalid type 1628 return (value, self.error_message) 1629 1630 # Convert to UTC 1631 if is_datetime and dt.tzinfo: 1632 offset = dt.tzinfo.utcoffset(dt) 1633 dt = dt.replace(tzinfo=None) 1634 else: 1635 offset = self.delta() 1636 # Offset must be in range -2359 to +2359 1637 if not -86340 < offset < 86340: 1638 return (value, self.offset_error) 1639 offset = datetime.timedelta(seconds=offset) 1640 1641 if not is_datetime: 1642 # Convert to standard time 08:00 hours 1643 dt = datetime.datetime.combine(dt, datetime.time(8, 0, 0)) 1644 dt_utc = (dt - offset).date() 1645 1646 # Validate 1647 if self.minimum and dt_utc < self.minimum or \ 1648 self.maximum and dt_utc > self.maximum: 1649 return (value, self.error_message) 1650 1651 return (dt_utc, None)
1652 1653 # -------------------------------------------------------------------------
1654 - def formatter(self, value):
1655 """ 1656 Format a date as string. 1657 1658 @param value: the value 1659 """ 1660 1661 if not value: 1662 return current.messages["NONE"] 1663 1664 offset = self.delta() 1665 if offset: 1666 delta = datetime.timedelta(seconds=offset) 1667 if not isinstance(value, datetime.datetime): 1668 combine = datetime.datetime.combine 1669 # Compute the break point 1670 bp = (combine(value, datetime.time(8, 0, 0)) - delta).time() 1671 value = combine(value, bp) 1672 value += delta 1673 1674 result = self.calendar.format_date(value, 1675 dtfmt=self.format, 1676 local=True, 1677 ) 1678 1679 return result
1680
1681 # ============================================================================= 1682 -class IS_ACL(IS_IN_SET):
1683 1684 """ 1685 Validator for ACLs 1686 1687 @attention: Incomplete! Does not validate yet, but just convert. 1688 """ 1689
1690 - def __call__(self, value):
1691 """ 1692 Validation 1693 1694 @param value: the value to validate 1695 """ 1696 1697 if not isinstance(value, (list, tuple)): 1698 value = [value] 1699 1700 acl = 0x0000 1701 for v in value: 1702 try: 1703 flag = int(v) 1704 except (ValueError, TypeError): 1705 flag = 0x0000 1706 else: 1707 acl |= flag 1708 1709 return (acl, None)
1710
1711 # ============================================================================= 1712 -class IS_COMBO_BOX(Validator):
1713 """ 1714 Designed for use with an Autocomplete. 1715 - catches any new entries & creates the appropriate record 1716 @ToDo: Audit 1717 """ 1718
1719 - def __init__(self, 1720 tablename, 1721 requires, # The normal validator 1722 error_message = None, 1723 ):
1724 self.tablename = tablename 1725 self.requires = requires 1726 self.error_message = error_message
1727 1728 # -------------------------------------------------------------------------
1729 - def __call__(self, value):
1730 1731 if not value: 1732 # Do the normal validation 1733 return self.requires(value) 1734 elif isinstance(value, int): 1735 # If this is an ID then this is an update form 1736 # @ToDo: Can we assume that? 1737 1738 # Do the normal validation 1739 return self.requires(value) 1740 else: 1741 # Name => create form 1742 tablename = self.tablename 1743 db = current.db 1744 table = db[tablename] 1745 1746 # Test for duplicates 1747 query = (table.name == value) 1748 r = db(query).select(table.id, 1749 limitby=(0, 1)).first() 1750 if r: 1751 # Use Existing record 1752 value = r.id 1753 return (value, None) 1754 if not current.auth.s3_has_permission("create", table): 1755 return (None, current.auth.messages.access_denied) 1756 value = table.insert(name=value) 1757 # onaccept 1758 onaccept = current.s3db.get_config(tablename, "onaccept") 1759 if onaccept: 1760 onaccept(form=Storage(vars=Storage(id=value))) 1761 return (value, None)
1762
1763 # ============================================================================= 1764 -class QUANTITY_INV_ITEM(Validator):
1765 """ 1766 For Inventory module 1767 """ 1768
1769 - def __init__(self, 1770 db, 1771 inv_item_id, 1772 item_pack_id 1773 ):
1774 1775 self.inv_item_id = inv_item_id 1776 self.item_pack_id = item_pack_id 1777 current.db = db
1778 1779 # -------------------------------------------------------------------------
1780 - def __call__(self, value):
1781 1782 db = current.db 1783 args = current.request.args 1784 track_quantity = 0 1785 if args[1] == "track_item" and len(args) > 2: 1786 # look to see if we already have a quantity stored in the track item 1787 track_item_id = args[2] 1788 # @ToDo: Optimise with limitby=(0,1) 1789 track_record = current.s3db.inv_track_item[track_item_id] 1790 track_quantity = track_record.quantity 1791 if track_quantity >= float(value): 1792 # value reduced or unchanged 1793 return (value, None) 1794 error = "Invalid Quantity" # @todo: better error catching 1795 query = (db.inv_inv_item.id == self.inv_item_id) & \ 1796 (db.inv_inv_item.item_pack_id == db.supply_item_pack.id) 1797 inv_item_record = db(query).select(db.inv_inv_item.quantity, 1798 db.supply_item_pack.quantity, 1799 db.supply_item_pack.name, 1800 limitby = (0, 1)).first() # @todo: this should be a virtual field 1801 if inv_item_record and value: 1802 query = (db.supply_item_pack.id == self.item_pack_id) 1803 send_record = db(query).select(db.supply_item_pack.quantity, 1804 limitby=(0, 1)).first() 1805 send_quantity = (float(value) - track_quantity) * send_record.quantity 1806 inv_quantity = inv_item_record.inv_inv_item.quantity * \ 1807 inv_item_record.supply_item_pack.quantity 1808 if send_quantity > inv_quantity: 1809 return (value, 1810 "Only %s %s (%s) in the Warehouse Stock." % 1811 (inv_quantity, 1812 inv_item_record.supply_item_pack.name, 1813 inv_item_record.supply_item_pack.quantity) 1814 ) 1815 else: 1816 return (value, None) 1817 else: 1818 return (value, error)
1819
1820 # ============================================================================= 1821 -class IS_IN_SET_LAZY(Validator):
1822 """ 1823 Like IS_IN_SET but with options obtained from a supplied function. 1824 1825 Options are instantiated when the validator or its options() method is 1826 called, so don't need to be generated until it's used. Useful if the 1827 field is not needed on every request, and does significant processing 1828 to construct its options, or generates a large collection. If the 1829 options are just from a database query, one can use IS_ONE_OF instead. 1830 1831 Raises an exception if an options collection is passed rather than a 1832 callable as this is a programming error, e.g. accidentally *calling* 1833 the options function in the constructor instead of passing the 1834 function. That would not get lazy options instantiation. 1835 1836 The options collection (theset) and labels collection parameters to 1837 IS_IN_SET are replaced by: 1838 1839 @param theset_fn: Function of no arguments that returns a collection 1840 of options and (optionally) labels. Both options and labels can be 1841 supplied via a dict or OrderedDict (options are keys, values are 1842 labels), list (or tuple) of two-element lists (or tuples) (element 0 in 1843 each pair is an option, element 1 is it's label). Otherwise, labels 1844 are obtained either by calling the supplied represent function on each 1845 item produced by theset_fn, or (if no represent is supplied), the items 1846 themselves are used as labels. 1847 1848 @param represent: Function of one argument that returns the label for 1849 a given option. 1850 1851 If there is a function call that returns the collection, just put 1852 "lambda:" in front of the call. E.g.: 1853 1854 Field("nationality", 1855 requires = IS_EMPTY_OR(IS_IN_SET_LAZY( 1856 lambda: gis.get_countries(key_type="code"))), 1857 label = T("Nationality"), 1858 represent = lambda code: gis.get_country(code, key_type="code") or UNKNOWN_OPT) 1859 1860 Keyword parameters are same as for IS_IN_SET, except for labels, which 1861 is not replaced by a function that parallels theset_fn, since ordering 1862 is problematic if theset_fn returns a dict. 1863 """ 1864
1865 - def __init__( 1866 self, 1867 theset_fn, 1868 represent=None, 1869 error_message="value not allowed", 1870 multiple=False, 1871 zero="", 1872 sort=False, 1873 ):
1874 self.multiple = multiple 1875 if not callable(theset_fn): 1876 raise TypeError("Argument must be a callable.") 1877 self.theset_fn = theset_fn 1878 self.theset = None 1879 self.labels = None 1880 self.represent = represent 1881 self.error_message = error_message 1882 self.zero = zero 1883 self.sort = sort
1884 1885 # -------------------------------------------------------------------------
1886 - def _make_theset(self):
1887 theset = self.theset_fn() 1888 if theset: 1889 if isinstance(theset, dict): 1890 self.theset = [str(item) for item in theset] 1891 self.labels = theset.values() 1892 elif isinstance(theset, (tuple,list)): # @ToDo: Can this be a Rows? 1893 if isinstance(theset[0], (tuple,list)) and len(theset[0])==2: 1894 self.theset = [str(item) for item,label in theset] 1895 self.labels = [str(label) for item,label in theset] 1896 else: 1897 self.theset = [str(item) for item in theset] 1898 represent = self.represent 1899 if represent: 1900 self.labels = [represent(item) for item in theset] 1901 else: 1902 self.theset = theset 1903 else: 1904 self.theset = []
1905 1906 # -------------------------------------------------------------------------
1907 - def options(self, zero=True):
1908 if not self.theset: 1909 self._make_theset() 1910 if not self.labels: 1911 items = [(k, k) for (i, k) in enumerate(self.theset)] 1912 else: 1913 items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] 1914 if self.sort: 1915 items.sort(options_sorter) 1916 if zero and not self.zero is None and not self.multiple: 1917 items.insert(0, ("", self.zero)) 1918 return items
1919 1920 # -------------------------------------------------------------------------
1921 - def __call__(self, value):
1922 if not self.theset: 1923 self._make_theset() 1924 if self.multiple: 1925 ### if below was values = re.compile("[\w\-:]+").findall(str(value)) 1926 if isinstance(value, (str,unicode)): 1927 values = [value] 1928 elif isinstance(value, (tuple, list)): 1929 values = value 1930 elif not value: 1931 values = [] 1932 else: 1933 values = [value] 1934 failures = [x for x in values if not x in self.theset] 1935 if failures and self.theset: 1936 if self.multiple and (value == None or value == ""): 1937 return ([], None) 1938 return (value, self.error_message) 1939 if self.multiple: 1940 if isinstance(self.multiple,(tuple,list)) and \ 1941 not self.multiple[0]<=len(values)<self.multiple[1]: 1942 return (values, self.error_message) 1943 return (values, None) 1944 return (value, None)
1945
1946 # ============================================================================= 1947 -class IS_TIME_INTERVAL_WIDGET(Validator):
1948 """ 1949 Simple validator for the S3TimeIntervalWidget, returns 1950 the selected time interval in seconds 1951 """ 1952
1953 - def __init__(self, field):
1954 self.field = field
1955 1956 # -------------------------------------------------------------------------
1957 - def __call__(self, value):
1958 1959 try: 1960 val = int(value) 1961 except ValueError: 1962 return (0, None) 1963 request = current.request 1964 _vars = request.post_vars 1965 try: 1966 mul = int(_vars[("%s_multiplier" % self.field).replace(".", "_")]) 1967 except ValueError: 1968 return (0, None) 1969 seconds = val * mul 1970 return (seconds, None)
1971
1972 # ============================================================================= 1973 -class IS_PERSON_GENDER(IS_IN_SET):
1974 """ 1975 Special validator for pr_person.gender and derivates, 1976 accepts the "O" option even if it's not in the set. 1977 """ 1978
1979 - def __call__(self, value):
1980 1981 if value == 4: 1982 # 4 = other, always accepted even if hidden 1983 return value, None 1984 else: 1985 return super(IS_PERSON_GENDER, self).__call__(value)
1986
1987 # ============================================================================= 1988 -class IS_PHONE_NUMBER(Validator):
1989 """ 1990 Validator for single phone numbers with option to 1991 enforce E.123 international notation (with leading + 1992 and no punctuation or spaces). 1993 """ 1994
1995 - def __init__(self, 1996 international = False, 1997 error_message = None):
1998 """ 1999 Constructor 2000 2001 @param international: enforce E.123 international notation, 2002 no effect if turned off globally in 2003 deployment settings 2004 @param error_message: alternative error message 2005 """ 2006 2007 self.international = international 2008 self.error_message = error_message
2009
2010 - def __call__(self, value):
2011 """ 2012 Validation of a value 2013 2014 @param value: the value 2015 @return: tuple (value, error), where error is None if value 2016 is valid. With international=True, the value returned 2017 is converted into E.123 international notation. 2018 """ 2019 2020 if isinstance(value, basestring): 2021 value = value.strip() 2022 if value and value[0] == unichr(8206): 2023 # Strip the LRM character 2024 value = value[1:] 2025 number = s3_str(value) 2026 number, error = s3_single_phone_requires(number) 2027 else: 2028 error = True 2029 2030 if not error: 2031 if self.international and \ 2032 current.deployment_settings \ 2033 .get_msg_require_international_phone_numbers(): 2034 2035 # Configure alternative error message 2036 error_message = self.error_message 2037 if not error_message: 2038 error_message = current.T("Enter phone number in international format like +46783754957") 2039 2040 # Require E.123 international format 2041 number = "".join(re.findall(r"[\d+]+", number)) 2042 match = re.match(r"(\+)([1-9]\d+)$", number) 2043 #match = re.match("(\+|00|\+00)([1-9]\d+)$", number) 2044 2045 if match: 2046 number = "+%s" % match.groups()[1] 2047 return (number, None) 2048 else: 2049 return (number, None) 2050 2051 error_message = self.error_message 2052 if not error_message: 2053 error_message = current.T("Enter a valid phone number") 2054 2055 return (value, error_message)
2056
2057 # ============================================================================= 2058 -class IS_PHONE_NUMBER_MULTI(Validator):
2059 """ 2060 Validator for multiple phone numbers. 2061 """ 2062
2063 - def __init__(self, 2064 error_message = None):
2065 """ 2066 Constructor 2067 2068 @param error_message: alternative error message 2069 """ 2070 2071 self.error_message = error_message
2072
2073 - def __call__(self, value):
2074 """ 2075 Validation of a value 2076 2077 @param value: the value 2078 @return: tuple (value, error), where error is None if value 2079 is valid. 2080 """ 2081 2082 value = value.strip() 2083 if value[0] == unichr(8206): 2084 # Strip the LRM character 2085 value = value[1:] 2086 number = s3_str(value) 2087 number, error = s3_phone_requires(number) 2088 if not error: 2089 return (number, None) 2090 2091 error_message = self.error_message 2092 if not error_message: 2093 error_message = current.T("Enter a valid phone number") 2094 2095 return (value, error_message)
2096
2097 # ============================================================================= 2098 -class IS_DYNAMIC_FIELDNAME(Validator):
2099 """ Validator for field names in dynamic tables """ 2100 2101 PATTERN = re.compile("^[a-z]+[a-z0-9_]*$") 2102
2103 - def __init__(self, 2104 error_message = "Invalid field name", 2105 ):
2106 """ 2107 Constructor 2108 2109 @param error_message: the error message for invalid values 2110 """ 2111 2112 self.error_message = error_message
2113 2114 # -------------------------------------------------------------------------
2115 - def __call__(self, value):
2116 """ 2117 Validation of a value 2118 2119 @param value: the value 2120 @return: tuple (value, error) 2121 """ 2122 2123 if value: 2124 2125 name = str(value).lower().strip() 2126 2127 from s3fields import s3_all_meta_field_names 2128 2129 if name != "id" and \ 2130 name not in s3_all_meta_field_names() and \ 2131 self.PATTERN.match(name): 2132 return (name, None) 2133 2134 return (value, self.error_message)
2135
2136 # ============================================================================= 2137 -class IS_DYNAMIC_FIELDTYPE(Validator):
2138 """ Validator for field types in dynamic tables """ 2139 2140 SUPPORTED_TYPES = ("boolean", 2141 "date", 2142 "datetime", 2143 "double", 2144 "integer", 2145 "reference", 2146 "string", 2147 "text", 2148 "upload", 2149 ) 2150
2151 - def __init__(self, 2152 error_message = "Unsupported field type", 2153 ):
2154 """ 2155 Constructor 2156 2157 @param error_message: the error message for invalid values 2158 """ 2159 2160 self.error_message = error_message
2161 2162 # -------------------------------------------------------------------------
2163 - def __call__(self, value):
2164 """ 2165 Validation of a value 2166 2167 @param value: the value 2168 @return: tuple (value, error) 2169 """ 2170 2171 if value: 2172 2173 field_type = str(value).lower().strip() 2174 2175 items = field_type.split(" ") 2176 base_type = items[0] 2177 2178 if base_type == "reference": 2179 2180 # Verify that referenced table is specified and exists 2181 if len(items) > 1: 2182 ktablename = items[1].split(".")[0] 2183 ktable = current.s3db.table(ktablename, db_only=True) 2184 if ktable: 2185 return (field_type, None) 2186 2187 elif base_type in self.SUPPORTED_TYPES: 2188 return (field_type, None) 2189 2190 return (value, self.error_message)
2191
2192 # ============================================================================= 2193 -class IS_ISO639_2_LANGUAGE_CODE(IS_IN_SET):
2194 """ 2195 Validate ISO639-2 Alpha-2/Alpha-3 language codes 2196 """ 2197
2198 - def __init__(self, 2199 error_message = "Invalid language code", 2200 multiple = False, 2201 select = DEFAULT, 2202 sort = False, 2203 translate = False, 2204 zero = "", 2205 ):
2206 """ 2207 Constructor 2208 2209 @param error_message: alternative error message 2210 @param multiple: allow selection of multiple options 2211 @param select: dict of options for the selector, 2212 defaults to settings.L10n.languages, 2213 set explicitly to None to allow all languages 2214 @param sort: sort options in selector 2215 @param translate: translate the language options into 2216 the current UI language 2217 @param zero: use this label for the empty-option (default="") 2218 """ 2219 2220 super(IS_ISO639_2_LANGUAGE_CODE, self).__init__( 2221 self.language_codes(), 2222 error_message = error_message, 2223 multiple = multiple, 2224 zero = zero, 2225 sort = sort, 2226 ) 2227 2228 if select is DEFAULT: 2229 self._select = current.deployment_settings.get_L10n_languages() 2230 else: 2231 self._select = select 2232 self.translate = translate
2233 2234 # -------------------------------------------------------------------------
2235 - def options(self, zero=True):
2236 """ 2237 Get the options for the selector. This could be only a subset 2238 of all valid options (self._select), therefore overriding 2239 superclass function here. 2240 """ 2241 2242 language_codes = self.language_codes() 2243 2244 if self._select: 2245 language_codes_dict = dict(language_codes) 2246 if self.translate: 2247 T = current.T 2248 items = ((k, T(v)) for k, v in self._select.items() 2249 if k in language_codes_dict) 2250 else: 2251 items = ((k, v) for k, v in self._select.items() 2252 if k in language_codes_dict) 2253 else: 2254 if self.translate: 2255 T = current.T 2256 items = ((k, T(v)) for k, v in language_codes) 2257 else: 2258 items = language_codes 2259 2260 if self.sort: 2261 items = sorted(items, key=lambda s: s3_unicode(s[1]).lower()) 2262 else: 2263 items = list(items) 2264 2265 if zero and not self.zero is None and not self.multiple: 2266 items.insert(0, ("", self.zero)) 2267 2268 return items
2269 2270 # -------------------------------------------------------------------------
2271 - def represent(self, code):
2272 """ 2273 Represent a language code by language name, uses the 2274 representation from deployment_settings if available 2275 (to allow overrides). 2276 2277 @param code: the language code 2278 """ 2279 2280 if not code: 2281 return current.messages["NONE"] 2282 2283 l10n_languages = current.deployment_settings.get_L10n_languages() 2284 name = l10n_languages.get(code) 2285 if not name: 2286 name = dict(self.language_codes()).get(code.split("-")[0]) 2287 if name is None: 2288 return current.messages.UNKNOWN_OPT 2289 2290 if self.translate: 2291 name = current.T(name) 2292 2293 return name
2294 2295 # ------------------------------------------------------------------------- 2296 @classmethod
2297 - def represent_local(cls, code):
2298 """ 2299 Represent a language code by the name of the language in that 2300 language. e.g. for Use in a Language dropdown 2301 2302 @param code: the language code 2303 """ 2304 2305 if not code: 2306 return current.messages["NONE"] 2307 2308 l10n_languages = current.deployment_settings.get_L10n_languages() 2309 name = l10n_languages.get(code) 2310 if not name: 2311 name = dict(cls.language_codes()).get(code.split("-")[0]) 2312 if name is None: 2313 return current.messages.UNKNOWN_OPT 2314 2315 T = current.T 2316 name = s3_str(T(name, language=code)) 2317 2318 return name
2319 2320 # ------------------------------------------------------------------------- 2321 @staticmethod
2322 - def language_codes():
2323 """ 2324 Returns a list of tuples of ISO639-1 alpha-2 language 2325 codes, can also be used to look up the language name 2326 2327 Just the subset which are useful for Translations 2328 - 2 letter code preferred, 3-letter code where none exists, 2329 no 'families' or Old 2330 """ 2331 2332 lang = [#("aar", "Afar"), 2333 ("aa", "Afar"), 2334 #("abk", "Abkhazian"), 2335 ("ab", "Abkhazian"), 2336 ("ace", "Achinese"), 2337 ("ach", "Acoli"), 2338 ("ada", "Adangme"), 2339 ("ady", "Adyghe; Adygei"), 2340 #("afa", "Afro-Asiatic languages"), 2341 ("afh", "Afrihili"), 2342 #("afr", "Afrikaans"), 2343 ("af", "Afrikaans"), 2344 ("ain", "Ainu"), 2345 #("aka", "Akan"), 2346 ("ak", "Akan"), 2347 ("akk", "Akkadian"), 2348 #("alb", "Albanian"), 2349 ("sq", "Albanian"), 2350 ("ale", "Aleut"), 2351 #("alg", "Algonquian languages"), 2352 ("alt", "Southern Altai"), 2353 #("amh", "Amharic"), 2354 ("am", "Amharic"), 2355 #("ang", "English, Old (ca.450-1100)"), 2356 ("anp", "Angika"), 2357 #("apa", "Apache languages"), 2358 #("ara", "Arabic"), 2359 ("ar", "Arabic"), 2360 #("arc", "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)"), 2361 #("arg", "Aragonese"), 2362 ("an", "Aragonese"), 2363 #("arm", "Armenian"), 2364 ("hy", "Armenian"), 2365 ("arn", "Mapudungun; Mapuche"), 2366 ("arp", "Arapaho"), 2367 #("art", "Artificial languages"), 2368 ("arw", "Arawak"), 2369 #("asm", "Assamese"), 2370 ("as", "Assamese"), 2371 ("ast", "Asturian; Bable; Leonese; Asturleonese"), 2372 #("ath", "Athapascan languages"), 2373 #("aus", "Australian languages"), 2374 #("ava", "Avaric"), 2375 ("av", "Avaric"), 2376 #("ave", "Avestan"), 2377 ("ae", "Avestan"), 2378 ("awa", "Awadhi"), 2379 #("aym", "Aymara"), 2380 ("ay", "Aymara"), 2381 #("aze", "Azerbaijani"), 2382 ("az", "Azerbaijani"), 2383 #("bad", "Banda languages"), 2384 #("bai", "Bamileke languages"), 2385 #("bak", "Bashkir"), 2386 ("ba", "Bashkir"), 2387 ("bal", "Baluchi"), 2388 #("bam", "Bambara"), 2389 ("bm", "Bambara"), 2390 ("ban", "Balinese"), 2391 #("baq", "Basque"), 2392 ("eu", "Basque"), 2393 ("bas", "Basa"), 2394 #("bat", "Baltic languages"), 2395 ("bej", "Beja; Bedawiyet"), 2396 #("bel", "Belarusian"), 2397 ("be", "Belarusian"), 2398 ("bem", "Bemba"), 2399 #("ben", "Bengali"), 2400 ("bn", "Bengali"), 2401 #("ber", "Berber languages"), 2402 ("bho", "Bhojpuri"), 2403 #("bih", "Bihari languages"), 2404 #("bh", "Bihari languages"), 2405 ("bik", "Bikol"), 2406 ("bin", "Bini; Edo"), 2407 #("bis", "Bislama"), 2408 ("bi", "Bislama"), 2409 ("bla", "Siksika"), 2410 #("bnt", "Bantu (Other)"), 2411 #("bos", "Bosnian"), 2412 ("bs", "Bosnian"), 2413 ("bra", "Braj"), 2414 #("bre", "Breton"), 2415 ("br", "Breton"), 2416 #("btk", "Batak languages"), 2417 ("bua", "Buriat"), 2418 ("bug", "Buginese"), 2419 #("bul", "Bulgarian"), 2420 ("bg", "Bulgarian"), 2421 #("bur", "Burmese"), 2422 ("my", "Burmese"), 2423 ("byn", "Blin; Bilin"), 2424 ("cad", "Caddo"), 2425 #("cai", "Central American Indian languages"), 2426 ("car", "Galibi Carib"), 2427 #("cat", "Catalan; Valencian"), 2428 ("ca", "Catalan; Valencian"), 2429 #("cau", "Caucasian languages"), 2430 ("ceb", "Cebuano"), 2431 #("cel", "Celtic languages"), 2432 #("cha", "Chamorro"), 2433 ("ch", "Chamorro"), 2434 ("chb", "Chibcha"), 2435 #("che", "Chechen"), 2436 ("ce", "Chechen"), 2437 ("chg", "Chagatai"), 2438 #("chi", "Chinese"), 2439 ("zh", "Chinese"), 2440 ("chk", "Chuukese"), 2441 ("chm", "Mari"), 2442 ("chn", "Chinook jargon"), 2443 ("cho", "Choctaw"), 2444 ("chp", "Chipewyan; Dene Suline"), 2445 ("chr", "Cherokee"), 2446 #("chu", "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"), 2447 ("cu", "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"), 2448 #("chv", "Chuvash"), 2449 ("cv", "Chuvash"), 2450 ("chy", "Cheyenne"), 2451 #("cmc", "Chamic languages"), 2452 ("cop", "Coptic"), 2453 #("cor", "Cornish"), 2454 ("kw", "Cornish"), 2455 #("cos", "Corsican"), 2456 ("co", "Corsican"), 2457 #("cpe", "Creoles and pidgins, English based"), 2458 #("cpf", "Creoles and pidgins, French-based"), 2459 #("cpp", "Creoles and pidgins, Portuguese-based"), 2460 #("cre", "Cree"), 2461 ("cr", "Cree"), 2462 ("crh", "Crimean Tatar; Crimean Turkish"), 2463 #("crp", "Creoles and pidgins"), 2464 ("csb", "Kashubian"), 2465 ("cus", "Cushitic languages"), 2466 #("cze", "Czech"), 2467 ("cs", "Czech"), 2468 ("dak", "Dakota"), 2469 #("dan", "Danish"), 2470 ("da", "Danish"), 2471 ("dar", "Dargwa"), 2472 #("day", "Land Dayak languages"), 2473 ("del", "Delaware"), 2474 ("den", "Slave (Athapascan)"), 2475 ("dgr", "Dogrib"), 2476 ("din", "Dinka"), 2477 #("div", "Divehi; Dhivehi; Maldivian"), 2478 ("dv", "Divehi; Dhivehi; Maldivian"), 2479 ("doi", "Dogri"), 2480 #("dra", "Dravidian languages"), 2481 ("dsb", "Lower Sorbian"), 2482 ("dua", "Duala"), 2483 #("dum", "Dutch, Middle (ca.1050-1350)"), 2484 #("dut", "Dutch; Flemish"), 2485 ("nl", "Dutch; Flemish"), 2486 ("dyu", "Dyula"), 2487 #("dzo", "Dzongkha"), 2488 ("dz", "Dzongkha"), 2489 ("efi", "Efik"), 2490 #("egy", "Egyptian (Ancient)"), 2491 ("eka", "Ekajuk"), 2492 ("elx", "Elamite"), 2493 #("eng", "English"), 2494 ("en", "English"), 2495 #("enm", "English, Middle (1100-1500)"), 2496 #("epo", "Esperanto"), 2497 ("eo", "Esperanto"), 2498 #("est", "Estonian"), 2499 ("et", "Estonian"), 2500 #("ewe", "Ewe"), 2501 ("ee", "Ewe"), 2502 ("ewo", "Ewondo"), 2503 ("fan", "Fang"), 2504 #("fao", "Faroese"), 2505 ("fo", "Faroese"), 2506 ("fat", "Fanti"), 2507 #("fij", "Fijian"), 2508 ("fj", "Fijian"), 2509 ("fil", "Filipino; Pilipino"), 2510 #("fin", "Finnish"), 2511 ("fi", "Finnish"), 2512 #("fiu", "Finno-Ugrian languages"), 2513 ("fon", "Fon"), 2514 #("fre", "French"), 2515 ("fr", "French"), 2516 #("frm", "French, Middle (ca.1400-1600)"), 2517 #("fro", "French, Old (842-ca.1400)"), 2518 ("frr", "Northern Frisian"), 2519 ("frs", "Eastern Frisian"), 2520 #("fry", "Western Frisian"), 2521 ("fy", "Western Frisian"), 2522 #("ful", "Fulah"), 2523 ("ff", "Fulah"), 2524 ("fur", "Friulian"), 2525 ("gaa", "Ga"), 2526 ("gay", "Gayo"), 2527 ("gba", "Gbaya"), 2528 #("gem", "Germanic languages"), 2529 #("geo", "Georgian"), 2530 ("ka", "Georgian"), 2531 #("ger", "German"), 2532 ("de", "German"), 2533 ("gez", "Geez"), 2534 ("gil", "Gilbertese"), 2535 #("gla", "Gaelic; Scottish Gaelic"), 2536 ("gd", "Gaelic; Scottish Gaelic"), 2537 #("gle", "Irish"), 2538 ("ga", "Irish"), 2539 #("glg", "Galician"), 2540 ("gl", "Galician"), 2541 #("glv", "Manx"), 2542 ("gv", "Manx"), 2543 #("gmh", "German, Middle High (ca.1050-1500)"), 2544 #("goh", "German, Old High (ca.750-1050)"), 2545 ("gon", "Gondi"), 2546 ("gor", "Gorontalo"), 2547 ("got", "Gothic"), 2548 ("grb", "Grebo"), 2549 #("grc", "Greek, Ancient (to 1453)"), 2550 #("gre", "Greek, Modern (1453-)"), 2551 ("el", "Greek"), # "Greek, Modern (1453-)" 2552 #("grn", "Guarani"), 2553 ("gn", "Guarani"), 2554 ("gsw", "Swiss German; Alemannic; Alsatian"), 2555 #("guj", "Gujarati"), 2556 ("gu", "Gujarati"), 2557 ("gwi", "Gwich'in"), 2558 ("hai", "Haida"), 2559 #("hat", "Haitian; Haitian Creole"), 2560 ("ht", "Haitian; Haitian Creole"), 2561 #("hau", "Hausa"), 2562 ("ha", "Hausa"), 2563 ("haw", "Hawaiian"), 2564 #("heb", "Hebrew"), 2565 ("he", "Hebrew"), 2566 #("her", "Herero"), 2567 ("hz", "Herero"), 2568 ("hil", "Hiligaynon"), 2569 #("him", "Himachali languages; Western Pahari languages"), 2570 #("hin", "Hindi"), 2571 ("hi", "Hindi"), 2572 ("hit", "Hittite"), 2573 ("hmn", "Hmong; Mong"), 2574 #("hmo", "Hiri Motu"), 2575 ("ho", "Hiri Motu"), 2576 #("hrv", "Croatian"), 2577 ("hr", "Croatian"), 2578 ("hsb", "Upper Sorbian"), 2579 #("hun", "Hungarian"), 2580 ("hu", "Hungarian"), 2581 ("hup", "Hupa"), 2582 ("iba", "Iban"), 2583 #("ibo", "Igbo"), 2584 ("ig", "Igbo"), 2585 #("ice", "Icelandic"), 2586 ("is", "Icelandic"), 2587 #("ido", "Ido"), 2588 ("io", "Ido"), 2589 #("iii", "Sichuan Yi; Nuosu"), 2590 ("ii", "Sichuan Yi; Nuosu"), 2591 #("ijo", "Ijo languages"), 2592 #("iku", "Inuktitut"), 2593 ("iu", "Inuktitut"), 2594 #("ile", "Interlingue; Occidental"), 2595 ("ie", "Interlingue; Occidental"), 2596 ("ilo", "Iloko"), 2597 #("ina", "Interlingua (International Auxiliary Language Association)"), 2598 ("ia", "Interlingua (International Auxiliary Language Association)"), 2599 #("inc", "Indic languages"), 2600 #("ind", "Indonesian"), 2601 ("id", "Indonesian"), 2602 #("ine", "Indo-European languages"), 2603 ("inh", "Ingush"), 2604 #("ipk", "Inupiaq"), 2605 ("ik", "Inupiaq"), 2606 #("ira", "Iranian languages"), 2607 #("iro", "Iroquoian languages"), 2608 #("ita", "Italian"), 2609 ("it", "Italian"), 2610 #("jav", "Javanese"), 2611 ("jv", "Javanese"), 2612 ("jbo", "Lojban"), 2613 #("jpn", "Japanese"), 2614 ("ja", "Japanese"), 2615 #("jpr", "Judeo-Persian"), 2616 #("jrb", "Judeo-Arabic"), 2617 ("kaa", "Kara-Kalpak"), 2618 ("kab", "Kabyle"), 2619 ("kac", "Kachin; Jingpho"), 2620 #("kal", "Kalaallisut; Greenlandic"), 2621 ("kl", "Kalaallisut; Greenlandic"), 2622 ("kam", "Kamba"), 2623 #("kan", "Kannada"), 2624 ("kn", "Kannada"), 2625 #("kar", "Karen languages"), 2626 #("kas", "Kashmiri"), 2627 ("ks", "Kashmiri"), 2628 #("kau", "Kanuri"), 2629 ("kr", "Kanuri"), 2630 ("kaw", "Kawi"), 2631 #("kaz", "Kazakh"), 2632 ("kk", "Kazakh"), 2633 ("kbd", "Kabardian"), 2634 ("kha", "Khasi"), 2635 #("khi", "Khoisan languages"), 2636 #("khm", "Central Khmer"), 2637 ("km", "Central Khmer"), 2638 ("kho", "Khotanese; Sakan"), 2639 #("kik", "Kikuyu; Gikuyu"), 2640 ("ki", "Kikuyu; Gikuyu"), 2641 #("kin", "Kinyarwanda"), 2642 ("rw", "Kinyarwanda"), 2643 #("kir", "Kirghiz; Kyrgyz"), 2644 ("ky", "Kirghiz; Kyrgyz"), 2645 ("kmb", "Kimbundu"), 2646 ("kok", "Konkani"), 2647 #("kom", "Komi"), 2648 ("kv", "Komi"), 2649 #("kon", "Kongo"), 2650 ("kg", "Kongo"), 2651 #("kor", "Korean"), 2652 ("ko", "Korean"), 2653 ("kos", "Kosraean"), 2654 ("kpe", "Kpelle"), 2655 ("krc", "Karachay-Balkar"), 2656 ("krl", "Karelian"), 2657 #("kro", "Kru languages"), 2658 ("kru", "Kurukh"), 2659 #("kua", "Kuanyama; Kwanyama"), 2660 ("kj", "Kuanyama; Kwanyama"), 2661 ("kum", "Kumyk"), 2662 #("kur", "Kurdish"), 2663 ("ku", "Kurdish"), 2664 ("kut", "Kutenai"), 2665 ("lad", "Ladino"), 2666 ("lah", "Lahnda"), 2667 ("lam", "Lamba"), 2668 #("lao", "Lao"), 2669 ("lo", "Lao"), 2670 #("lat", "Latin"), 2671 ("la", "Latin"), 2672 #("lav", "Latvian"), 2673 ("lv", "Latvian"), 2674 ("lez", "Lezghian"), 2675 #("lim", "Limburgan; Limburger; Limburgish"), 2676 ("li", "Limburgan; Limburger; Limburgish"), 2677 #("lin", "Lingala"), 2678 ("ln", "Lingala"), 2679 #("lit", "Lithuanian"), 2680 ("lt", "Lithuanian"), 2681 ("lol", "Mongo"), 2682 ("loz", "Lozi"), 2683 #("ltz", "Luxembourgish; Letzeburgesch"), 2684 ("lb", "Luxembourgish; Letzeburgesch"), 2685 ("lua", "Luba-Lulua"), 2686 #("lub", "Luba-Katanga"), 2687 ("lu", "Luba-Katanga"), 2688 #("lug", "Ganda"), 2689 ("lg", "Ganda"), 2690 ("lui", "Luiseno"), 2691 ("lun", "Lunda"), 2692 ("luo", "Luo (Kenya and Tanzania)"), 2693 ("lus", "Lushai"), 2694 #("mac", "Macedonian"), 2695 ("mk", "Macedonian"), 2696 ("mad", "Madurese"), 2697 ("mag", "Magahi"), 2698 #("mah", "Marshallese"), 2699 ("mh", "Marshallese"), 2700 ("mai", "Maithili"), 2701 ("mak", "Makasar"), 2702 #("mal", "Malayalam"), 2703 ("ml", "Malayalam"), 2704 ("man", "Mandingo"), 2705 #("mao", "Maori"), 2706 ("mi", "Maori"), 2707 #("map", "Austronesian languages"), 2708 #("mar", "Marathi"), 2709 ("mr", "Marathi"), 2710 ("mas", "Masai"), 2711 #("may", "Malay"), 2712 ("ms", "Malay"), 2713 ("mdf", "Moksha"), 2714 ("mdr", "Mandar"), 2715 ("men", "Mende"), 2716 #("mga", "Irish, Middle (900-1200)"), 2717 ("mic", "Mi'kmaq; Micmac"), 2718 ("min", "Minangkabau"), 2719 #("mis", "Uncoded languages"), 2720 #("mkh", "Mon-Khmer languages"), 2721 #("mlg", "Malagasy"), 2722 ("mg", "Malagasy"), # Madagascar 2723 ("mlt", "Maltese"), 2724 ("mt", "Maltese"), 2725 ("mnc", "Manchu"), 2726 ("mni", "Manipuri"), 2727 #("mno", "Manobo languages"), 2728 ("moh", "Mohawk"), 2729 #("mon", "Mongolian"), 2730 ("mn", "Mongolian"), 2731 ("mos", "Mossi"), 2732 #("mul", "Multiple languages"), 2733 #("mun", "Munda languages"), 2734 ("mus", "Creek"), 2735 ("mwl", "Mirandese"), 2736 ("mwr", "Marwari"), 2737 #("myn", "Mayan languages"), 2738 ("myv", "Erzya"), 2739 #("nah", "Nahuatl languages"), 2740 #("nai", "North American Indian languages"), 2741 ("nap", "Neapolitan"), 2742 #("nau", "Nauru"), 2743 ("na", "Nauru"), 2744 #("nav", "Navajo; Navaho"), 2745 ("nv", "Navajo; Navaho"), 2746 #("nbl", "Ndebele, South; South Ndebele"), 2747 ("nr", "Ndebele, South; South Ndebele"), 2748 #("nde", "Ndebele, North; North Ndebele"), 2749 ("nd", "Ndebele, North; North Ndebele"), 2750 #("ndo", "Ndonga"), 2751 ("ng", "Ndonga"), 2752 ("nds", "Low German; Low Saxon; German, Low; Saxon, Low"), 2753 #("nep", "Nepali"), 2754 ("ne", "Nepali"), 2755 ("new", "Nepal Bhasa; Newari"), 2756 ("nia", "Nias"), 2757 #("nic", "Niger-Kordofanian languages"), 2758 ("niu", "Niuean"), 2759 #("nno", "Norwegian Nynorsk; Nynorsk, Norwegian"), 2760 ("nn", "Norwegian Nynorsk; Nynorsk, Norwegian"), 2761 #("nob", "Bokmål, Norwegian; Norwegian Bokmål"), 2762 ("nb", "Bokmål, Norwegian; Norwegian Bokmål"), 2763 ("nog", "Nogai"), 2764 #("non", "Norse, Old"), 2765 #("nor", "Norwegian"), 2766 ("no", "Norwegian"), 2767 ("nqo", "N'Ko"), 2768 ("nso", "Pedi; Sepedi; Northern Sotho"), 2769 #("nub", "Nubian languages"), 2770 #("nwc", "Classical Newari; Old Newari; Classical Nepal Bhasa"), 2771 #("nya", "Chichewa; Chewa; Nyanja"), 2772 ("ny", "Chichewa; Chewa; Nyanja"), 2773 ("nym", "Nyamwezi"), 2774 ("nyn", "Nyankole"), 2775 ("nyo", "Nyoro"), 2776 ("nzi", "Nzima"), 2777 #("oci", "Occitan (post 1500); Provençal"), 2778 ("oc", "Occitan (post 1500); Provençal"), 2779 #("oji", "Ojibwa"), 2780 ("oj", "Ojibwa"), 2781 #("ori", "Oriya"), 2782 ("or", "Oriya"), 2783 #("orm", "Oromo"), 2784 ("om", "Oromo"), 2785 ("osa", "Osage"), 2786 #("oss", "Ossetian; Ossetic"), 2787 ("os", "Ossetian; Ossetic"), 2788 #("ota", "Turkish, Ottoman (1500-1928)"), 2789 #("oto", "Otomian languages"), 2790 #("paa", "Papuan languages"), 2791 ("pag", "Pangasinan"), 2792 ("pal", "Pahlavi"), 2793 ("pam", "Pampanga; Kapampangan"), 2794 #("pan", "Panjabi; Punjabi"), 2795 ("pa", "Panjabi; Punjabi"), 2796 ("pap", "Papiamento"), 2797 ("pau", "Palauan"), 2798 #("peo", "Persian, Old (ca.600-400 B.C.)"), 2799 #("per", "Persian"), 2800 ("fa", "Persian"), 2801 #("phi", "Philippine languages"), 2802 ("phn", "Phoenician"), 2803 #("pli", "Pali"), 2804 ("pi", "Pali"), 2805 #("pol", "Polish"), 2806 ("pl", "Polish"), 2807 ("pon", "Pohnpeian"), 2808 #("por", "Portuguese"), 2809 ("pt", "Portuguese"), 2810 #("pra", "Prakrit languages"), 2811 #("pro", "Provençal, Old (to 1500)"), 2812 ("prs", "Dari"), 2813 #("pus", "Pushto; Pashto"), 2814 ("ps", "Pushto; Pashto"), 2815 #("qaa-qtz", "Reserved for local use"), 2816 #("que", "Quechua"), 2817 ("qu", "Quechua"), 2818 ("raj", "Rajasthani"), 2819 ("rap", "Rapanui"), 2820 ("rar", "Rarotongan; Cook Islands Maori"), 2821 #("roa", "Romance languages"), 2822 #("roh", "Romansh"), 2823 ("rm", "Romansh"), 2824 ("rom", "Romany"), 2825 #("rum", "Romanian; Moldavian; Moldovan"), 2826 ("ro", "Romanian; Moldavian; Moldovan"), 2827 #("run", "Rundi"), 2828 ("rn", "Rundi"), 2829 ("rup", "Aromanian; Arumanian; Macedo-Romanian"), 2830 #("rus", "Russian"), 2831 ("ru", "Russian"), 2832 ("sad", "Sandawe"), 2833 #("sag", "Sango"), 2834 ("sg", "Sango"), 2835 ("sah", "Yakut"), 2836 #("sai", "South American Indian (Other)"), 2837 #("sal", "Salishan languages"), 2838 ("sam", "Samaritan Aramaic"), 2839 #("san", "Sanskrit"), 2840 ("sa", "Sanskrit"), 2841 ("sas", "Sasak"), 2842 ("sat", "Santali"), 2843 ("scn", "Sicilian"), 2844 ("sco", "Scots"), 2845 ("sel", "Selkup"), 2846 #("sem", "Semitic languages"), 2847 #("sga", "Irish, Old (to 900)"), 2848 ("sgn", "Sign Languages"), 2849 ("shn", "Shan"), 2850 ("sid", "Sidamo"), 2851 #("sin", "Sinhala; Sinhalese"), 2852 ("si", "Sinhala; Sinhalese"), 2853 #("sio", "Siouan languages"), 2854 #("sit", "Sino-Tibetan languages"), 2855 #("sla", "Slavic languages"), 2856 #("slo", "Slovak"), 2857 ("sk", "Slovak"), 2858 #("slv", "Slovenian"), 2859 ("sl", "Slovenian"), 2860 ("sma", "Southern Sami"), 2861 #("sme", "Northern Sami"), 2862 ("se", "Northern Sami"), 2863 #("smi", "Sami languages"), 2864 ("smj", "Lule Sami"), 2865 ("smn", "Inari Sami"), 2866 #("smo", "Samoan"), 2867 ("sm", "Samoan"), 2868 ("sms", "Skolt Sami"), 2869 #("sna", "Shona"), 2870 ("sn", "Shona"), 2871 #("snd", "Sindhi"), 2872 ("sd", "Sindhi"), 2873 ("snk", "Soninke"), 2874 ("sog", "Sogdian"), 2875 #("som", "Somali"), 2876 ("so", "Somali"), 2877 #("son", "Songhai languages"), 2878 #("sot", "Sotho, Southern"), 2879 ("st", "Sotho, Southern"), 2880 #("spa", "Spanish; Castilian"), 2881 ("es", "Spanish; Castilian"), 2882 #("srd", "Sardinian"), 2883 ("sc", "Sardinian"), 2884 ("srn", "Sranan Tongo"), 2885 #("srp", "Serbian"), 2886 ("sr", "Serbian"), 2887 ("srr", "Serer"), 2888 #("ssa", "Nilo-Saharan languages"), 2889 #("ssw", "Swati"), 2890 ("ss", "Swati"), 2891 ("suk", "Sukuma"), 2892 #("sun", "Sundanese"), 2893 ("su", "Sundanese"), 2894 ("sus", "Susu"), 2895 ("sux", "Sumerian"), 2896 #("swa", "Swahili"), 2897 ("sw", "Swahili"), 2898 #("swe", "Swedish"), 2899 ("sv", "Swedish"), 2900 #("syc", "Classical Syriac"), 2901 ("syr", "Syriac"), 2902 #("tah", "Tahitian"), 2903 ("ty", "Tahitian"), 2904 #("tai", "Tai languages"), 2905 #("tam", "Tamil"), 2906 ("ta", "Tamil"), 2907 #("tat", "Tatar"), 2908 ("tt", "Tatar"), 2909 #("tel", "Telugu"), 2910 ("te", "Telugu"), 2911 ("tem", "Timne"), 2912 ("ter", "Tereno"), 2913 ("tet", "Tetum"), 2914 #("tgk", "Tajik"), 2915 ("tg", "Tajik"), 2916 #("tgl", "Tagalog"), 2917 ("tl", "Tagalog"), 2918 #("tha", "Thai"), 2919 ("th", "Thai"), 2920 #("tib", "Tibetan"), 2921 ("bo", "Tibetan"), 2922 ("tig", "Tigre"), 2923 #("tir", "Tigrinya"), 2924 ("ti", "Tigrinya"), 2925 ("tiv", "Tiv"), 2926 ("tkl", "Tokelau"), 2927 #("tlh", "Klingon; tlhIngan-Hol"), 2928 ("tli", "Tlingit"), 2929 ("tmh", "Tamashek"), 2930 ("tog", "Tonga (Nyasa)"), 2931 #("ton", "Tonga (Tonga Islands)"), 2932 ("to", "Tonga (Tonga Islands)"), 2933 ("tpi", "Tok Pisin"), 2934 ("tsi", "Tsimshian"), 2935 #("tsn", "Tswana"), 2936 ("tn", "Tswana"), 2937 #("tso", "Tsonga"), 2938 ("ts", "Tsonga"), 2939 #("tuk", "Turkmen"), 2940 ("tk", "Turkmen"), 2941 ("tum", "Tumbuka"), 2942 #("tup", "Tupi languages"), 2943 #("tur", "Turkish"), 2944 ("tr", "Turkish"), 2945 #("tut", "Altaic languages"), 2946 ("tvl", "Tuvalu"), 2947 #("twi", "Twi"), 2948 ("tw", "Twi"), 2949 ("tyv", "Tuvinian"), 2950 ("udm", "Udmurt"), 2951 ("uga", "Ugaritic"), 2952 #("uig", "Uighur; Uyghur"), 2953 ("ug", "Uighur; Uyghur"), 2954 #("ukr", "Ukrainian"), 2955 ("uk", "Ukrainian"), 2956 ("umb", "Umbundu"), 2957 #("und", "Undetermined"), 2958 #("urd", "Urdu"), 2959 ("ur", "Urdu"), 2960 #("uzb", "Uzbek"), 2961 ("uz", "Uzbek"), 2962 ("vai", "Vai"), 2963 #("ven", "Venda"), 2964 ("ve", "Venda"), 2965 #("vie", "Vietnamese"), 2966 ("vi", "Vietnamese"), 2967 #("vol", "Volapük"), 2968 ("vo", "Volapük"), 2969 ("vot", "Votic"), 2970 #("wak", "Wakashan languages"), 2971 ("wal", "Walamo"), 2972 ("war", "Waray"), 2973 ("was", "Washo"), 2974 #("wel", "Welsh"), 2975 ("cy", "Welsh"), 2976 #("wen", "Sorbian languages"), 2977 #("wln", "Walloon"), 2978 ("wa", "Walloon"), 2979 #("wol", "Wolof"), 2980 ("wo", "Wolof"), 2981 ("xal", "Kalmyk; Oirat"), 2982 #("xho", "Xhosa"), 2983 ("xh", "Xhosa"), 2984 ("yao", "Yao"), 2985 ("yap", "Yapese"), 2986 #("yid", "Yiddish"), 2987 ("yi", "Yiddish"), 2988 #("yor", "Yoruba"), 2989 ("yo", "Yoruba"), 2990 #("ypk", "Yupik languages"), 2991 ("zap", "Zapotec"), 2992 #("zbl", "Blissymbols; Blissymbolics; Bliss"), 2993 ("zen", "Zenaga"), 2994 ("zgh", "Standard Moroccan Tamazight"), 2995 #("zha", "Zhuang; Chuang"), 2996 ("za", "Zhuang; Chuang"), 2997 #("znd", "Zande languages"), 2998 #("zul", "Zulu"), 2999 ("zu", "Zulu"), 3000 ("zun", "Zuni"), 3001 #("zxx", "No linguistic content; Not applicable"), 3002 ("zza", "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"), 3003 ] 3004 3005 settings = current.deployment_settings 3006 3007 l10n_languages = settings.get_L10n_languages() 3008 lang += l10n_languages.items() 3009 3010 extra_codes = settings.get_L10n_extra_codes() 3011 if extra_codes: 3012 lang += extra_codes 3013 3014 return list(set(lang)) # Remove duplicates
3015 3016 # END ========================================================================= 3017