1
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
61 SEPARATORS = (",", ":")
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
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
110
111 elif method == "list":
112 output = self.select(r, **attr)
113
114
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
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
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
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
226 if native:
227
228
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
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
245 buttons = self.render_buttons(r, ["list"], **attr)
246 if buttons:
247 output["buttons"] = buttons
248
249
250 link = None
251 if r.component:
252
253 defaults = r.component.get_defaults(r.record)
254
255 if resource.link is None:
256
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
263 link = self._embed_component(resource, record=r.id)
264
265
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
273 if r.http == "POST":
274 r.post_vars.update({fkey: value})
275
276
277 field.comment = None
278 field.readable = False
279 field.writable = False
280
281 else:
282
283 for (k, v) in defaults.items():
284 table[k].default = v
285
286
287 link = Storage(resource=resource.link, master=r.record)
288
289 get_vars = r.get_vars
290
291
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
310 organizer = get_vars.get("organizer")
311 if organizer:
312 self._set_organizer_dates(organizer)
313
314
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"]
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
348 message = crud_string(self.tablename, "msg_record_created")
349
350
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
379 self._interim_save_button()
380
381
382 if r.representation == "html" and r.method == "create":
383 self._default_cancel_button(r)
384
385
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
403 if self.settings.navigate_away_confirm:
404 response.s3.jquery_ready.append("S3EnableNavigateAwayConfirm()")
405
406
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
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
444
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
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
583
584
585 - def read(self, r, **attr):
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
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
613 record_id = self.record_id
614
615 if r.interactive:
616
617 component = r.component
618
619
620
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
626 r.method = "create"
627 return self.create(r, **attr)
628 else:
629 empty = False
630
631
632
633
634 if not r.method:
635 authorised = self._permitted("update")
636 if authorised and representation == "html" and editable:
637 return self.update(r, **attr)
638
639
640 subheadings = _config("subheadings")
641
642
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
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
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
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
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
701 last_update = self.last_update()
702 if last_update:
703 try:
704 output["modified_on"] = last_update["modified_on"]
705 except KeyError:
706
707 pass
708 try:
709 output["modified_by"] = last_update["modified_by"]
710 except KeyError:
711
712 pass
713
714
715 from s3merge import S3Merge
716 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id)
717
718 elif representation == "plain":
719
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
733 for field in fields:
734 try:
735 value = record[field]
736 except KeyError:
737
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
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
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
759
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
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
787
788
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
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
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
859 _config = self._config
860 editable = _config("editable", True)
861
862
863 onvalidation = _config("update_onvalidation") or \
864 _config("onvalidation")
865 onaccept = _config("update_onaccept") or \
866 _config("onaccept")
867
868
869 record_id = self.record_id
870 if r.interactive and not record_id:
871 r.error(404, current.ERROR.BAD_RECORD)
872
873
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
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
891 subheadings = _config("subheadings")
892
893
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
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
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
935 message = crud_string(self.tablename, "msg_record_modified")
936
937
938 self._interim_save_button()
939
940
941 if r.representation == "html" and \
942 (r.method == "update" or not r.method):
943 self._default_cancel_button(r)
944
945
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
962 if self.settings.navigate_away_confirm:
963 s3.jquery_ready.append("S3EnableNavigateAwayConfirm()")
964
965
966 output["form"] = form
967 if representation == "plain":
968 output["item"] = form
969 output["title"] = ""
970
971
972 buttons = self.render_buttons(r,
973 ["delete"],
974 record_id=record_id,
975 **attr)
976 if buttons:
977 output["buttons"] = buttons
978
979
980 last_update = self.last_update()
981 if last_update:
982 try:
983 output["modified_on"] = last_update["modified_on"]
984 except KeyError:
985
986 pass
987 try:
988 output["modified_by"] = last_update["modified_by"]
989 except KeyError:
990
991 pass
992
993
994 from s3merge import S3Merge
995 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id)
996
997
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
1029 - def delete(self, r, **attr):
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
1042 config = self._config
1043 deletable = config("deletable", True)
1044 delete_next = config("delete_next", None)
1045
1046
1047 if not deletable:
1048 r.error(403, current.ERROR.NOT_PERMITTED,
1049 next=r.url(method=""))
1050
1051
1052 record_id = self.record_id
1053
1054
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
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
1073 return output
1074
1075 elif r.interactive and (r.http == "POST" or
1076 r.http == "GET" and record_id):
1077
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"
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
1099
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
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
1117
1118 resource.error = current.T("Deletion failed")
1119 numrows = 0
1120
1121 if numrows is None:
1122
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
1143 - def select(self, r, **attr):
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
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
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
1187 if "dt_searching" not in dtargs:
1188 dtargs["dt_searching"] = False
1189
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
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
1218 if show_filter_form:
1219
1220
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
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)
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
1256 output["list_filter_form"] = ""
1257
1258
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
1267 response = current.response
1268 view = response.view
1269
1270
1271 s3 = response.s3
1272 cancel = s3.cancel
1273 s3.cancel = {"hide": "list-add", "show": "show-add-btn"}
1274
1275
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
1290 response.view = view
1291 s3.cancel = cancel
1292
1293 elif addbtn:
1294
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
1305
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
1346 start, limit = self._limits(get_vars, default_limit=None)
1347
1348
1349 tooltip = get_vars.get("tooltip", None)
1350
1351
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
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
1436 authorised = self._permitted()
1437 if not authorised:
1438 r.unauthorised()
1439
1440 resource = self.resource
1441 get_config = resource.get_config
1442
1443
1444 linkto = get_config("linkto", None)
1445
1446
1447 list_id = attr.get("list_id", "datatable")
1448
1449
1450 list_fields = resource.list_fields()
1451
1452
1453 orderby = get_config("orderby", None)
1454
1455 response = current.response
1456 s3 = response.s3
1457 representation = r.representation
1458
1459
1460 get_vars = self.request.get_vars
1461 if representation == "aadata":
1462 start, limit = self._limits(get_vars)
1463 else:
1464
1465
1466
1467 start = None
1468 limit = None if s3.no_sspag else 0
1469
1470
1471 output = {}
1472
1473
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
1484 if s3.dataTable_pageLength:
1485 display_length = s3.dataTable_pageLength
1486 else:
1487 display_length = 25
1488
1489
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
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
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538 if r.component and "showadd_btn" in output:
1539
1540 del output["showadd_btn"]
1541 datatable = ""
1542
1543
1544
1545
1546
1547
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
1558 response.view = self._view(r, "list_filter.html")
1559 output["items"] = datatable
1560
1561 elif representation == "aadata":
1562
1563
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
1573 if orderby is None:
1574 orderby = get_config("orderby", None)
1575
1576
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
1591 draw = int(get_vars.get("draw", 0))
1592
1593
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
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
1631 layout = get_config("list_layout", None)
1632
1633
1634 list_id = get_vars.get("list_id",
1635 attr.get("list_id", "datalist"))
1636
1637
1638 if hasattr(layout, "list_fields"):
1639 list_fields = layout.list_fields
1640 else:
1641 list_fields = resource.list_fields()
1642
1643
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
1659 response = current.response
1660 s3 = response.s3
1661
1662
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
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
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
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
1693 output = {}
1694
1695
1696 representation = r.representation
1697
1698
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
1708 if limit:
1709 initial_limit = min(limit, pagelength)
1710 else:
1711 initial_limit = pagelength
1712
1713
1714
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
1730 del output["showadd_btn"]
1731
1732
1733
1734
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
1742
1743
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
1759 response.view = self._view(r, "list_filter.html")
1760 output["items"] = data
1761
1762 elif representation == "dl":
1763
1764
1765 response.view = "plain.html"
1766 output["item"] = data
1767
1768 elif representation == "popup":
1769
1770
1771 response.view = "popup.html"
1772 output["items"] = data
1773
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
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
1814 authorised = self._permitted()
1815 if not authorised:
1816 r.unauthorised()
1817
1818
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
1827 if not linkto:
1828 linkto = self._linkto(r)
1829
1830
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
1853 response.view = self._view(r, "list.html")
1854
1855
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
1864 if s3.dataTable_pageLength:
1865 display_length = s3.dataTable_pageLength
1866 else:
1867 display_length = 25
1868
1869
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
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
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
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
1917 output["items"] = datatable
1918
1919 elif r.representation == "aadata":
1920
1921 resource.build_query(filter=s3.filter, vars=session.s3.filter)
1922
1923
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
1932 if orderby is None:
1933 orderby = _config("orderby", None)
1934
1935
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
1950 draw = int(get_vars.draw or 0)
1951
1952
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
1971 - def review(self, r, **attr):
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
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
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
2186 fields = Storage()
2187 for fname in record:
2188
2189
2190
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
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
2238 if ftype == "upload" and value:
2239
2240
2241 skip_validation = True
2242
2243
2244
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
2255 if isinstance(widget, S3Selector):
2256
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
2264 widget_represent = widget.represent
2265 else:
2266
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
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
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
2329
2330 @staticmethod
2399
2400
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
2807 db.import_from_csv_file(stream)
2808 db.commit()
2809
2810
2811 @staticmethod
2872 resource.configure(oncommit_import_item = log)
2873 try:
2874 success = resource.import_xml(tree)
2875 except SyntaxError:
2876 pass
2877
2878
2879 if result.item:
2880 result = result.item
2881
2882
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
2951 - def _postprocess_embedded(self, form, component=None, key=None):
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
2977 post_vars = request.post_vars
2978 form_vars = Storage(table._filter_fields(post_vars))
2979
2980
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
2991 onvalidation = get_config("update_onvalidation") or \
2992 get_config("onvalidation")
2993 callback(onvalidation, _form, tablename=component)
2994
2995 if not _form.errors:
2996 db(table._id == selected).update(**_form.vars)
2997 else:
2998 form.errors.update(_form.errors)
2999 return
3000
3001 s3db.update_super(table, {"id": selected})
3002
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
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
3014 onvalidation = get_config("create_onvalidation") or \
3015 get_config("onvalidation")
3016 callback(onvalidation, _form, tablename=component)
3017
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
3025 post_vars[key] = str(selected)
3026 form.request_vars[key] = str(selected)
3027 form.vars[key] = selected
3028
3029 s3db.update_super(table, {"id": selected})
3030
3031 auth = current.auth
3032 auth.s3_set_record_owner(table, selected)
3033 auth.s3_make_session_owner(table, selected)
3034
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
3043 - def _linkto(self, r, authorised=None, update=None, native=False):
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
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
3099
3100 if str(record_id).isdigit():
3101
3102
3103
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
3109
3110
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
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
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
3203 deletable = dresource.get_config("deletable", True)
3204 if not deletable:
3205 r.error(403, current.ERROR.NOT_PERMITTED)
3206
3207
3208 authorised = current.auth.s3_has_permission("delete",
3209 dresource.table,
3210 record_id=delete)
3211 if not authorised:
3212 r.unauthorised()
3213
3214
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
3234
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
3279 - def _limits(get_vars, default_limit=0):
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
3293 if isinstance(start, list):
3294 start = start[-1]
3295 if isinstance(limit, list):
3296 limit = limit[-1]
3297
3298 if limit:
3299
3300 if isinstance(limit, basestring) and limit.lower() == "none":
3301
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
3309 start, limit = None, default_limit
3310
3311 else:
3312
3313
3314 start = None
3315 limit = default_limit
3316
3317 return start, limit
3318
3319
3320