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

Source Code for Module s3.s3organizer

  1  # -*- coding: utf-8 -*- 
  2   
  3  """ S3 Organizer (Calendar-based CRUD) 
  4   
  5      @copyright: 2018-2019 (c) Sahana Software Foundation 
  6      @license: MIT 
  7   
  8      Permission is hereby granted, free of charge, to any person 
  9      obtaining a copy of this software and associated documentation 
 10      files (the "Software"), to deal in the Software without 
 11      restriction, including without limitation the rights to use, 
 12      copy, modify, merge, publish, distribute, sublicense, and/or sell 
 13      copies of the Software, and to permit persons to whom the 
 14      Software is furnished to do so, subject to the following 
 15      conditions: 
 16   
 17      The above copyright notice and this permission notice shall be 
 18      included in all copies or substantial portions of the Software. 
 19   
 20      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 21      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 22      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 23      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 24      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 25      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 26      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 27      OTHER DEALINGS IN THE SOFTWARE. 
 28  """ 
 29   
 30  __all__ = ("S3Organizer", 
 31             "S3OrganizerWidget", 
 32             ) 
 33   
 34  import datetime 
 35  try: 
 36      import dateutil 
 37      import dateutil.tz 
 38  except ImportError: 
 39      import sys 
 40      sys.stderr.write("ERROR: python-dateutil module needed for date handling\n") 
 41      raise 
 42  import json 
 43  import os 
 44  import uuid 
 45   
 46  from gluon import current, DIV, INPUT 
 47  from gluon.storage import Storage 
 48   
 49  from s3datetime import s3_decode_iso_datetime 
 50  from s3rest import S3Method 
 51  from s3utils import s3_str 
 52  from s3validators import JSONERRORS 
 53  from s3widgets import S3DateWidget 
