| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2
3 """ S3 RESTful CRUD Methods
4
5 @see: U{B{I{S3XRC}} <http://eden.sahanafoundation.org/wiki/S3XRC>}
6
7 @requires: U{B{I{gluon}} <http://web2py.com>}
8 @requires: U{B{I{lxml}} <http://codespeak.net/lxml>}
9
10 @copyright: 2009-2019 (c) Sahana Software Foundation
11 @license: MIT
12
13 Permission is hereby granted, free of charge, to any person
14 obtaining a copy of this software and associated documentation
15 files (the "Software"), to deal in the Software without
16 restriction, including without limitation the rights to use,
17 copy, modify, merge, publish, distribute, sublicense, and/or sell
18 copies of the Software, and to permit persons to whom the
19 Software is furnished to do so, subject to the following
20 conditions:
21
22 The above copyright notice and this permission notice shall be
23 included in all copies or substantial portions of the Software.
24
25 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
27 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
29 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
30 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
31 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 OTHER DEALINGS IN THE SOFTWARE.
33 """
34
35 __all__ = ("S3CRUD",)
36
37 import json
38
39 try:
40 from lxml import etree
41 except ImportError:
42 import sys
43 sys.stderr.write("ERROR: lxml module needed for XML handling\n")
44 raise
45
46 from gluon import current, redirect, HTTP, URL, \
47 A, DIV, FORM, INPUT, TABLE, TD, TR, XML
48 from gluon.contenttype import contenttype
49 from gluon.languages import lazyT
50 from gluon.storage import Storage
51 from gluon.tools import callback
52
53 from s3datetime import S3DateTime, s3_decode_iso_datetime
54 from s3export import S3Exporter
55 from s3forms import S3SQLDefaultForm
56 from s3rest import S3Method
57 from s3utils import s3_str, s3_unicode, s3_validate, s3_represent_value, s3_set_extension
58 from s3widgets import S3EmbeddedComponentWidget, S3Selector, ICON
59
60 # Compact JSON encoding
61 SEPARATORS = (",", ":")
62
63 # =============================================================================
64 -class S3CRUD(S3Method):
65 """
66 Interactive CRUD Method Handler
67 """
68
69 # -------------------------------------------------------------------------
71 """
72 Apply CRUD methods
73
74 @param r: the S3Request
75 @param attr: dictionary of parameters for the method handler
76
77 @return: output object to send to the view
78 """
79
80 self.settings = current.response.s3.crud
81 sqlform = self._config("crud_form")
82 self.sqlform = sqlform if sqlform else S3SQLDefaultForm()
83
84 # Pre-populate create-form?
85 self.data = None
86 if r.http == "GET" and not self.record_id:
87 populate = attr.pop("populate", None)
88 if callable(populate):
89 try:
90 self.data = populate(r, **attr)
91 except TypeError:
92 self.data = None
93 except:
94 raise
95 elif isinstance(populate, dict):
96 self.data = populate
97
98 method = self.method
99
100 if r.http == "DELETE" or self.method == "delete":
101 output = self.delete(r, **attr)
102 elif method == "create":
103 output = self.create(r, **attr)
104 elif method == "read":
105 output = self.read(r, **attr)
106 elif method == "update":
107 output = self.update(r, **attr)
108
109 # Standard list view: list-type and hide-filter set by controller
110 # (default: list_type="datatable", hide_filter=None)
111 elif method == "list":
112 output = self.select(r, **attr)
113
114 # URL Methods to explicitly choose list-type and hide-filter in the URL
115 elif method in ("datatable", "datatable_f"):
116 _attr = Storage(attr)
117 _attr["list_type"] = "datatable"
118 if method == "datatable_f":
119 self.hide_filter = False
120 output = self.select(r, **_attr)
121 elif method in ("datalist", "datalist_f"):
122 _attr = Storage(attr)
123 _attr["list_type"] = "datalist"
124 if method == "datalist_f":
125 self.hide_filter = False
126 output = self.select(r, **_attr)
127
128 elif method == "validate":
129 output = self.validate(r, **attr)
130 elif method == "review":
131 if r.record:
132 output = self.review(r, **attr)
133 else:
134 output = self.unapproved(r, **attr)
135 else:
136 r.error(405, current.ERROR.BAD_METHOD)
137
138 return output
139
140 # -------------------------------------------------------------------------
142 """
143 Entry point for other method handlers to embed this
144 method as widget
145
146 @param r: the S3Request
147 @param method: the widget method
148 @param widget_id: the widget ID
149 @param visible: whether the widget is initially visible
150 @param attr: controller attributes
151 """
152
153 # Settings
154 self.settings = current.response.s3.crud
155 sqlform = self._config("crud_form")
156 self.sqlform = sqlform if sqlform else S3SQLDefaultForm()
157
158 _attr = Storage(attr)
159 _attr["list_id"] = widget_id
160
161 if method == "datatable":
162 output = self._datatable(r, **_attr)
163 if isinstance(output, dict):
164 output = DIV(output["items"], _id="table-container")
165 return output
166 elif method == "datalist":
167 output = self._datalist(r, **_attr)
168 if isinstance(output, dict) and "items" in output:
169 output = DIV(output["items"], _id="list-container")
170 return output
171 elif method == "create":
172 return self._widget_create(r, **_attr)
173 else:
174 return None
175
176 # -------------------------------------------------------------------------
178 """
179 Create new records
180
181 @param r: the S3Request
182 @param attr: dictionary of parameters for the method handler
183 """
184
185 session = current.session
186 request = self.request
187 response = current.response
188
189 resource = self.resource
190 table = resource.table
191 tablename = resource.tablename
192
193 representation = r.representation
194
195 output = {}
196
197 native = r.method == "create"
198
199 # Get table configuration
200 _config = self._config
201 insertable = _config("insertable", True)
202 if not insertable:
203 if native:
204 r.error(405, current.ERROR.METHOD_DISABLED)
205 else:
206 return {"form": None}
207
208 authorised = self._permitted(method="create")
209 if not authorised:
210 if native:
211 r.unauthorised()
212 else:
213 return {"form": None}
214
215 # Get callbacks
216 onvalidation = _config("create_onvalidation") or \
217 _config("onvalidation")
218 onaccept = _config("create_onaccept") or \
219 _config("onaccept")
220
221 if r.interactive:
222
223 crud_string = self.crud_string
224
225 # Page details
226 if native:
227
228 # Set view
229 if representation in ("popup", "iframe"):
230 response.view = self._view(r, "popup.html")
231 output["caller"] = request.vars.caller
232 else:
233 response.view = self._view(r, "create.html")
234
235 # Title
236 if r.component:
237 title = crud_string(r.tablename, "title_display")
238 output["title"] = title
239 else:
240 title = crud_string(tablename, "label_create")
241 output["title"] = title
242 output["title_list"] = crud_string(tablename, "title_list")
243
244 # Buttons
245 buttons = self.render_buttons(r, ["list"], **attr)
246 if buttons:
247 output["buttons"] = buttons
248
249 # Component defaults and linking
250 link = None
251 if r.component:
252
253 defaults = r.component.get_defaults(r.record)
254
255 if resource.link is None:
256 # Apply component defaults
257 linked = resource.linked
258 ctable = linked.table if linked else table
259 for (k, v) in defaults.items():
260 ctable[k].default = v
261
262 # Configure post-process for S3EmbeddedComponentWidget
263 link = self._embed_component(resource, record=r.id)
264
265 # Set default value for parent key (fkey)
266 pkey = resource.pkey
267 fkey = resource.fkey
268 field = table[fkey]
269 value = r.record[pkey]
270 field.default = field.update = value
271
272 # Add parent key to POST vars so that callbacks can see it
273 if r.http == "POST":
274 r.post_vars.update({fkey: value})
275
276 # Hide the parent link in component forms
277 field.comment = None
278 field.readable = False
279 field.writable = False
280
281 else:
282 # Apply component defaults
283 for (k, v) in defaults.items():
284 table[k].default = v
285
286 # Configure post-process to add a link table entry
287 link = Storage(resource=resource.link, master=r.record)
288
289 get_vars = r.get_vars
290
291 # Hierarchy parent
292 hierarchy = None
293 link_to_parent = get_vars.get("link_to_parent")
294 if link_to_parent:
295 try:
296 parent = long(link_to_parent)
297 except ValueError:
298 r.error(400, "Invalid parent record ID: %s" % link_to_parent)
299 else:
300 from s3hierarchy import S3Hierarchy
301 h = S3Hierarchy(tablename)
302 if h.config:
303 try:
304 hierarchy = h.preprocess_create_node(r, parent)
305 except KeyError:
306 import sys
307 r.error(404, sys.exc_info()[1])
308
309 # Organizer
310 organizer = get_vars.get("organizer")
311 if organizer:
312 self._set_organizer_dates(organizer)
313
314 # Copy record
315 from_table = None
316 from_record = get_vars.get("from_record")
317 map_fields = get_vars.get("from_fields")
318
319 if from_record:
320 del get_vars["from_record"] # forget it
321 if from_record.find(".") != -1:
322 from_table, from_record = from_record.split(".", 1)
323 from_table = current.db.get(from_table, None)
324 if not from_table:
325 r.error(404, current.ERROR.BAD_RESOURCE)
326 else:
327 from_table = table
328 try:
329 from_record = long(from_record)
330 except ValueError:
331 r.error(404, current.ERROR.BAD_RECORD)
332 authorised = current.auth.s3_has_permission("read",
333 from_table._tablename,
334 from_record)
335 if not authorised:
336 r.unauthorised()
337 if map_fields:
338 del r.get_vars["from_fields"]
339 if map_fields.find("$") != -1:
340 mf = map_fields.split(",")
341 mf = [f.find("$") != -1 and f.split("$") or \
342 [f, f] for f in mf]
343 map_fields = Storage(mf)
344 else:
345 map_fields = map_fields.split(",")
346
347 # Success message
348 message = crud_string(self.tablename, "msg_record_created")
349
350 # Copy formkey if un-deleting a duplicate
351 if "id" in request.post_vars:
352 post_vars = request.post_vars
353 original = str(post_vars.id)
354 formkey = session.get("_formkey[%s/None]" % tablename)
355 formname = "%s/%s" % (tablename, original)
356 session["_formkey[%s]" % formname] = formkey
357 if "deleted" in table:
358 table.deleted.writable = True
359 post_vars["deleted"] = False
360 if "created_on" in table:
361 table.created_on.writable = True
362 post_vars["created_on"] = request.utcnow
363 if "created_by" in table:
364 table.created_by.writable = True
365 if current.auth.user:
366 post_vars["created_by"] = current.auth.user.id
367 else:
368 post_vars["created_by"] = None
369 post_vars["_undelete"] = True
370 post_vars["_formname"] = formname
371 post_vars["id"] = original
372 request.vars.update(**post_vars)
373 else:
374 original = None
375
376 subheadings = _config("subheadings")
377
378 # Interim save button
379 self._interim_save_button()
380
381 # Default Cancel Button
382 if r.representation == "html" and r.method == "create":
383 self._default_cancel_button(r)
384
385 # Get the form
386 output["form"] = self.sqlform(request=request,
387 resource=resource,
388 data=self.data,
389 record_id=original,
390 from_table=from_table,
391 from_record=from_record,
392 map_fields=map_fields,
393 onvalidation=onvalidation,
394 onaccept=onaccept,
395 link=link,
396 hierarchy=hierarchy,
397 message=message,
398 subheadings=subheadings,
399 format=representation,
400 )
401
402 # Navigate-away confirmation
403 if self.settings.navigate_away_confirm:
404 response.s3.jquery_ready.append("S3EnableNavigateAwayConfirm()")
405
406 # Redirection
407 if representation in ("popup", "iframe", "plain", "dl"):
408 self.next = None
409 else:
410 if r.http == "POST" and "interim_save" in r.post_vars:
411 next_vars = self._remove_filters(r.get_vars)
412 create_next = r.url(target="[id]", method="update",
413 vars=next_vars)
414 elif r.http == "POST" and "save_close" in r.post_vars:
415 create_next = _config("create_next_close")
416 elif session.s3.rapid_data_entry and not r.component:
417 if "w" in r.get_vars:
418 # Don't redirect to form tab from summary page
419 w = r.get_vars.pop("w")
420 else:
421 w = None
422 create_next = r.url()
423 if w:
424 r.get_vars["w"] = w
425 else:
426 create_next = _config("create_next")
427
428 if not create_next:
429 next_vars = self._remove_filters(r.get_vars)
430 if r.component:
431 self.next = r.url(method="",
432 vars=next_vars)
433 else:
434 self.next = r.url(id="[id]",
435 method="read",
436 vars=next_vars)
437 elif callable(create_next):
438 self.next = create_next(r)
439 else:
440 self.next = create_next
441
442 elif representation == "plain":
443 # NB formstyle will be "table3cols" so widgets need to support that
444 # or else we need to be able to override this
445 response.view = self._view(r, "plain.html")
446 crud_string = self.crud_string
447 message = crud_string(tablename, "msg_record_created")
448 subheadings = _config("subheadings")
449 output["title"] = crud_string(tablename, "label_create")
450 output["details_btn"] = ""
451 output["item"] = self.sqlform(request=request,
452 resource=resource,
453 data=self.data,
454 onvalidation=onvalidation,
455 onaccept=onaccept,
456 #link=link,
457 message=message,
458 subheadings=subheadings,
459 format=representation)
460
461 elif representation == "csv":
462 import cgi
463 import csv
464 csv.field_size_limit(1000000000)
465 infile = request.vars.filename
466 if isinstance(infile, cgi.FieldStorage) and infile.filename:
467 infile = infile.file
468 else:
469 try:
470 infile = open(infile, "rb")
471 except IOError:
472 session.error = current.T("Cannot read from file: %(filename)s") % \
473 {"filename": infile}
474 redirect(r.url(method="", representation="html"))
475 try:
476 self.import_csv(infile, table=table)
477 except:
478 session.error = current.T("Unable to parse CSV file or file contains invalid data")
479 else:
480 session.confirmation = current.T("Data uploaded")
481
482 elif representation == "pdf":
483 from s3pdf import S3PDF
484 exporter = S3PDF()
485 return exporter(r, **attr)
486
487 elif representation == "url":
488 results = self.import_url(r)
489 return results
490
491 else:
492 r.error(415, current.ERROR.BAD_FORMAT)
493
494 return output
495
496 # -------------------------------------------------------------------------
498 """
499 Create-buttons/form in summary views, both GET and POST
500
501 @param r: the S3Request
502 @param attr: dictionary of parameters for the method handler
503 """
504
505 response = current.response
506
507 resource = self.resource
508 get_config = resource.get_config
509 tablename = resource.tablename
510
511 output = {}
512 insertable = get_config("insertable", True)
513 if insertable:
514
515 listadd = get_config("listadd", True)
516 addbtn = get_config("addbtn", False)
517
518 if listadd:
519 # Hidden form + Add-button to activate it
520 self.data = None
521 if r.http == "GET" and not self.record_id:
522 populate = attr.pop("populate", None)
523 if callable(populate):
524 try:
525 self.data = populate(r, **attr)
526 except TypeError:
527 self.data = None
528 except:
529 raise
530 elif isinstance(populate, dict):
531 self.data = populate
532
533 view = response.view
534
535 # JS Cancel (no redirect with embedded form)
536 s3 = response.s3
537 cancel = s3.cancel
538 s3.cancel = {"hide": "list-add", "show": "show-add-btn"}
539
540 form = self.create(r, **attr).get("form", None)
541 if form and form.accepted and self.next:
542 # Tell the summary handler that we're done
543 # and supposed to redirect to another view
544 return {"success": True, "next": self.next}
545
546 # Restore standard view and cancel-config
547 response.view = view
548 s3.cancel = cancel
549
550 if form is not None:
551 form_postp = r.resource.get_config("form_postp")
552 if form_postp:
553 form_postp(form)
554 output["form"] = form
555 output["showadd_btn"] = self.crud_button(tablename=tablename,
556 name="label_create",
557 icon="add",
558 _id="show-add-btn")
559 addtitle = self.crud_string(tablename, "label_create")
560 output["addtitle"] = addtitle
561 if r.http == "POST":
562 # Always show the form if there was a form error
563 script = '''$('#list-add').show();$('#show-add-btn').hide()'''
564 s3.jquery_ready.append(script)
565
566 # Add-button script
567 # - now in S3.js
568 #script = '''$('#show-add-btn').click(function(){$('#show-add-btn').hide(10, function(){$('#list-add').slideDown('medium')})})'''
569 #s3.jquery_ready.append(script)
570
571 elif addbtn:
572 # No form, just Add-button linked to create-view
573 add_btn = self.crud_button(
574 tablename=tablename,
575 name="label_create",
576 icon="add",
577 _id="add-btn")
578 output["buttons"] = {"add_btn": add_btn}
579
580 view = self._view(r, "listadd.html")
581 output = XML(response.render(view, output))
582 return output
583
584 # -------------------------------------------------------------------------
586 """
587 Read a single record
588
589 @param r: the S3Request
590 @param attr: dictionary of parameters for the method handler
591 """
592
593 # Check authorization to read the record
594 authorised = self._permitted()
595 if not authorised:
596 r.unauthorised()
597
598 request = self.request
599 response = current.response
600
601 resource = self.resource
602 table = resource.table
603 tablename = resource.tablename
604
605 representation = r.representation
606
607 output = {}
608
609 _config = self._config
610 editable = _config("editable", True)
611
612 # Get the target record ID
613 record_id = self.record_id
614
615 if r.interactive:
616
617 component = r.component
618
619 # If this is a single-component and no record exists,
620 # try to create one if the user is permitted
621 if not record_id and component and not component.multiple:
622 empty = True
623 authorised = self._permitted(method="create")
624 if authorised and _config("insertable", True):
625 # This should become Native
626 r.method = "create"
627 return self.create(r, **attr)
628 else:
629 empty = False
630
631 # Redirect to update if user has permission unless
632 # a method has been specified in the URL
633 # MH: Is this really desirable? Many users would prefer to open as read
634 if not r.method: #or r.method == "review":
635 authorised = self._permitted("update")
636 if authorised and representation == "html" and editable:
637 return self.update(r, **attr)
638
639 # Form configuration
640 subheadings = _config("subheadings")
641
642 # Title and subtitle
643 crud_string = self.crud_string
644 title = crud_string(r.tablename, "title_display")
645 output["title"] = title
646 if component and not empty:
647 subtitle = crud_string(tablename, "title_display")
648 output["subtitle"] = subtitle
649 output["title_list"] = crud_string(tablename, "title_list")
650
651 # Hide component key when on tab
652 if component and resource.link is None:
653 try:
654 field = table[resource.fkey]
655 except (AttributeError, KeyError):
656 pass
657 else:
658 field.readable = field.writable = False
659
660 # Item
661 if record_id:
662 try:
663 item = self.sqlform(request = request,
664 resource = resource,
665 record_id = record_id,
666 readonly = True,
667 subheadings = subheadings,
668 format = representation,
669 )
670 except HTTP, e:
671 message = current.ERROR.BAD_RECORD \
672 if e.status == 404 else e.message
673 r.error(e.status, message)
674 else:
675 item = DIV(crud_string(tablename, "msg_list_empty"),
676 _class = "empty",
677 )
678
679 # View
680 if representation == "html":
681 response.view = self._view(r, "display.html")
682 output["item"] = item
683 elif representation == "popup":
684 response.view = self._view(r, "popup.html")
685 output["form"] = item
686 caller = attr.get("caller", None)
687 output["caller"] = caller
688 elif representation == "iframe":
689 response.view = self._view(r, "iframe.html")
690 output["form"] = item
691
692 # Buttons
693 buttons = self.render_buttons(r,
694 ["edit", "delete", "list", "summary"],
695 record_id = record_id,
696 **attr)
697 if buttons:
698 output["buttons"] = buttons
699
700 # Last update
701 last_update = self.last_update()
702 if last_update:
703 try:
704 output["modified_on"] = last_update["modified_on"]
705 except KeyError:
706 # Field not in table
707 pass
708 try:
709 output["modified_by"] = last_update["modified_by"]
710 except KeyError:
711 # Field not in table, such as auth_user
712 pass
713
714 # De-duplication
715 from s3merge import S3Merge
716 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id)
717
718 elif representation == "plain":
719 # e.g. Map Popup
720 T = current.T
721 fields = [f for f in table if f.readable]
722 if r.component:
723 if record_id:
724 record = current.db(table._id == record_id).select(limitby=(0, 1),
725 *fields
726 ).first()
727 else:
728 record = None
729 else:
730 record = r.record
731 if record:
732 # Hide empty fields from popups on map
733 for field in fields:
734 try:
735 value = record[field]
736 except KeyError:
737 # e.g. gis_location.wkt
738 value = None
739 if value is None or value == "" or value == []:
740 field.readable = False
741 item = self.sqlform(request=request,
742 resource=resource,
743 record_id=record_id,
744 readonly=True,
745 format=representation)
746
747 # Link to Open record
748 popup_edit_url = _config("popup_edit_url", None)
749 if popup_edit_url and \
750 current.auth.s3_has_permission("update", table, record_id):
751 # Open edit form in iframe
752 details_btn = A(T("Edit"),
753 _href=popup_edit_url,
754 _class="btn iframe",
755 )
756 output["details_btn"] = details_btn
757 else:
758 # Open read view in new tab
759 # Set popup_url to "" to have no button present
760 popup_url = _config("popup_url", None)
761 if popup_url is None:
762 popup_url = r.url(method="read", representation="html")
763 if popup_url:
764 popup_url = popup_url.replace("%5Bid%5D", str(record_id))
765 details_btn = A(T("Open"),
766 _href = popup_url,
767 _class = "btn",
768 _target = "_blank",
769 )
770 output["details_btn"] = details_btn
771
772 # Title and subtitle
773 title = self.crud_string(r.tablename, "title_display")
774 output["title"] = title
775
776 else:
777 item = T("Record not found")
778
779 output["item"] = item
780 response.view = self._view(r, "plain.html")
781
782 elif representation == "csv":
783 exporter = S3Exporter().csv
784 output = exporter(resource)
785
786 #elif representation == "map":
787 # exporter = S3Map()
788 # output = exporter(r, **attr)
789
790 elif representation == "pdf":
791 exporter = S3Exporter().pdf
792 output = exporter(resource, request=r, **attr)
793
794 elif representation == "shp":
795 list_fields = resource.list_fields()
796 exporter = S3Exporter().shp
797 output = exporter(resource, list_fields=list_fields, **attr)
798
799 elif representation == "svg":
800 list_fields = resource.list_fields()
801 exporter = S3Exporter().svg
802 output = exporter(resource, list_fields=list_fields, **attr)
803
804 elif representation == "xls":
805 list_fields = resource.list_fields()
806 exporter = S3Exporter().xls
807 output = exporter(resource, list_fields=list_fields)
808
809 elif representation == "json":
810 exporter = S3Exporter().json
811
812 # Render extra "_tooltip" field for each row?
813 get_vars = request.get_vars
814 if "tooltip" in get_vars:
815 tooltip = get_vars["tooltip"]
816 else:
817 tooltip = None
818
819 output = exporter(resource, tooltip=tooltip)
820
821 elif representation == "card":
822
823 if not resource.get_config("pdf_card_layout"):
824 # This format is not supported for this resource
825 r.error(415, current.ERROR.BAD_FORMAT)
826
827 pagesize = resource.get_config("pdf_card_pagesize")
828 output = S3Exporter().pdfcard(resource,
829 pagesize = pagesize,
830 )
831
832 disposition = "attachment; filename=\"%s_card.pdf\"" % resource.name
833 response.headers["Content-Type"] = contenttype(".pdf")
834 response.headers["Content-disposition"] = disposition
835
836 else:
837 r.error(415, current.ERROR.BAD_FORMAT)
838
839 return output
840
841 # -------------------------------------------------------------------------
843 """
844 Update a record
845
846 @param r: the S3Request
847 @param attr: dictionary of parameters for the method handler
848 """
849
850 resource = self.resource
851 table = resource.table
852 tablename = resource.tablename
853
854 representation = r.representation
855
856 output = {}
857
858 # Get table configuration
859 _config = self._config
860 editable = _config("editable", True)
861
862 # Get callbacks
863 onvalidation = _config("update_onvalidation") or \
864 _config("onvalidation")
865 onaccept = _config("update_onaccept") or \
866 _config("onaccept")
867
868 # Get the target record ID
869 record_id = self.record_id
870 if r.interactive and not record_id:
871 r.error(404, current.ERROR.BAD_RECORD)
872
873 # Check if editable
874 if not editable:
875 if r.interactive:
876 return self.read(r, **attr)
877 else:
878 r.error(405, current.ERROR.METHOD_DISABLED)
879
880 # Check permission for update
881 authorised = self._permitted(method="update")
882 if not authorised:
883 r.unauthorised()
884
885 if r.interactive or representation == "plain":
886
887 response = current.response
888 s3 = response.s3
889
890 # Form configuration
891 subheadings = _config("subheadings")
892
893 # Set view
894 if representation == "html":
895 response.view = self._view(r, "update.html")
896 elif representation in "popup":
897 response.view = self._view(r, "popup.html")
898 elif representation == "plain":
899 response.view = self._view(r, "plain.html")
900 elif representation == "iframe":
901 response.view = self._view(r, "iframe.html")
902
903 # Title and subtitle
904 crud_string = self.crud_string
905 if r.component:
906 title = crud_string(r.tablename, "title_display")
907 subtitle = crud_string(self.tablename, "title_update")
908 output["title"] = title
909 output["subtitle"] = subtitle
910 else:
911 title = crud_string(self.tablename, "title_update")
912 output["title"] = title
913 output["title_list"] = crud_string(tablename, "title_list")
914
915 # Component join
916 link = None
917 if r.component:
918 if resource.link is None:
919 link = self._embed_component(resource, record=r.id)
920 pkey = resource.pkey
921 fkey = resource.fkey
922 field = table[fkey]
923 value = r.record[pkey]
924 field.comment = None
925 field.default = value
926 field.update = value
927 if r.http == "POST":
928 r.post_vars.update({fkey: value})
929 field.readable = False
930 field.writable = False
931 else:
932 link = Storage(resource=resource.link, master=r.record)
933
934 # Success message
935 message = crud_string(self.tablename, "msg_record_modified")
936
937 # Interim save button
938 self._interim_save_button()
939
940 # Default Cancel Button
941 if r.representation == "html" and \
942 (r.method == "update" or not r.method):
943 self._default_cancel_button(r)
944
945 # Get the form
946 try:
947 form = self.sqlform(request=self.request,
948 resource=resource,
949 record_id=record_id,
950 onvalidation=onvalidation,
951 onaccept=onaccept,
952 message=message,
953 link=link,
954 subheadings=subheadings,
955 format=representation)
956 except HTTP, e:
957 message = current.ERROR.BAD_RECORD \
958 if e.status == 404 else e.message
959 r.error(e.status, message)
960
961 # Navigate-away confirmation
962 if self.settings.navigate_away_confirm:
963 s3.jquery_ready.append("S3EnableNavigateAwayConfirm()")
964
965 # Put form into output
966 output["form"] = form
967 if representation == "plain":
968 output["item"] = form
969 output["title"] = ""
970
971 # Add delete and list buttons
972 buttons = self.render_buttons(r,
973 ["delete"],
974 record_id=record_id,
975 **attr)
976 if buttons:
977 output["buttons"] = buttons
978
979 # Last update
980 last_update = self.last_update()
981 if last_update:
982 try:
983 output["modified_on"] = last_update["modified_on"]
984 except KeyError:
985 # Field not in table
986 pass
987 try:
988 output["modified_by"] = last_update["modified_by"]
989 except KeyError:
990 # Field not in table, such as auth_user
991 pass
992
993 # De-duplication
994 from s3merge import S3Merge
995 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id)
996
997 # Redirection
998 if r.http == "POST" and "interim_save" in r.post_vars:
999 next_vars = self._remove_filters(r.get_vars)
1000 self.next = r.url(target="[id]", method="update",
1001 vars=next_vars)
1002 else:
1003 update_next = _config("update_next")
1004 if representation in ("popup", "iframe", "plain", "dl"):
1005 self.next = None
1006 elif not update_next:
1007 next_vars = self._remove_filters(r.get_vars)
1008 if r.component:
1009 self.next = r.url(method="", vars=next_vars)
1010 else:
1011 self.next = r.url(id="[id]",
1012 method="read",
1013 vars=next_vars)
1014 else:
1015 try:
1016 self.next = update_next(self)
1017 except TypeError:
1018 self.next = update_next
1019
1020 elif representation == "url":
1021 return self.import_url(r)
1022
1023 else:
1024 r.error(415, current.ERROR.BAD_FORMAT)
1025
1026 return output
1027
1028 # -------------------------------------------------------------------------
1030 """
1031 Delete record(s)
1032
1033 @param r: the S3Request
1034 @param attr: dictionary of parameters for the method handler
1035
1036 @todo: update for link table components
1037 """
1038
1039 output = {}
1040
1041 # Get table-specific parameters
1042 config = self._config
1043 deletable = config("deletable", True)
1044 delete_next = config("delete_next", None)
1045
1046 # Check if deletable
1047 if not deletable:
1048 r.error(403, current.ERROR.NOT_PERMITTED,
1049 next=r.url(method=""))
1050
1051 # Get the target record ID
1052 record_id = self.record_id
1053
1054 # Check permission to delete
1055 authorised = self._permitted()
1056 if not authorised:
1057 r.unauthorised()
1058
1059 elif (r.interactive or r.representation == "aadata") and \
1060 r.http == "GET" and not record_id:
1061 output = self._datatable(r, **attr)
1062 if isinstance(output, dict):
1063 # Provide a confirmation form and a record list
1064 form = FORM(TABLE(TR(TD(self.settings.confirm_delete,
1065 _style="color:red"),
1066 TD(INPUT(_type="submit",
1067 _value=current.T("Delete"),
1068 _style="margin-left:10px")))))
1069 output["form"] = form
1070 current.response.view = self._view(r, "delete.html")
1071 else:
1072 # @todo: sorting not working yet
1073 return output
1074
1075 elif r.interactive and (r.http == "POST" or
1076 r.http == "GET" and record_id):
1077 # Delete the records, notify success and redirect to the next view
1078 numrows = self.resource.delete(format=r.representation)
1079 if numrows > 1:
1080 message = "%s %s" % (numrows, current.T("records deleted"))
1081 elif numrows == 1:
1082 message = self.crud_string(self.tablename,
1083 "msg_record_deleted")
1084 else:
1085 r.error(404, self.resource.error, next=r.url(method=""))
1086 current.response.confirmation = message
1087 r.http = "DELETE" # must be set for immediate redirect
1088 self.next = delete_next or r.url(method="")
1089
1090 elif r.http == "DELETE" or \
1091 r.representation == "json" and r.http == "POST" and record_id:
1092
1093 numrows = None
1094 resource = self.resource
1095
1096 recursive = r.get_vars.get("recursive", False)
1097 if recursive and recursive.lower() in ("1", "true"):
1098 # Try recursive deletion of the whole hierarchy branch
1099 # => falls back to normal delete if no hierarchy configured
1100 from s3hierarchy import S3Hierarchy
1101 h = S3Hierarchy(resource.tablename)
1102 if h.config:
1103 node_ids = None
1104 pkey = h.pkey
1105 if str(pkey) == str(resource._id):
1106 node_ids = [record_id]
1107 else:
1108 # Need to lookup the hierarchy node key
1109 query = (resource._id == record_id)
1110 row = current.db(query).select(pkey,
1111 limitby=(0, 1)).first()
1112 if row:
1113 node_ids = [row[pkey]]
1114 numrows = h.delete(node_ids) if node_ids else 0
1115 if not numrows:
1116 # Cascade failed, do not fall back
1117 # @todo: propagate the original error
1118 resource.error = current.T("Deletion failed")
1119 numrows = 0
1120
1121 if numrows is None:
1122 # Delete the records and return a JSON message
1123 numrows = resource.delete(format=r.representation)
1124
1125 if numrows > 1:
1126 message = "%s %s" % (numrows, current.T("records deleted"))
1127 elif numrows == 1:
1128 message = self.crud_string(self.tablename,
1129 "msg_record_deleted")
1130 else:
1131 r.error(404, resource.error, next=r.url(method=""))
1132
1133 item = current.xml.json_message(message=message)
1134 current.response.view = "xml.html"
1135 output.update(item=item)
1136
1137 else:
1138 r.error(405, current.ERROR.BAD_METHOD)
1139
1140 return output
1141
1142 # -------------------------------------------------------------------------
1144 """
1145 Filterable datatable/datalist
1146
1147 @param r: the S3Request
1148 @param attr: dictionary of parameters for the method handler
1149 """
1150
1151 resource = self.resource
1152
1153 tablename = resource.tablename
1154 get_config = resource.get_config
1155
1156 list_fields = get_config("list_fields", None)
1157
1158 representation = r.representation
1159 if representation in ("html", "iframe", "aadata", "dl", "popup"):
1160
1161 hide_filter = self.hide_filter
1162 filter_widgets = get_config("filter_widgets", None)
1163
1164 show_filter_form = False
1165 if filter_widgets and not hide_filter and \
1166 representation not in ("aadata", "dl"):
1167 show_filter_form = True
1168 # Apply filter defaults (before rendering the data!)
1169 from s3filter import S3FilterForm
1170 default_filters = S3FilterForm.apply_filter_defaults(r, resource)
1171 else:
1172 default_filters = None
1173
1174 get_vars = r.get_vars
1175 attr = dict(attr)
1176
1177 # Data
1178 list_type = attr.get("list_type", "datatable")
1179 if list_type == "datalist":
1180 filter_ajax = True
1181 target = "datalist"
1182 output = self._datalist(r, **attr)
1183 else:
1184 if filter_widgets and not hide_filter:
1185 dtargs = attr.get("dtargs", {})
1186 # Hide datatable filter box if we have a filter form
1187 if "dt_searching" not in dtargs:
1188 dtargs["dt_searching"] = False
1189 # Override default ajax URL if we have default filters
1190 if default_filters:
1191 ajax_vars = dict(get_vars)
1192 ajax_vars.update(default_filters)
1193 ajax_url = r.url(representation = "aadata",
1194 vars = ajax_vars,
1195 )
1196 dtargs["dt_ajax_url"] = ajax_url
1197 attr["dtargs"] = dtargs
1198 filter_ajax = True
1199 target = "datatable"
1200 output = self._datatable(r, **attr)
1201
1202 if representation in ("aadata", "dl"):
1203 return output
1204
1205 output["list_type"] = list_type
1206
1207 crud_string = self.crud_string
1208
1209 # Page title
1210 if representation != "iframe":
1211 if r.component:
1212 title = crud_string(r.tablename, "title_display")
1213 else:
1214 title = crud_string(self.tablename, "title_list")
1215 output["title"] = title
1216
1217 # Filter-form
1218 if show_filter_form:
1219
1220 # Where to retrieve filtered data from:
1221 filter_submit_url = attr.get("filter_submit_url")
1222 if not filter_submit_url:
1223 get_vars_ = self._remove_filters(get_vars)
1224 filter_submit_url = r.url(vars=get_vars_)
1225
1226 # Where to retrieve updated filter options from:
1227 filter_ajax_url = attr.get("filter_ajax_url")
1228 if filter_ajax_url is None:
1229 filter_ajax_url = r.url(method = "filter",
1230 vars = {},
1231 representation = "options",
1232 )
1233 filter_clear = get_config("filter_clear",
1234 current.deployment_settings.get_ui_filter_clear())
1235 filter_formstyle = get_config("filter_formstyle", None)
1236 filter_submit = get_config("filter_submit", True)
1237 filter_form = S3FilterForm(filter_widgets,
1238 clear = filter_clear,
1239 formstyle = filter_formstyle,
1240 submit = filter_submit,
1241 ajax = filter_ajax,
1242 url = filter_submit_url,
1243 ajaxurl = filter_ajax_url,
1244 _class = "filter-form",
1245 _id = "%s-filter-form" % target
1246 )
1247 fresource = current.s3db.resource(resource.tablename) # Use a clean resource
1248 alias = resource.alias if r.component else None
1249 output["list_filter_form"] = filter_form.html(fresource,
1250 get_vars,
1251 target = target,
1252 alias = alias
1253 )
1254 else:
1255 # Render as empty string to avoid the exception in the view
1256 output["list_filter_form"] = ""
1257
1258 # Add-form or -button
1259 insertable = get_config("insertable", True)
1260 if insertable:
1261
1262 addbtn = get_config("addbtn", False)
1263 listadd = get_config("listadd", True)
1264
1265 if listadd:
1266 # Save the view
1267 response = current.response
1268 view = response.view
1269
1270 # JS Cancel (no redirect with embedded form)
1271 s3 = response.s3
1272 cancel = s3.cancel
1273 s3.cancel = {"hide": "list-add", "show": "show-add-btn"}
1274
1275 # Add a hidden add-form and a button to activate it
1276 form = self.create(r, **attr).get("form", None)
1277 if form is not None:
1278 output["form"] = form
1279 addtitle = self.crud_string(tablename, "label_create")
1280 output["addtitle"] = addtitle
1281 showadd_btn = self.crud_button(None,
1282 tablename = tablename,
1283 name = "label_create",
1284 icon = "add",
1285 _id = "show-add-btn",
1286 )
1287 output["showadd_btn"] = showadd_btn
1288
1289 # Restore the view
1290 response.view = view
1291 s3.cancel = cancel
1292
1293 elif addbtn:
1294 # Add an action-button linked to the create view
1295 buttons = self.render_buttons(r, ["add"], **attr)
1296 if buttons:
1297 output["buttons"] = buttons
1298
1299 return output
1300
1301 elif representation == "plain":
1302
1303 if resource.count() == 1:
1304 # Provide the record
1305 # (used by Map's Layer Properties window)
1306 resource.load()
1307 r.record = resource.records().first()
1308 if r.record:
1309 r.id = r.record.id
1310 self.record_id = self._record_id(r)
1311 if "update" in r.get_vars and \
1312 self._permitted(method="update"):
1313 items = self.update(r, **attr).get("form", None)
1314 else:
1315 items = self.sqlform(request = self.request,
1316 resource = self.resource,
1317 record_id = r.id,
1318 readonly = True,
1319 format = representation,
1320 )
1321 else:
1322 raise HTTP(404, body="Record not Found")
1323 else:
1324 rows = resource.select(list_fields,
1325 limit = None,
1326 as_rows = True,
1327 )
1328 if rows:
1329 items = rows.as_list()
1330 else:
1331 items = []
1332
1333 current.response.view = "plain.html"
1334 return {"item": items}
1335
1336 elif representation == "csv":
1337
1338 exporter = S3Exporter().csv
1339 return exporter(resource)
1340
1341 elif representation == "json":
1342
1343 get_vars = self.request.get_vars
1344
1345 # Start/limit (no default limit)
1346 start, limit = self._limits(get_vars, default_limit=None)
1347
1348 # Render extra "_tooltip" field for each row?
1349 tooltip = get_vars.get("tooltip", None)
1350
1351 # Represent?
1352 represent = get_vars.get("represent", False)
1353 if represent and represent != "0":
1354 represent = True
1355
1356 exporter = S3Exporter().json
1357 return exporter(resource,
1358 start = start,
1359 limit = limit,
1360 represent = represent,
1361 tooltip = tooltip,
1362 )
1363
1364 elif representation == "pdf":
1365
1366 report_hide_comments = get_config("report_hide_comments", None)
1367 report_filename = get_config("report_filename", None)
1368 report_formname = get_config("report_formname", None)
1369
1370 exporter = S3Exporter().pdf
1371 return exporter(resource,
1372 request = r,
1373 list_fields = list_fields,
1374 report_hide_comments = report_hide_comments,
1375 report_filename = report_filename,
1376 report_formname = report_formname,
1377 **attr)
1378
1379 elif representation == "shp":
1380 exporter = S3Exporter().shp
1381 return exporter(resource,
1382 list_fields = list_fields,
1383 **attr)
1384
1385 elif representation == "svg":
1386 exporter = S3Exporter().svg
1387 return exporter(resource,
1388 list_fields = list_fields,
1389 **attr)
1390
1391 elif representation == "xls":
1392 report_groupby = get_config("report_groupby", None)
1393 exporter = S3Exporter().xls
1394 return exporter(resource,
1395 list_fields = list_fields,
1396 report_groupby = report_groupby,
1397 **attr)
1398
1399 elif representation == "msg":
1400 if r.http == "POST":
1401 from s3notify import S3Notifications
1402 return S3Notifications.send(r, resource)
1403 else:
1404 r.error(405, current.ERROR.BAD_METHOD)
1405
1406 elif representation == "card":
1407 if not resource.get_config("pdf_card_layout"):
1408 # This format is not supported for this resource
1409 r.error(415, current.ERROR.BAD_FORMAT)
1410
1411 pagesize = resource.get_config("pdf_card_pagesize")
1412 output = S3Exporter().pdfcard(resource,
1413 pagesize = pagesize,
1414 )
1415
1416 response = current.response
1417 disposition = "attachment; filename=\"%s_cards.pdf\"" % resource.name
1418 response.headers["Content-Type"] = contenttype(".pdf")
1419 response.headers["Content-disposition"] = disposition
1420
1421 return output
1422
1423 else:
1424 r.error(415, current.ERROR.BAD_FORMAT)
1425
1426 # -------------------------------------------------------------------------
1428 """
1429 Get a data table
1430
1431 @param r: the S3Request
1432 @param attr: parameters for the method handler
1433 """
1434
1435 # Check permission to read in this table
1436 authorised = self._permitted()
1437 if not authorised:
1438 r.unauthorised()
1439
1440 resource = self.resource
1441 get_config = resource.get_config
1442
1443 # Get table-specific parameters
1444 linkto = get_config("linkto", None)
1445
1446 # List ID
1447 list_id = attr.get("list_id", "datatable")
1448
1449 # List fields
1450 list_fields = resource.list_fields()
1451
1452 # Default orderby
1453 orderby = get_config("orderby", None)
1454
1455 response = current.response
1456 s3 = response.s3
1457 representation = r.representation
1458
1459 # Pagination
1460 get_vars = self.request.get_vars
1461 if representation == "aadata":
1462 start, limit = self._limits(get_vars)
1463 else:
1464 # Initial page request always uses defaults (otherwise
1465 # filtering and pagination would have to be relative to
1466 # the initial limits, but there is no use-case for that)
1467 start = None
1468 limit = None if s3.no_sspag else 0
1469
1470 # Initialize output
1471 output = {}
1472
1473 # Linkto
1474 if not linkto:
1475 linkto = self._linkto(r)
1476
1477 left = []
1478 distinct = False
1479 dtargs = attr.get("dtargs", {})
1480
1481 if r.interactive:
1482
1483 # How many records per page?
1484 if s3.dataTable_pageLength:
1485 display_length = s3.dataTable_pageLength
1486 else:
1487 display_length = 25
1488
1489 # Server-side pagination?
1490 if not s3.no_sspag:
1491 dt_pagination = "true"
1492 if not limit:
1493 limit = 2 * display_length
1494 current.session.s3.filter = get_vars
1495 if orderby is None:
1496 dt_sorting = {"iSortingCols": "1",
1497 "sSortDir_0": "asc"
1498 }
1499
1500 if len(list_fields) > 1:
1501 dt_sorting["bSortable_0"] = "false"
1502 dt_sorting["iSortCol_0"] = "1"
1503 else:
1504 dt_sorting["bSortable_0"] = "true"
1505 dt_sorting["iSortCol_0"] = "0"
1506
1507 orderby, left = resource.datatable_filter(list_fields,
1508 dt_sorting,
1509 )[1:3]
1510 else:
1511 dt_pagination = "false"
1512
1513 # Get the data table
1514 dt, totalrows = resource.datatable(fields = list_fields,
1515 start = start,
1516 limit = limit,
1517 left = left,
1518 orderby = orderby,
1519 distinct = distinct,
1520 )
1521 displayrows = totalrows
1522
1523 if not dt.data:
1524 # Empty table - or just no match?
1525 #if dt.empty:
1526 # datatable = DIV(self.crud_string(resource.tablename,
1527 # "msg_list_empty"),
1528 # _class="empty")
1529 #else:
1530 # #datatable = DIV(self.crud_string(resource.tablename,
1531 # "msg_no_match"),
1532 # _class="empty")
1533
1534 # Must include export formats to allow subsequent unhiding
1535 # when Ajax (un-)filtering produces exportable table contents:
1536 #s3.no_formats = True
1537
1538 if r.component and "showadd_btn" in output:
1539 # Hide the list and show the form by default
1540 del output["showadd_btn"]
1541 datatable = ""
1542
1543 # Always show table, otherwise it can't be Ajax-filtered
1544 # @todo: need a better algorithm to determine total_rows
1545 # (which excludes URL filters), so that datatables
1546 # shows the right empty-message (ZeroRecords instead
1547 # of EmptyTable)
1548 dtargs["dt_pagination"] = dt_pagination
1549 dtargs["dt_pageLength"] = display_length
1550 dtargs["dt_base_url"] = r.url(method="", vars={})
1551 dtargs["dt_permalink"] = r.url()
1552 datatable = dt.html(totalrows,
1553 displayrows,
1554 id=list_id,
1555 **dtargs)
1556
1557 # View + data
1558 response.view = self._view(r, "list_filter.html")
1559 output["items"] = datatable
1560
1561 elif representation == "aadata":
1562
1563 # Apply datatable filters
1564 searchq, orderby, left = resource.datatable_filter(list_fields,
1565 get_vars)
1566 if searchq is not None:
1567 totalrows = resource.count()
1568 resource.add_filter(searchq)
1569 else:
1570 totalrows = None
1571
1572 # Orderby fallbacks
1573 if orderby is None:
1574 orderby = get_config("orderby", None)
1575
1576 # Get a data table
1577 if totalrows != 0:
1578 dt, displayrows = resource.datatable(fields = list_fields,
1579 start = start,
1580 limit = limit,
1581 left = left,
1582 orderby = orderby,
1583 distinct = distinct,
1584 )
1585 else:
1586 dt, displayrows = None, 0
1587 if totalrows is None:
1588 totalrows = displayrows
1589
1590 # Echo
1591 draw = int(get_vars.get("draw", 0))
1592
1593 # Representation
1594 if dt is not None:
1595 output = dt.json(totalrows,
1596 displayrows,
1597 list_id,
1598 draw,
1599 **dtargs)
1600 else:
1601 output = '{"recordsTotal":%s,' \
1602 '"recordsFiltered":0,' \
1603 '"dataTable_id":"%s",' \
1604 '"draw":%s,' \
1605 '"data":[]}' % (totalrows, list_id, draw)
1606
1607 else:
1608 r.error(415, current.ERROR.BAD_FORMAT)
1609
1610 return output
1611
1612 # -------------------------------------------------------------------------
1614 """
1615 Get a data list
1616
1617 @param r: the S3Request
1618 @param attr: parameters for the method handler
1619 """
1620
1621 # Check permission to read in this table
1622 authorised = self._permitted()
1623 if not authorised:
1624 r.unauthorised()
1625
1626 resource = self.resource
1627 get_config = resource.get_config
1628 get_vars = self.request.get_vars
1629
1630 # Get table-specific parameters
1631 layout = get_config("list_layout", None)
1632
1633 # List ID
1634 list_id = get_vars.get("list_id", # Could we check for refresh here? (Saves extra get_var)
1635 attr.get("list_id", "datalist"))
1636
1637 # List fields
1638 if hasattr(layout, "list_fields"):
1639 list_fields = layout.list_fields
1640 else:
1641 list_fields = resource.list_fields()
1642
1643 # Default orderby
1644 orderby = get_config("list_orderby",
1645 get_config("orderby", None))
1646 if orderby is None:
1647 if "created_on" in resource.fields:
1648 default_orderby = ~(resource.table["created_on"])
1649 else:
1650 for f in list_fields:
1651 rfield = resource.resolve_selector(f)
1652 if rfield.field and rfield.colname != str(resource._id):
1653 default_orderby = rfield.field
1654 break
1655 else:
1656 default_orderby = None
1657
1658 # Pagination
1659 response = current.response
1660 s3 = response.s3
1661
1662 # Pagelength = number of items per page
1663 if "dl_pagelength" in attr:
1664 pagelength = attr["dl_pagelength"]
1665 elif s3.dl_pagelength:
1666 pagelength = s3.dl_pagelength
1667 else:
1668 pagelength = 10
1669
1670 # Rowsize = number of items per row
1671 if "dl_rowsize" in attr:
1672 rowsize = attr["dl_rowsize"]
1673 elif s3.dl_rowsize:
1674 rowsize = s3.dl_rowsize
1675 else:
1676 rowsize = 1
1677
1678 # Make sure that pagelength is a multiple of rowsize
1679 if pagelength % rowsize:
1680 pagelength = (int(pagelength / rowsize) + 1) * rowsize
1681
1682 record_id = get_vars.get("record", None)
1683 if record_id is not None:
1684 # Ajax-reload of a single record
1685 from s3query import FS
1686 resource.add_filter(FS("id") == record_id)
1687 start = 0
1688 limit = 1
1689 else:
1690 start, limit = self._limits(get_vars)
1691
1692 # Initialize output
1693 output = {}
1694
1695 # Prepare data list
1696 representation = r.representation
1697
1698 # Ajax-delete items?
1699 if representation == "dl" and r.http in ("DELETE", "POST"):
1700 if "delete" in get_vars:
1701 return {"item": self._dl_ajax_delete(r, resource)}
1702 else:
1703 r.error(405, current.ERROR.BAD_METHOD)
1704
1705 if representation in ("html", "dl", "popup"):
1706
1707 # Retrieve the data
1708 if limit:
1709 initial_limit = min(limit, pagelength)
1710 else:
1711 initial_limit = pagelength
1712
1713 # We don't have client-side sorting yet to override
1714 # default-orderby, so fall back unconditionally here:
1715 if not orderby:
1716 orderby = default_orderby
1717
1718 datalist, numrows = resource.datalist(fields = list_fields,
1719 start = start,
1720 limit = initial_limit,
1721 orderby = orderby,
1722 list_id = list_id,
1723 layout = layout,
1724 )
1725
1726 if numrows == 0:
1727 s3.no_formats = True
1728 if r.component and "showadd_btn" in output:
1729 # Hide the list and show the form by default
1730 del output["showadd_btn"]
1731
1732 # Allow customization of the datalist Ajax-URL
1733 # Note: the Ajax-URL must use the .dl representation and
1734 # plain.html view for pagination to work properly!
1735 ajax_url = attr.get("list_ajaxurl", None)
1736 if not ajax_url:
1737 ajax_vars = dict((k,v) for k, v in r.get_vars.iteritems()
1738 if k not in ("start", "limit"))
1739 ajax_url = r.url(representation="dl", vars=ajax_vars)
1740
1741 # Render the list (even if empty => Ajax-section is required
1742 # in any case to be able to Ajax-refresh e.g. after adding
1743 # new records or changing the filter)
1744 if representation == "dl" or not limit:
1745 limit = numrows
1746 dl = datalist.html(start = start if start else 0,
1747 limit = limit,
1748 pagesize = pagelength,
1749 rowsize = rowsize,
1750 ajaxurl = ajax_url)
1751 data = dl
1752 else:
1753 r.error(415, current.ERROR.BAD_FORMAT)
1754
1755
1756 if representation == "html":
1757
1758 # View + data
1759 response.view = self._view(r, "list_filter.html")
1760 output["items"] = data
1761
1762 elif representation == "dl":
1763
1764 # View + data
1765 response.view = "plain.html"
1766 output["item"] = data
1767
1768 elif representation == "popup":
1769
1770 # View + data
1771 response.view = "popup.html"
1772 output["items"] = data
1773 # Pagination Support (normally added by views/dataLists.html)
1774 if s3.debug:
1775 appname = current.request.application
1776 sappend = s3.scripts.append
1777 sappend("/%s/static/scripts/jquery.infinitescroll.js" % appname)
1778 sappend("/%s/static/scripts/jquery.viewport.js" % appname)
1779 sappend("/%s/static/scripts/S3/s3.dataLists.js" % appname)
1780 else:
1781 s3.scripts.append("/%s/static/scripts/S3/s3.dataLists.min.js" % current.request.application)
1782
1783 return output
1784
1785 # -------------------------------------------------------------------------
1787 """
1788 Get a list of unapproved records in this resource
1789
1790 @param r: the S3Request
1791 @param attr: dictionary of parameters for the method handler
1792 """
1793
1794 session = current.session
1795 response = current.response
1796 s3 = response.s3
1797
1798 resource = self.resource
1799 table = self.table
1800
1801 representation = r.representation
1802
1803 output = {}
1804
1805 # Get table-specific parameters
1806 _config = self._config
1807 orderby = _config("orderby", None)
1808 linkto = _config("linkto", None)
1809 list_fields = _config("list_fields")
1810
1811 list_id = "datatable"
1812
1813 # Check permission to read in this table
1814 authorised = self._permitted()
1815 if not authorised:
1816 r.unauthorised()
1817
1818 # Pagination
1819 get_vars = self.request.get_vars
1820 if representation == "aadata":
1821 start, limit = self._limits(get_vars)
1822 else:
1823 start = None
1824 limit = None if s3.no_sspag else 0
1825
1826 # Linkto
1827 if not linkto:
1828 linkto = self._linkto(r)
1829
1830 # List fields
1831 if not list_fields:
1832 fields = resource.readable_fields()
1833 list_fields = [f.name for f in fields]
1834 else:
1835 fields = [table[f] for f in list_fields if f in table.fields]
1836 if not fields:
1837 fields = []
1838
1839 if not fields or \
1840 fields[0].name != table.fields[0]:
1841 fields.insert(0, table[table.fields[0]])
1842 if list_fields[0] != table.fields[0]:
1843 list_fields.insert(0, table.fields[0])
1844
1845 left = []
1846 distinct = False
1847
1848 if r.interactive:
1849
1850 resource.build_query(filter=s3.filter)
1851
1852 # View
1853 response.view = self._view(r, "list.html")
1854
1855 # Page title
1856 crud_string = self.crud_string
1857 if r.component:
1858 title = crud_string(r.tablename, "title_display")
1859 else:
1860 title = crud_string(self.tablename, "title_list")
1861 output["title"] = title
1862
1863 # How many records per page?
1864 if s3.dataTable_pageLength:
1865 display_length = s3.dataTable_pageLength
1866 else:
1867 display_length = 25
1868
1869 # Server-side pagination?
1870 if not s3.no_sspag:
1871 dt_pagination = "true"
1872 if not limit:
1873 limit = 2 * display_length
1874 session.s3.filter = get_vars
1875 if orderby is None:
1876 # Default initial sorting
1877 scol = len(list_fields) > 1 and "1" or "0"
1878 get_vars.update(iSortingCols="1",
1879 iSortCol_0=scol,
1880 sSortDir_0="asc")
1881 orderby, left = resource.datatable_filter(list_fields,
1882 get_vars,
1883 )[1:3]
1884 del get_vars["iSortingCols"]
1885 del get_vars["iSortCol_0"]
1886 del get_vars["sSortDir_0"]
1887 else:
1888 dt_pagination = "false"
1889
1890 # Get the data table
1891 dt, totalrows = resource.datatable(fields = list_fields,
1892 start = start,
1893 limit = limit,
1894 left = left,
1895 orderby = orderby,
1896 distinct = distinct,
1897 )
1898 displayrows = totalrows
1899
1900 # No records?
1901 if dt is None:
1902 s3.no_formats = True
1903 datatable = current.T("No records to review")
1904 else:
1905 dt_dom = s3.get("dataTable_dom",
1906 current.deployment_settings.get_ui_datatables_dom())
1907 datatable = dt.html(totalrows, displayrows, list_id,
1908 dt_pagination=dt_pagination,
1909 dt_pageLength=display_length,
1910 dt_dom = dt_dom,
1911 )
1912 s3.actions = [{"label": s3_str(current.T("Review")),
1913 "url": r.url(id="[id]", method="review"),
1914 "_class": "action-btn"}]
1915
1916 # Add items to output
1917 output["items"] = datatable
1918
1919 elif r.representation == "aadata":
1920
1921 resource.build_query(filter=s3.filter, vars=session.s3.filter)
1922
1923 # Apply datatable filters
1924 searchq, orderby, left = resource.datatable_filter(list_fields, get_vars)
1925 if searchq is not None:
1926 totalrows = resource.count()
1927 resource.add_filter(searchq)
1928 else:
1929 totalrows = None
1930
1931 # Orderby fallbacks
1932 if orderby is None:
1933 orderby = _config("orderby", None)
1934
1935 # Get a data table
1936 if totalrows != 0:
1937 dt, displayrows = resource.datatable(fields = list_fields,
1938 start = start,
1939 limit = limit,
1940 left = left,
1941 orderby = orderby,
1942 distinct = distinct,
1943 )
1944 else:
1945 dt, displayrows = None, 0
1946 if totalrows is None:
1947 totalrows = displayrows
1948
1949 # Echo
1950 draw = int(get_vars.draw or 0)
1951
1952 # Representation
1953 if dt is not None:
1954 output = dt.json(totalrows,
1955 displayrows,
1956 list_id,
1957 draw)
1958 else:
1959 output = '{"recordsTotal": %s, ' \
1960 '"recordsFiltered": 0,' \
1961 '"dataTable_id": "%s", ' \
1962 '"draw": %s, ' \
1963 '"data": []}' % (totalrows, list_id, draw)
1964
1965 else:
1966 r.error(415, current.ERROR.BAD_FORMAT)
1967
1968 return output
1969
1970 # -------------------------------------------------------------------------
1972 """
1973 Review/approve/reject an unapproved record.
1974
1975 @param r: the S3Request
1976 @param attr: dictionary of parameters for the method handler
1977 """
1978
1979 if not self._permitted("review"):
1980 r.unauthorized()
1981
1982 T = current.T
1983
1984 session = current.session
1985 response = current.response
1986
1987 output = Storage()
1988 if r.interactive:
1989
1990 _next = r.url(id="[id]", method="review")
1991
1992 if self._permitted("approve"):
1993
1994 approve = FORM(INPUT(_value=T("Approve"),
1995 _type="submit",
1996 _name="approve-btn",
1997 _id="approve-btn",
1998 _class="action-btn"))
1999
2000 reject = FORM(INPUT(_value=T("Reject"),
2001 _type="submit",
2002 _name="reject-btn",
2003 _id="reject-btn",
2004 _class="action-btn"))
2005
2006 edit = A(T("Edit"),
2007 _href=r.url(id=r.id, method="update",
2008 vars={"_next": r.url(id=r.id, method="review")}),
2009 _class="action-btn")
2010
2011 cancel = A(T("Cancel"),
2012 _href=r.url(id=0),
2013 _class="action-lnk")
2014
2015 output["approve_form"] = DIV(TABLE(TR(approve, reject, edit, cancel)),
2016 _id="approve_form")
2017
2018 reviewing = False
2019 if approve.accepts(r.post_vars, session, formname="approve"):
2020 resource = current.s3db.resource(r.tablename, r.id,
2021 approved=False,
2022 unapproved=True)
2023 try:
2024 success = resource.approve()
2025 except:
2026 success = False
2027 if success:
2028 confirmation = response.confirmation
2029 if confirmation:
2030 response.confirmation = "%s, %s" % (T("Record approved"),
2031 confirmation,
2032 )
2033 else:
2034 response.confirmation = T("Record approved")
2035 output["approve_form"] = ""
2036 else:
2037 response.warning = T("Record could not be approved.")
2038
2039 r.http = "GET"
2040 _next = r.url(id=0, method="review")
2041
2042 elif reject.accepts(r.post_vars, session, formname="reject"):
2043 resource = current.s3db.resource(r.tablename, r.id,
2044 approved=False,
2045 unapproved=True)
2046 try:
2047 success = resource.reject()
2048 except:
2049 success = False
2050 if success:
2051 response.confirmation = T("Record deleted")
2052 output["approve_form"] = ""
2053 else:
2054 response.warning = T("Record could not be deleted.")
2055
2056 r.http = "GET"
2057 _next = r.url(id=0, method="review")
2058
2059 else:
2060 reviewing = True
2061
2062 if reviewing:
2063 output.update(self.read(r, **attr))
2064 self.next = _next
2065 r.http = r.env.request_method
2066 current.response.view = "review.html"
2067
2068 else:
2069 r.error(415, current.ERROR.BAD_FORMAT)
2070
2071 return output
2072
2073 # -------------------------------------------------------------------------
2075 """
2076 Validate records (AJAX). This method reads a JSON object from
2077 the request body, validates it against the current resource,
2078 and returns a JSON object with either the validation errors or
2079 the text representations of the data.
2080
2081 @param r: the S3Request
2082 @param attr: dictionary of parameters for the method handler
2083
2084 Input JSON format:
2085
2086 {"<fieldname>":"<value>", "<fieldname>":"<value>"}
2087
2088 Output JSON format:
2089
2090 {"<fieldname>": {"value":"<value>",
2091 "text":"<representation>",
2092 "_error":"<error message>"}}
2093
2094 The input JSON can also be a list of multiple records. This
2095 will return a list of results accordingly. Note that "text"
2096 is not provided if there was a validation error, and vice
2097 versa.
2098
2099 The record ID should always be present in the JSON to
2100 avoid false duplicate errors.
2101
2102 Non-existent fields will return "invalid field" as _error.
2103
2104 Representations will be URL-escaped and any markup stripped.
2105
2106 The ?component=<alias> URL query can be used to specify a
2107 component of the current resource rather than the main table.
2108
2109 This method does only accept .json format.
2110 """
2111
2112 if r.representation != "json":
2113 r.error(415, current.ERROR.BAD_FORMAT)
2114
2115 resource = self.resource
2116
2117 get_vars = r.get_vars
2118 if "component" in get_vars:
2119 alias = get_vars["component"]
2120 else:
2121 alias = None
2122 if "resource" in get_vars:
2123 tablename = get_vars["resource"]
2124
2125 if tablename != "%s_%s" % (r.controller, r.function):
2126 # Customise the resource
2127 customise = current.deployment_settings.customise_resource(tablename)
2128 if customise:
2129 customise(r, tablename)
2130
2131 components = [alias] if alias else None
2132 try:
2133 resource = current.s3db.resource(tablename,
2134 components = components,
2135 )
2136 except (AttributeError, SyntaxError):
2137 r.error(404, current.ERROR.BAD_RESOURCE)
2138
2139 if alias:
2140 try:
2141 component = resource.components[alias]
2142 except KeyError:
2143 r.error(404, current.ERROR.BAD_RESOURCE)
2144 else:
2145 component = resource
2146
2147 source = r.body
2148 source.seek(0)
2149
2150 try:
2151 data = json.load(source)
2152 except ValueError:
2153 r.error(501, current.ERROR.BAD_SOURCE)
2154
2155 if not isinstance(data, list):
2156 single = True
2157 data = [data]
2158 else:
2159 single = False
2160
2161 table = component.table
2162 pkey = table._id.name
2163
2164 get_config = current.s3db.get_config
2165 tablename = component.tablename
2166 onvalidation = get_config(tablename, "onvalidation")
2167 update_onvalidation = get_config(tablename, "update_onvalidation",
2168 onvalidation)
2169 create_onvalidation = get_config(tablename, "create_onvalidation",
2170 onvalidation)
2171
2172 output = []
2173 for record in data:
2174
2175 has_errors = False
2176
2177 # Retrieve the record ID
2178 if pkey in record:
2179 original = {pkey: record[pkey]}
2180 elif "_id" in record:
2181 original = {pkey: record["_id"]}
2182 else:
2183 original = None
2184
2185 # Field validation
2186 fields = Storage()
2187 for fname in record:
2188
2189 # We do not validate primary keys
2190 # (because we don't update them)
2191 if fname in (pkey, "_id"):
2192 continue
2193
2194 error = None
2195 validated = fields[fname] = Storage()
2196
2197 skip_validation = False
2198 skip_formatting = False
2199
2200 value = record[fname]
2201
2202 if fname not in table.fields:
2203 validated["value"] = value
2204 validated["_error"] = "invalid field"
2205 continue
2206 else:
2207 field = table[fname]
2208
2209 # Convert numeric types (does not always happen in the widget)
2210 widget = field.widget
2211 if widget and hasattr(widget, "s3_parse"):
2212 parser = widget.s3_parse
2213 else:
2214 parser = None
2215 ftype = field.type
2216 if ftype == "integer":
2217 if value not in (None, ""):
2218 if not callable(parser):
2219 parser = int
2220 try:
2221 value = parser(value)
2222 except ValueError:
2223 value = 0
2224 else:
2225 value = None
2226 elif ftype == "double":
2227 if value not in (None, ""):
2228 if not callable(parser):
2229 parser = float
2230 try:
2231 value = parser(value)
2232 except ValueError:
2233 value = 0.0
2234 else:
2235 value = None
2236
2237 # Catch upload fields
2238 if ftype == "upload" and value:
2239
2240 # We cannot Ajax-validate the file (it's not uploaded yet)
2241 skip_validation = True
2242
2243 # We cannot render a link/preview of the file
2244 # (unless it's already uploaded)
2245 try:
2246 fullname = field.retrieve(value, nameonly=True)[1]
2247 except Exception:
2248 skip_formatting = True
2249 else:
2250 import os
2251 skip_formatting = not isinstance(fullname, basestring) or \
2252 not os.path.isfile(fullname)
2253
2254 # Validate and serialize the value
2255 if isinstance(widget, S3Selector):
2256 # Use widget-validator instead of field-validator
2257 if not skip_validation:
2258 value, error = widget.validate(value,
2259 requires=field.requires,
2260 )
2261 validated["value"] = widget.serialize(value) \
2262 if not error else value
2263 # Use widget-represent instead of standard represent
2264 widget_represent = widget.represent
2265 else:
2266 # Validate and format the value
2267 if not skip_validation:
2268 try:
2269 value, error = s3_validate(table, fname, value, original)
2270 except AttributeError:
2271 error = "invalid field"
2272 validated["value"] = field.formatter(value) \
2273 if not error else value
2274 widget_represent = None
2275
2276 # Handle errors, update the validated item
2277 if error:
2278 has_errors = True
2279 validated["_error"] = s3_unicode(error)
2280 elif skip_formatting:
2281 validated["text"] = s3_unicode(value)
2282 elif widget_represent:
2283 try:
2284 text = widget_represent(value)
2285 except:
2286 text = s3_unicode(value)
2287 validated["text"] = text
2288 else:
2289 try:
2290 text = s3_represent_value(field, value = value)
2291 except:
2292 text = s3_unicode(value)
2293 validated["text"] = text
2294
2295 # Form validation (=onvalidation)
2296 if not has_errors:
2297 if original is not None:
2298 onvalidation = update_onvalidation
2299 else:
2300 onvalidation = create_onvalidation
2301 form = Storage(vars=Storage(record), errors=Storage())
2302 if onvalidation is not None:
2303 callback(onvalidation, form, tablename=tablename)
2304 for fn in form.errors:
2305 msg = s3_unicode(form.errors[fn])
2306 if fn in fields:
2307 validated = fields[fn]
2308 has_errors = True
2309 validated._error = msg
2310 if "text" in validated:
2311 del validated["text"]
2312 else:
2313 msg = "%s: %s" % (fn, msg)
2314 if "_error" in fields:
2315 fields["_error"] = "\n".join([msg,
2316 fields["_error"]])
2317 else:
2318 fields["_error"] = msg
2319
2320 output.append(fields)
2321
2322 if single and len(output) == 1:
2323 output = output[0]
2324
2325 return json.dumps(output, separators=SEPARATORS)
2326
2327 # -------------------------------------------------------------------------
2328 # Utility functions
2329 # -------------------------------------------------------------------------
2330 @staticmethod
2399
2400 # -------------------------------------------------------------------------
2402 """
2403 Get the last update meta-data of the current record
2404
2405 @return: a dict {modified_by: <user>, modified_on: <datestr>},
2406 depending on which of these attributes are available
2407 in the current record
2408 """
2409
2410 output = {}
2411 record_id = self.record_id
2412 if record_id:
2413 record = None
2414 fields = []
2415 table = self.table
2416 if "modified_on" in table.fields:
2417 fields.append(table.modified_on)
2418 if "modified_by" in table.fields:
2419 fields.append(table.modified_by)
2420
2421 if fields:
2422 query = (table._id == record_id)
2423 record = current.db(query).select(limitby=(0, 1),
2424 *fields).first()
2425 if record:
2426 T = current.T
2427 if "modified_by" in record:
2428 if not record.modified_by:
2429 modified_by = T("anonymous user")
2430 else:
2431 modified_by = \
2432 table.modified_by.represent(record.modified_by)
2433 output["modified_by"] = T("by %(person)s") % \
2434 {"person": modified_by}
2435 if "modified_on" in record:
2436 modified_on = \
2437 S3DateTime.datetime_represent(record.modified_on,
2438 utc=True,
2439 )
2440 output["modified_on"] = T("on %(date)s") % \
2441 {"date": modified_on}
2442 return output
2443
2444 # -------------------------------------------------------------------------
2563
2564 # -------------------------------------------------------------------------
2565 @staticmethod
2588
2589 # -------------------------------------------------------------------------
2590 @classmethod
2733
2734 # -------------------------------------------------------------------------
2792
2793 # -------------------------------------------------------------------------
2795 """
2796 Import CSV file into database
2797
2798 @param stream: file handle
2799 @param table: the table to import to
2800 """
2801
2802 if table:
2803 table.import_from_csv_file(stream)
2804 else:
2805 db = current.db
2806 # This is the preferred method as it updates reference fields
2807 db.import_from_csv_file(stream)
2808 db.commit()
2809
2810 # -------------------------------------------------------------------------
2811 @staticmethod
2813 """
2814 Import data from vars in URL query
2815
2816 @param r: the S3Request
2817 @note: can only update single records (no mass-update)
2818
2819 @todo: update for link table components
2820 @todo: re-integrate into S3Importer
2821 """
2822
2823 xml = current.xml
2824
2825 table = r.target()[2]
2826
2827 record = r.record
2828 resource = r.resource
2829
2830 # Handle components
2831 if record and r.component:
2832 resource = resource.components[r.component_name]
2833 resource.load()
2834 if len(resource) == 1:
2835 record = resource.records()[0]
2836 else:
2837 record = None
2838 r.vars.update({resource.fkey: r.record[resource.pkey]})
2839 elif not record and r.component:
2840 item = xml.json_message(False, 400, "Invalid Request!")
2841 return {"item": item}
2842
2843 # Check for update
2844 if record and xml.UID in table.fields:
2845 r.vars.update({xml.UID: xml.export_uid(record[xml.UID])})
2846
2847 # Build tree
2848 element = etree.Element(xml.TAG.resource)
2849 element.set(xml.ATTRIBUTE.name, resource.tablename)
2850 for var in r.vars:
2851 if var.find(".") != -1:
2852 continue
2853 elif var in table.fields:
2854 field = table[var]
2855 value = str(r.vars[var]).decode("utf-8")
2856 if var in xml.FIELDS_TO_ATTRIBUTES:
2857 element.set(var, value)
2858 else:
2859 data = etree.Element(xml.TAG.data)
2860 data.set(xml.ATTRIBUTE.field, var)
2861 if field.type == "upload":
2862 data.set(xml.ATTRIBUTE.filename, value)
2863 else:
2864 data.text = value
2865 element.append(data)
2866 tree = xml.tree([element], domain=xml.domain)
2867
2868 # Import data
2869 result = Storage(committed=False)
2870 def log(item):
2871 result["item"] = item
2872 resource.configure(oncommit_import_item = log)
2873 try:
2874 success = resource.import_xml(tree)
2875 except SyntaxError:
2876 pass
2877
2878 # Check result
2879 if result.item:
2880 result = result.item
2881
2882 # Build response
2883 if success and result.committed:
2884 r.id = result.id
2885 method = result.method
2886 if method == result.METHOD.CREATE:
2887 item = xml.json_message(True, 201, "Created as %s?%s.id=%s" %
2888 (str(r.url(method="",
2889 representation="html",
2890 vars={},
2891 )
2892 ),
2893 r.name, result.id)
2894 )
2895 else:
2896 item = xml.json_message(True, 200, "Record updated")
2897 else:
2898 item = xml.json_message(False, 403,
2899 "Could not create/update record: %s" %
2900 resource.error or xml.error,
2901 tree=xml.tree2json(tree))
2902
2903 return {"item": item}
2904
2905
2906 # -------------------------------------------------------------------------
2908 """
2909 Renders the right key constraint in a link table as
2910 S3EmbeddedComponentWidget and stores the postprocess hook.
2911
2912 @param resource: the link table resource
2913 """
2914
2915 link = None
2916
2917 component = resource.linked
2918 if component is not None and component.actuate == "embed":
2919
2920 ctablename = component.tablename
2921 attr = {"link": resource.tablename,
2922 "component": ctablename,
2923 }
2924
2925 autocomplete = component.autocomplete
2926 if autocomplete and autocomplete in component.table:
2927 attr["autocomplete"] = autocomplete
2928
2929 if record is not None:
2930 attr["link_filter"] = "%s.%s.%s.%s.%s" % (
2931 resource.tablename,
2932 component.lkey,
2933 record,
2934 component.rkey,
2935 component.fkey)
2936
2937 rkey = component.rkey
2938 if rkey in resource.table:
2939 field = resource.table[rkey]
2940 field.widget = S3EmbeddedComponentWidget(**attr)
2941 field.comment = None
2942
2943 callback = self._postprocess_embedded
2944 postprocess = lambda form, key=rkey, component=ctablename: \
2945 callback(form, key=key, component=component)
2946 link = Storage(postprocess=postprocess)
2947
2948 return link
2949
2950 # -------------------------------------------------------------------------
2952 """
2953 Post-processes a form with an S3EmbeddedComponentWidget and
2954 created/updates the component record.
2955
2956 @param form: the form
2957 @param component: the component tablename
2958 @param key: the field name of the foreign key for the component
2959 in the link table
2960 """
2961
2962 s3db = current.s3db
2963 request = current.request
2964
2965 get_config = lambda key, tablename=component: \
2966 s3db.get_config(tablename, key, None)
2967 try:
2968 selected = form.vars[key]
2969 except (AttributeError, KeyError):
2970 selected = None
2971
2972 if request.env.request_method == "POST":
2973 db = current.db
2974 table = db[component]
2975
2976 # Extract data for embedded form from post_vars
2977 post_vars = request.post_vars
2978 form_vars = Storage(table._filter_fields(post_vars))
2979
2980 # Pass values through validator to convert them into db-format
2981 for k in form_vars:
2982 value, error = s3_validate(table, k, form_vars[k])
2983 if not error:
2984 form_vars[k] = value
2985
2986 _form = Storage(vars = form_vars, errors = Storage())
2987 if _form.vars:
2988 if selected:
2989 form_vars[table._id.name] = selected
2990 # Onvalidation
2991 onvalidation = get_config("update_onvalidation") or \
2992 get_config("onvalidation")
2993 callback(onvalidation, _form, tablename=component)
2994 # Update the record if no errors
2995 if not _form.errors:
2996 db(table._id == selected).update(**_form.vars)
2997 else:
2998 form.errors.update(_form.errors)
2999 return
3000 # Update super-entity links
3001 s3db.update_super(table, {"id": selected})
3002 # Update realm
3003 update_realm = s3db.get_config(table, "update_realm")
3004 if update_realm:
3005 current.auth.set_realm_entity(table, selected,
3006 force_update=True)
3007 # Onaccept
3008 onaccept = get_config("update_onaccept") or \
3009 get_config("onaccept")
3010 callback(onaccept, _form, tablename=component)
3011 else:
3012 form_vars.pop(table._id.name, None)
3013 # Onvalidation
3014 onvalidation = get_config("create_onvalidation") or \
3015 get_config("onvalidation")
3016 callback(onvalidation, _form, tablename=component)
3017 # Insert the record if no errors
3018 if not _form.errors:
3019 selected = table.insert(**_form.vars)
3020 else:
3021 form.errors.update(_form.errors)
3022 return
3023 if selected:
3024 # Update post_vars and form.vars
3025 post_vars[key] = str(selected)
3026 form.request_vars[key] = str(selected)
3027 form.vars[key] = selected
3028 # Update super-entity links
3029 s3db.update_super(table, {"id": selected})
3030 # Set record owner
3031 auth = current.auth
3032 auth.s3_set_record_owner(table, selected)
3033 auth.s3_make_session_owner(table, selected)
3034 # Onaccept
3035 onaccept = get_config("create_onaccept") or \
3036 get_config("onaccept")
3037 callback(onaccept, _form, tablename=component)
3038 else:
3039 form.errors[key] = current.T("Could not create record.")
3040 return
3041
3042 # -------------------------------------------------------------------------
3044 """
3045 Returns a linker function for the record ID column in list views
3046
3047 @param r: the S3Request
3048 @param authorised: user authorised for update
3049 (override internal check)
3050 @param update: provide link to update rather than to read
3051 @param native: link to the native controller rather than to
3052 component controller
3053 """
3054
3055 c = None
3056 f = None
3057
3058 s3db = current.s3db
3059
3060 prefix, name, _, tablename = r.target()
3061 permit = current.auth.s3_has_permission
3062
3063 if authorised is None:
3064 authorised = permit("update", tablename)
3065
3066 if authorised and update:
3067 linkto = s3db.get_config(tablename, "linkto_update", None)
3068 else:
3069 linkto = s3db.get_config(tablename, "linkto", None)
3070
3071 if r.component and native:
3072 # link to native component controller (be sure that you have one)
3073 c = prefix
3074 f = name
3075
3076 if r.representation == "iframe":
3077 if current.deployment_settings.get_ui_iframe_opens_full():
3078 iframe_safe = lambda url: s3_set_extension(url, "html")
3079 else:
3080 iframe_safe = lambda url: s3_set_extension(url, "iframe")
3081 else:
3082 iframe_safe = False
3083
3084 def list_linkto(record_id, r=r, c=c, f=f,
3085 linkto=linkto,
3086 update=authorised and update):
3087
3088 if linkto:
3089 try:
3090 url = str(linkto(record_id))
3091 except TypeError:
3092 url = linkto % record_id
3093 else:
3094 get_vars = self._linkto_vars(r)
3095
3096 if r.component:
3097 if r.link and not r.actuate_link():
3098 # We're rendering a link table here, but must
3099 # however link to the component record IDs
3100 if str(record_id).isdigit():
3101 # dataTables uses the value in the ID column
3102 # to render action buttons, so we replace that
3103 # value by the component record ID using .represent
3104 _id = r.link.table._id
3105 _id.represent = lambda opt, \
3106 link=r.link, master=r.id: \
3107 link.component_id(master, opt)
3108 # The native link behind the action buttons uses
3109 # record_id, so we replace that too just in case
3110 # the action button cannot be displayed
3111 record_id = r.link.component_id(r.id, record_id)
3112
3113 if c and f:
3114 args = [record_id]
3115 else:
3116 c = r.controller
3117 f = r.function
3118 args = [r.id, r.component_name, record_id]
3119 else:
3120 args = [record_id]
3121
3122 # Add explicit open-method if required
3123 if update != "auto":
3124 if update:
3125 args = args + ["update"]
3126 else:
3127 args = args + ["read"]
3128
3129 url = str(URL(r=r, c=c, f=f, args=args, vars=get_vars))
3130
3131 if iframe_safe:
3132 url = iframe_safe(url)
3133 return url
3134
3135 return list_linkto
3136
3137 # -------------------------------------------------------------------------
3138 @staticmethod
3140 """
3141 Retain certain GET vars of the request in action links
3142
3143 @param r: the S3Request
3144
3145 @return: Storage with GET vars
3146 """
3147
3148 get_vars = r.get_vars
3149 linkto_vars = Storage()
3150
3151 # Retain "viewing"
3152 if not r.component and "viewing" in get_vars:
3153 linkto_vars.viewing = get_vars["viewing"]
3154
3155 keep_vars = current.response.s3.crud.keep_vars
3156 if keep_vars:
3157 for key in keep_vars:
3158 if key in get_vars:
3159 linkto_vars[key] = get_vars[key]
3160
3161 return linkto_vars
3162
3163 # -------------------------------------------------------------------------
3164 @staticmethod
3190
3191 # -------------------------------------------------------------------------
3192 @classmethod
3194
3195 UID = current.xml.UID
3196
3197 delete = r.get_vars.get("delete", None)
3198 if delete is not None:
3199
3200 dresource = current.s3db.resource(resource, id=delete)
3201
3202 # Deleting in this resource allowed at all?
3203 deletable = dresource.get_config("deletable", True)
3204 if not deletable:
3205 r.error(403, current.ERROR.NOT_PERMITTED)
3206
3207 # Permitted to delete this record?
3208 authorised = current.auth.s3_has_permission("delete",
3209 dresource.table,
3210 record_id=delete)
3211 if not authorised:
3212 r.unauthorised()
3213
3214 # Delete it
3215 uid = None
3216 if UID in dresource.table:
3217 rows = dresource.select([UID],
3218 start=0,
3219 limit=1,
3220 as_rows=True)
3221 if rows:
3222 uid = rows[0][UID]
3223 numrows = dresource.delete(format=r.representation)
3224 if numrows > 1:
3225 message = "%s %s" % (numrows,
3226 current.T("records deleted"))
3227 elif numrows == 1:
3228 message = cls.crud_string(dresource.tablename,
3229 "msg_record_deleted")
3230 else:
3231 r.error(404, dresource.error)
3232
3233 # Return a JSON message
3234 # @note: make sure the view doesn't get overridden afterwards!
3235 current.response.view = "xml.html"
3236 return current.xml.json_message(message=message, uuid=uid)
3237 else:
3238 r.error(404, current.ERROR.BAD_RECORD)
3239
3240 # -------------------------------------------------------------------------
3242 """
3243 Set default dates for organizer resources
3244
3245 @param dates: a string with two ISO dates separated by --, like:
3246 "2010-11-29T23:00:00.000Z--2010-11-29T23:59:59.000Z"
3247 """
3248
3249 resource = self.resource
3250
3251 if dates:
3252 dates = dates.split("--")
3253 if len(dates) != 2:
3254 return
3255
3256 from s3organizer import S3Organizer
3257
3258 try:
3259 config = S3Organizer.parse_config(resource)
3260 except AttributeError:
3261 return
3262
3263 start = config["start"]
3264 if start and start.field:
3265 try:
3266 start.field.default = s3_decode_iso_datetime(dates[0])
3267 except ValueError:
3268 pass
3269
3270 end = config["end"]
3271 if end and end.field:
3272 try:
3273 end.field.default = s3_decode_iso_datetime(dates[1])
3274 except ValueError:
3275 pass
3276
3277 # -------------------------------------------------------------------------
3278 @staticmethod
3280 """
3281 Extract page limits (start and limit) from GET vars
3282
3283 @param get_vars: the GET vars
3284 @param default_limit: the default limit, explicit value or:
3285 0 => response.s3.ROWSPERPAGE
3286 None => no default limit
3287 """
3288
3289 start = get_vars.get("start", None)
3290 limit = get_vars.get("limit", default_limit)
3291
3292 # Deal with overrides (pagination limits come last)
3293 if isinstance(start, list):
3294 start = start[-1]
3295 if isinstance(limit, list):
3296 limit = limit[-1]
3297
3298 if limit:
3299 # Ability to override default limit to "Show All"
3300 if isinstance(limit, basestring) and limit.lower() == "none":
3301 #start = None # needed?
3302 limit = None
3303 else:
3304 try:
3305 start = int(start) if start is not None else None
3306 limit = int(limit)
3307 except (ValueError, TypeError):
3308 # Fall back to defaults
3309 start, limit = None, default_limit
3310
3311 else:
3312 # Use defaults, assume sspag because this is a
3313 # pagination request by definition
3314 start = None
3315 limit = default_limit
3316
3317 return start, limit
3318
3319 # END =========================================================================
3320
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:52:04 2019 | http://epydoc.sourceforge.net |