| Home | Trees | Indices | Help |
|
|---|
|
|
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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
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
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
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
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
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
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 # -------------------------------------------------------------------------
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
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:51:56 2019 | http://epydoc.sourceforge.net |