54 55 # ============================================================================= 56 -class S3Organizer(S3Method):
57 """ Calendar-based CRUD Method """ 58 59 # -------------------------------------------------------------------------
60 - def apply_method(self, r, **attr):
61 """ 62 Page-render entry point for REST interface. 63 64 @param r: the S3Request instance 65 @param attr: controller attributes 66 """ 67 68 output = {} 69 if r.http == "GET": 70 if r.representation == "json": 71 output = self.get_json_data(r, **attr) 72 elif r.interactive: 73 output = self.organizer(r, **attr) 74 else: 75 r.error(415, current.ERROR.BAD_FORMAT) 76 elif r.http == "POST": 77 if r.representation == "json": 78 output = self.update_json(r, **attr) 79 else: 80 r.error(415, current.ERROR.BAD_FORMAT) 81 else: 82 r.error(405, current.ERROR.BAD_METHOD) 83 84 return output
85 86 # -------------------------------------------------------------------------
87 - def organizer(self, r, **attr):
88 """ 89 Render the organizer view (HTML method) 90 91 @param r: the S3Request instance 92 @param attr: controller attributes 93 94 @returns: dict of values for the view 95 """ 96 97 output = {} 98 99 resource = self.resource 100 get_config = resource.get_config 101 102 # Parse resource configuration 103 config = self.parse_config(resource) 104 start = config["start"] 105 end = config["end"] 106 107 widget_id = "organizer" 108 109 # Filter Defaults 110 hide_filter = self.hide_filter 111 filter_widgets = get_config("filter_widgets", None) 112 113 show_filter_form = False 114 default_filters = None 115 116 if filter_widgets and not hide_filter: 117 118 # Drop all filter widgets for start/end fields 119 # (so they don't clash with the organizer's own filters) 120 fw = [] 121 prefix_selector = self.prefix_selector 122 for filter_widget in filter_widgets: 123 if not filter_widget: 124 continue 125 filter_field = filter_widget.field 126 if isinstance(filter_field, basestring): 127 filter_field = prefix_selector(resource, filter_field) 128 if start and start.selector == filter_field or \ 129 end and end.selector == filter_field: 130 continue 131 fw.append(filter_widget) 132 filter_widgets = fw 133 134 if filter_widgets: 135 show_filter_form = True 136 # Apply filter defaults (before rendering the data!) 137 from s3filter import S3FilterForm 138 default_filters = S3FilterForm.apply_filter_defaults(r, resource) 139 140 # Filter Form 141 if show_filter_form: 142 143 get_vars = r.get_vars 144 145 # Where to retrieve filtered data from 146 filter_submit_url = attr.get("filter_submit_url") 147 if not filter_submit_url: 148 get_vars_ = self._remove_filters(get_vars) 149 filter_submit_url = r.url(vars=get_vars_) 150 151 # Where to retrieve updated filter options from: 152 filter_ajax_url = attr.get("filter_ajax_url") 153 if filter_ajax_url is None: 154 filter_ajax_url = r.url(method = "filter", 155 vars = {}, 156 representation = "options", 157 ) 158 159 filter_clear = get_config("filter_clear", 160 current.deployment_settings.get_ui_filter_clear()) 161 filter_formstyle = get_config("filter_formstyle", None) 162 filter_submit = get_config("filter_submit", True) 163 filter_form = S3FilterForm(filter_widgets, 164 clear = filter_clear, 165 formstyle = filter_formstyle, 166 submit = filter_submit, 167 ajax = True, 168 url = filter_submit_url, 169 ajaxurl = filter_ajax_url, 170 _class = "filter-form", 171 _id = "%s-filter-form" % widget_id 172 ) 173 fresource = current.s3db.resource(resource.tablename) # Use a clean resource 174 alias = resource.alias if r.component else None 175 output["list_filter_form"] = filter_form.html(fresource, 176 get_vars, 177 target = widget_id, 178 alias = alias 179 ) 180 else: 181 # Render as empty string to avoid the exception in the view 182 output["list_filter_form"] = "" 183 184 # Page Title 185 crud_string = self.crud_string 186 if r.representation != "iframe": 187 if r.component: 188 title = crud_string(r.tablename, "title_display") 189 else: 190 title = crud_string(self.tablename, "title_list") 191 output["title"] = title 192 193 # Configure Resource 194 permitted = self._permitted 195 resource_config = {"ajaxURL": r.url(representation="json"), 196 "useTime": config.get("use_time"), 197 "baseURL": r.url(method=""), 198 "labelCreate": s3_str(crud_string(self.tablename, "label_create")), 199 "insertable": resource.get_config("insertable", True) and \ 200 permitted("create"), 201 "editable": resource.get_config("editable", True) and \ 202 permitted("update"), 203 "startEditable": start.field and start.field.writable, 204 "durationEditable": end and end.field and end.field.writable, 205 "deletable": resource.get_config("deletable", True) and \ 206 permitted("delete"), 207 # Forced reload on update, e.g. if onaccept changes 208 # other data that are visible in the organizer 209 "reloadOnUpdate": config.get("reload_on_update", False), 210 } 211 212 # Start and End Field 213 resource_config["start"] = start.selector if start else None 214 resource_config["end"] = end.selector if end else None 215 216 # Description Labels 217 labels = [] 218 for rfield in config["description"]: 219 label = rfield.label 220 if label is not None: 221 label = s3_str(label) 222 labels.append((rfield.colname, label)) 223 resource_config["columns"] = labels 224 225 # Colors 226 color = config.get("color") 227 if color: 228 resource_config["color"] = color.colname 229 resource_config["colors"] = config.get("colors") 230 231 # Generate form key 232 formkey = uuid.uuid4().get_hex() 233 234 # Store form key in session 235 session = current.session 236 keyname = "_formkey[%s]" % self.formname(r) 237 session[keyname] = session.get(keyname, [])[-9:] + [formkey] 238 239 # Instantiate Organizer Widget 240 widget = S3OrganizerWidget([resource_config]) 241 output["organizer"] = widget.html(widget_id = widget_id, 242 formkey = formkey, 243 ) 244 245 # View 246 current.response.view = self._view(r, "organize.html") 247 248 return output
249 250 # -------------------------------------------------------------------------
251 - def get_json_data(self, r, **attr):
252 """ 253 Extract the resource data and return them as JSON (Ajax method) 254 255 @param r: the S3Request instance 256 @param attr: controller attributes 257 258 TODO correct documentation! 259 @returns: JSON string containing an array of items, format: 260 [{"id": the record ID, 261 "title": the record title, 262 "start": start date as ISO8601 string, 263 "end": end date as ISO8601 string (if resource has end dates), 264 "description": array of item values to render a description, 265 TODO: 266 "editable": item date/duration can be changed (true|false), 267 "deletable": item can be deleted (true|false), 268 }, 269 ... 270 ] 271 """ 272 273 db = current.db 274 auth = current.auth 275 276 resource = self.resource 277 table = resource.table 278 id_col = str(resource._id) 279 280 config = self.parse_config(resource) 281 282 # Determine fields to load 283 fields = [resource._id.name] 284 285 start_rfield = config["start"] 286 fields.append(start_rfield) 287 288 end_rfield = config["end"] 289 if end_rfield: 290 fields.append(end_rfield) 291 292 represent = config["title"] 293 if hasattr(represent, "selector"): 294 title_field = represent.colname 295 fields.append(represent) 296 else: 297 title_field = None 298 299 description = config["description"] 300 if description: 301 fields.extend(description) 302 columns = [rfield.colname for rfield in description] 303 else: 304 columns = None 305 306 color = config["color"] 307 if color: 308 fields.append(color) 309 310 # Add date filter 311 start, end = self.parse_interval(r.get_vars.get("$interval")) 312 if start and end: 313 from s3query import FS 314 start_fs = FS(start_rfield.selector) 315 if not end_rfield: 316 query = (start_fs >= start) & (start_fs < end) 317 else: 318 end_fs = FS(end_rfield.selector) 319 query = (start_fs < end) & (end_fs >= start) | \ 320 (start_fs >= start) & (start_fs < end) & (end_fs == None) 321 resource.add_filter(query) 322 else: 323 r.error(400, "Invalid interval parameter") 324 325 # Extract the records 326 data = resource.select(fields, 327 limit = None, 328 raw_data = True, 329 represent = True, 330 ) 331 rows = data.rows 332 333 # Bulk-represent the records 334 record_ids = [row._row[id_col] for row in rows] 335 if hasattr(represent, "bulk"): 336 representations = represent.bulk(record_ids) 337 else: 338 representations = None 339 340 # Determine which records can be updated/deleted 341 query = table.id.belongs(record_ids) 342 343 q = query & auth.s3_accessible_query("update", table) 344 accessible_rows = db(q).select(table._id, 345 limitby = (0, len(record_ids)), 346 ) 347 editable = set(row[id_col] for row in accessible_rows) 348 349 q = query & auth.s3_accessible_query("delete", table) 350 accessible_rows = db(q).select(table._id, 351 limitby = (0, len(record_ids)), 352 ) 353 deletable = set(row[id_col] for row in accessible_rows) 354 355 # Encode the items 356 items = [] 357 for row in rows: 358 359 raw = row._row 360 record_id = raw[id_col] 361 362 # Get the start date 363 if start_rfield: 364 start_date = self.isoformat(raw[start_rfield.colname]) 365 else: 366 start_date = None 367 if start_date is None: 368 # Undated item => skip 369 continue 370 371 # Construct item title 372 if title_field: 373 title = row[title_field] 374 elif representations: 375 title = representations.get(record_id) 376 elif callable(represent): 377 title = represent(record_id) 378 else: 379 # Fallback: record ID 380 title = row[id_col] 381 382 # Build the item 383 item = {"id": record_id, 384 "t": s3_str(title), 385 "s": start_date, 386 "pe": 1 if record_id in editable else 0, 387 "pd": 1 if record_id in deletable else 0, 388 } 389 390 if end_rfield: 391 end_date = self.isoformat(raw[end_rfield.colname]) 392 item["e"] = end_date 393 394 if columns: 395 data = [] 396 for colname in columns: 397 value = row[colname] 398 if value is not None: 399 value = s3_str(value) 400 data.append(value) 401 item["d"] = data 402 403 if color: 404 item["c"] = raw[color.colname] 405 406 items.append(item) 407 408 return json.dumps({"c": columns, "r": items})
409 410 # -------------------------------------------------------------------------
411 - def update_json(self, r, **attr):
412 """ 413 Update or delete calendar items (Ajax method) 414 415 @param r: the S3Request instance 416 @param attr: controller attributes 417 """ 418 419 # Read+parse body JSON 420 s = r.body 421 s.seek(0) 422 try: 423 options = json.load(s) 424 except JSONERRORS: 425 options = None 426 if not isinstance(options, dict): 427 r.error(400, "Invalid request options") 428 429 # Verify formkey 430 keyname = "_formkey[%s]" % self.formname(r) 431 formkey = options.get("k") 432 if not formkey or formkey not in current.session.get(keyname, []): 433 r.error(403, current.ERROR.NOT_PERMITTED) 434 435 resource = self.resource 436 tablename = resource.tablename 437 438 # Updates 439 items = options.get("u") 440 if items and type(items) is list: 441 442 # Error if resource is not editable 443 if not resource.get_config("editable", True): 444 r.error(403, current.ERROR.NOT_PERMITTED) 445 446 # Parse the organizer-config of the target resource 447 config = self.parse_config(resource) 448 start = config.get("start") 449 if start: 450 if not start.field or start.tname != tablename: 451 # Field must be in target resource 452 # TODO support fields in subtables 453 start = None 454 end = config.get("end") 455 if end: 456 if not end.field or end.tname != tablename: 457 # Field must be in target resource 458 # TODO support fields in subtables 459 end = None 460 461 # Resource details 462 db = current.db 463 table = resource.table 464 prefix, name = resource.prefix, resource.name 465 466 # Model methods 467 s3db = current.s3db 468 onaccept = s3db.onaccept 469 update_super = s3db.update_super 470 471 # Auth methods 472 auth = current.auth 473 audit = current.audit 474 permitted = auth.s3_has_permission 475 set_realm_entity = auth.set_realm_entity 476 477 # Process the updates 478 for item in items: 479 480 # Get the record ID 481 record_id = item.get("id") 482 if not record_id: 483 continue 484 485 # Check permission to update the record 486 if not permitted("update", table, record_id=record_id): 487 r.unauthorised() 488 489 # Collect and validate the update-data 490 data = {} 491 error = None 492 if "s" in item: 493 if not start: 494 error = "Event start not editable" 495 else: 496 try: 497 dt = s3_decode_iso_datetime(item["s"]) 498 except ValueError: 499 error = "Invalid start date" 500 if start.field: 501 dt, error = start.field.validate(dt) 502 data[start.fname] = dt 503 if not error and "e" in item: 504 if not end: 505 error = "Event end not editable" 506 else: 507 try: 508 dt = s3_decode_iso_datetime(item["e"]) 509 except ValueError: 510 error = "Invalid end date" 511 if end.field: 512 dt, error = end.field.validate(dt) 513 data[end.fname] = dt 514 if error: 515 r.error(400, error) 516 517 # Update the record, postprocess update 518 if data: 519 success = db(table._id == record_id).update(**data) 520 if not success: 521 r.error(400, "Failed to update %s#%s" % (tablename, record_id)) 522 else: 523 data[table._id.name] = record_id 524 525 # Audit update 526 audit("update", prefix, name, 527 record=record_id, representation="json") 528 # Update super entity links 529 update_super(table, data) 530 # Update realm 531 if resource.get_config("update_realm"): 532 set_realm_entity(table, record_id, force_update=True) 533 # Onaccept 534 onaccept(table, data, method="update") 535 536 # Deletions 537 items = options.get("d") 538 if items and type(items) is list: 539 540 # Error if resource is not deletable 541 if not resource.get_config("deletable", True): 542 r.error(403, current.ERROR.NOT_PERMITTED) 543 544 # Collect record IDs 545 delete_ids = [] 546 for item in items: 547 record_id = item.get("id") 548 if not record_id: 549 continue 550 delete_ids.append(record_id) 551 552 # Delete the records 553 # (includes permission check, audit and ondelete-postprocess) 554 if delete_ids: 555 dresource = current.s3db.resource(tablename, id=delete_ids) 556 deleted = dresource.delete(cascade=True) 557 if deleted != len(delete_ids): 558 r.error(400, "Failed to delete %s items" % tablename) 559 560 return current.xml.json_message()
561 562 # ------------------------------------------------------------------------- 563 @classmethod
564 - def parse_config(cls, resource):
565 """ 566 Parse the resource configuration and add any fallbacks 567 568 @param resource: the S3Resource 569 570 @returns: the resource organizer configuration, format: 571 {"start": S3ResourceField, 572 "end": S3ResourceField or None, 573 "use_time": whether this resource has timed events, 574 "title": selector or callable to produce item titles, 575 "description": list of selectors for the item description, 576 } 577 """ 578 579 prefix = lambda selector: cls.prefix_selector(resource, selector) 580 581 table = resource.table 582 config = resource.get_config("organize") 583 if not config: 584 config = {} 585 586 # Identify start field 587 introspect = False 588 start_rfield = end_rfield = None 589 start = config.get("start") 590 if not start: 591 introspect = True 592 for fn in ("date", "start_date"): 593 if fn in table.fields: 594 start = fn 595 break 596 if start: 597 start_rfield = resource.resolve_selector(prefix(start)) 598 else: 599 raise AttributeError("No start date found in %s" % table) 600 601 # Identify end field 602 end = config.get("end") 603 if not end and introspect: 604 for fn in ("end_date", "closed_on"): 605 if fn in table.fields: 606 start = fn 607 break 608 if end: 609 end_rfield = resource.resolve_selector(prefix(end)) 610 if start_rfield.colname == end_rfield.colname: 611 end_rfield = None 612 613 # Should we use a timed calendar to organize? 614 use_time = config.get("use_time", True) 615 if start_rfield.ftype == "date": 616 use_time = False 617 elif end_rfield: 618 if end_rfield.ftype == "date": 619 if introspect: 620 # Ignore end if introspected 621 end_rfield = None 622 else: 623 use_time = False 624 625 # Get represent-function to produce an item title 626 represent = config.get("title") 627 if represent is None: 628 for fn in ("subject", "name", "type_id"): 629 if fn in table.fields: 630 represent = fn 631 break 632 633 # If represent is a field selector, resolve it 634 if type(represent) is str: 635 represent = resource.resolve_selector(prefix(represent)) 636 637 # Description 638 setting = config.get("description") 639 description = [] 640 if isinstance(setting, (tuple, list)): 641 for item in setting: 642 if type(item) is tuple and len(item) > 1: 643 label, selector = item[:2] 644 else: 645 label, selector = None, item 646 rfield = resource.resolve_selector(prefix(selector)) 647 if label is not None: 648 rfield.label = label 649 description.append(rfield) 650 651 # Colors 652 color = config.get("color") 653 if color: 654 colors = config.get("colors") 655 if callable(colors): 656 colors = colors(resource, color) 657 color = resource.resolve_selector(prefix(color)) 658 else: 659 colors = None 660 661 return {"start": start_rfield, 662 "end": end_rfield, 663 "use_time": use_time, 664 "title": represent, 665 "description": description, 666 "color": color, 667 "colors": colors, 668 }
669 670 # ------------------------------------------------------------------------- 671 @staticmethod
672 - def parse_interval(intervalstr):
673 """ 674 Parse an interval string of the format "<ISO8601>--<ISO8601>" 675 into a pair of datetimes 676 677 @param intervalstr: the interval string 678 679 @returns: tuple of datetimes (start, end) 680 """ 681 682 start = end = None 683 684 if intervalstr: 685 dates = intervalstr.split("--") 686 if len(dates) != 2: 687 return start, end 688 689 try: 690 start = s3_decode_iso_datetime(dates[0]) 691 except ValueError: 692 pass 693 else: 694 start = start.replace(hour=0, minute=0, second=0) 695 try: 696 end = s3_decode_iso_datetime(dates[1]) 697 except ValueError: 698 pass 699 else: 700 end = end.replace(hour=0, minute=0, second=0) 701 702 return start, end
703 704 # ------------------------------------------------------------------------- 705 @staticmethod
706 - def prefix_selector(resource, selector):
707 """ 708 Helper method to prefix an unprefixed field selector 709 710 @param resource: the target resource 711 @param selector: the field selector 712 713 @return: the prefixed selector 714 """ 715 716 alias = resource.alias if resource.parent else None 717 items = selector.split("$", 0) 718 head = items[0] 719 if "." in head: 720 if alias not in (None, "~"): 721 prefix, key = head.split(".", 1) 722 if prefix == "~": 723 prefix = alias 724 elif prefix != alias: 725 prefix = "%s.%s" % (alias, prefix) 726 items[0] = "%s.%s" % (prefix, key) 727 selector = "$".join(items) 728 else: 729 if alias is None: 730 alias = "~" 731 selector = "%s.%s" % (alias, selector) 732 return selector
733 734 # ------------------------------------------------------------------------- 735 @staticmethod
736 - def formname(r):
737 738 if r.component: 739 prefix = "%s/%s/%s" % (r.tablename, r.id, r.component.alias) 740 else: 741 prefix = r.tablename 742 743 return "%s/organizer" % prefix
744 745 # ------------------------------------------------------------------------- 746 @staticmethod
747 - def isoformat(dt):
748 """ 749 Format a date/datetime as ISO8601 datetime string 750 751 @param dt: the date/datetime instance 752 753 @returns: the ISO-formatted datetime string, 754 or None if dt was None 755 """ 756 757 if dt is None: 758 formatted = None 759 else: 760 if isinstance(dt, datetime.datetime) and dt.tzinfo is None: 761 dt = dt.replace(tzinfo = dateutil.tz.tzutc()) 762 formatted = dt.isoformat() 763 return formatted
764
765 # ============================================================================= 766 -class S3OrganizerWidget(object):
767 """ Helper to configure and render the organizer UI widget """ 768
769 - def __init__(self, resources):
770 """ 771 Constructor 772 773 @param resources: a list of resource specs, format: 774 [{"ajax_url": URL to retrieve events 775 "start": start date field (selector) 776 "end": end date field (selector) 777 }, 778 ... 779 ] 780 """ 781 782 self.resources = resources
783 784 # -------------------------------------------------------------------------
785 - def html(self, widget_id=None, formkey=None):
786 """ 787 Render the organizer container and instantiate the UI widget 788 789 @param widget_id: the container's DOM ID 790 """ 791 792 T = current.T 793 settings = current.deployment_settings 794 795 if not widget_id: 796 widget_id = "organizer" 797 798 # Parse resource configuration 799 resources = self.resources 800 if not isinstance(resources, (list, tuple)): 801 resources = [resources] 802 resource_configs = [] 803 use_time = False 804 for resource_config in resources: 805 resource_use_time = resource_config.get("useTime") 806 if resource_use_time: 807 use_time = True 808 resource_configs.append(resource_config) 809 810 # Inject script and widget instantiation 811 script_opts = {"resources": resource_configs, 812 "useTime": use_time, 813 "labelEdit": s3_str(T("Edit")), 814 "labelDelete": s3_str(T("Delete")), 815 "deleteConfirmation": s3_str(T("Do you want to delete this entry?")), 816 "firstDay": settings.get_L10n_firstDOW(), 817 } 818 # Options from settings 819 bhours = settings.get_ui_organizer_business_hours() 820 if bhours: 821 script_opts["businessHours"] = bhours 822 tformat = settings.get_ui_organizer_time_format() 823 if tformat: 824 script_opts["timeFormat"] = tformat 825 826 self.inject_script(widget_id, script_opts) 827 828 # Add a datepicker to navigate to arbitrary dates 829 picker = S3DateWidget()(Storage(name="date_select"), 830 None, 831 _type="hidden", 832 _id="%s-date-picker" % widget_id, 833 ) 834 835 # Generate and return the HTML for the widget container 836 return DIV(INPUT(_name = "_formkey", 837 _type = "hidden", 838 _value = str(formkey) if formkey else "", 839 ), 840 picker, 841 _id = widget_id, 842 _class = "s3-organizer", 843 )
844 845 # ------------------------------------------------------------------------- 846 @staticmethod
847 - def inject_script(widget_id, options):
848 """ 849 Inject the necessary JavaScript 850 851 @param widget_id: the container's DOM ID 852 @param options: widget options (JSON-serializable dict) 853 """ 854 855 s3 = current.response.s3 856 scripts = s3.scripts 857 858 request = current.request 859 appname = request.application 860 861 # Inject CSS 862 # TODO move into themes? 863 if s3.debug: 864 s3.stylesheets.append("fullcalendar/fullcalendar.css") 865 s3.stylesheets.append("qtip/jquery.qtip.css") 866 else: 867 s3.stylesheets.append("fullcalendar/fullcalendar.min.css") 868 s3.stylesheets.append("qtip/jquery.qtip.min.css") 869 870 # Select scripts 871 if s3.debug: 872 inject = ["moment.js", 873 "fullcalendar/fullcalendar.js", 874 "jquery.qtip.js", 875 "S3/s3.ui.organizer.js", 876 ] 877 else: 878 inject = ["moment.min.js", 879 "fullcalendar/fullcalendar.min.js", 880 "jquery.qtip.min.js", 881 "S3/s3.ui.organizer.min.js", 882 ] 883 884 # Choose locale 885 language = current.session.s3.language 886 l10n_path = os.path.join(request.folder, 887 "static", "scripts", "fullcalendar", "locale", 888 ) 889 l10n_file = "%s.js" % language 890 script = "fullcalendar/locale/%s" % l10n_file 891 if script not in scripts and \ 892 os.path.exists(os.path.join(l10n_path, l10n_file)): 893 options["locale"] = language 894 inject.insert(-1, "fullcalendar/locale/%s" % l10n_file) 895 896 # Inject scripts 897 for path in inject: 898 script = "/%s/static/scripts/%s" % (appname, path) 899 if script not in scripts: 900 scripts.append(script) 901 902 # Script to attach the timeplot widget 903 script = """$("#%(widget_id)s").organizer(%(options)s)""" % \ 904 {"widget_id": widget_id, 905 "options": json.dumps(options), 906 } 907 s3.jquery_ready.append(script)
908 909 # END ========================================================================= 910