Package vita ::
Package modules ::
Package s3 ::
Module s3rest
|
|
1
2
3 """ RESTful API
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 @author: Dominic König <dominic[at]aidiq.com>
11
12 @copyright: 2009-2011 (c) Sahana Software Foundation
13 @license: MIT
14
15 Permission is hereby granted, free of charge, to any person
16 obtaining a copy of this software and associated documentation
17 files (the "Software"), to deal in the Software without
18 restriction, including without limitation the rights to use,
19 copy, modify, merge, publish, distribute, sublicense, and/or sell
20 copies of the Software, and to permit persons to whom the
21 Software is furnished to do so, subject to the following
22 conditions:
23
24 The above copyright notice and this permission notice shall be
25 included in all copies or substantial portions of the Software.
26
27 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 OTHER DEALINGS IN THE SOFTWARE.
35
36 """
37
38 __all__ = ["S3Resource", "S3Request", "S3Method"]
39
40 import os, sys, cgi, uuid, datetime, time, urllib, StringIO, re
41 import gluon.contrib.simplejson as json
42
43 from gluon.storage import Storage
44 from gluon.sql import Row, Rows
45 from gluon.html import *
46 from gluon.http import HTTP, redirect
47 from gluon.sqlhtml import SQLTABLE, SQLFORM
48 from gluon.validators import IS_EMPTY_OR
49 from gluon.tools import callback
50
51 from lxml import etree
52 from s3tools import SQLTABLES3
53 from s3import import S3Importer
57 """
58 API for resources
59
60 """
61
62 - def __init__(self, manager, prefix, name,
63 id=None,
64 uid=None,
65 filter=None,
66 vars=None,
67 parent=None,
68 components=None):
69 """
70 Constructor
71
72 @param manager: the S3ResourceController
73 @param prefix: prefix of the resource name (=module name)
74 @param name: name of the resource (without prefix)
75 @param id: record ID (or list of record IDs)
76 @param uid: record UID (or list of record UIDs)
77 @param filter: filter query (DAL resources only)
78 @param vars: dictionary of URL query variables
79 @param parent: the parent resource
80 @param components: component name (or list of component names)
81
82 """
83
84 self.manager = manager
85 self.db = manager.db
86
87 self.HOOKS = manager.HOOKS
88 self.ERROR = manager.ERROR
89
90
91 self.exporter = manager.exporter
92 self.importer = manager.importer
93
94 self.xml = manager.xml
95
96
97 self.XSLT_PATH = "static/formats"
98 self.XSLT_EXTENSION = "xsl"
99
100
101 self.permit = manager.permit
102 self.accessible_query = manager.auth.s3_accessible_query
103
104
105 self.audit = manager.audit
106
107
108 self.prefix = prefix
109 self.name = name
110 self.vars = None
111
112
113 self.tablename = "%s_%s" % (self.prefix, self.name)
114 self.table = self.db.get(self.tablename, None)
115 if not self.table:
116 raise KeyError("Undefined table: %s" % self.tablename)
117 model = self.manager.model
118
119
120 self.query_builder = manager.query_builder
121 self._query = None
122 self._multiple = True
123
124
125 self._rows = None
126 self._ids = []
127 self._uids = []
128 self._length = None
129 self._slice = False
130
131
132 self.lastid = None
133
134 self.files = Storage()
135
136
137 self.components = Storage()
138 self.parent = parent
139
140 if self.parent is None:
141
142
143 if components and not isinstance(components, (list, tuple)):
144 components = [components]
145 clist = model.get_components(self.prefix, self.name)
146 for i in xrange(len(clist)):
147 c, pkey, fkey = clist[i]
148 if components and c.name not in components:
149 continue
150 resource = S3Resource(self.manager, c.prefix, c.name,
151 parent=self)
152 self.components[c.name] = Storage(component=c,
153 pkey=pkey,
154 fkey=fkey,
155 resource=resource,
156 filter=None)
157
158
159 self.build_query(id=id, uid=uid, filter=filter, vars=vars)
160
161
162 self.crud = self.manager.crud
163
164 self.search = model.get_config(self.table, "search_method", None)
165 if not self.search:
166 if "name" in self.table:
167 T = self.manager.T
168 self.search = self.manager.search(
169 name="search_simple",
170 label=T("Name"),
171 comment=T("Enter a name to search for. You may use % as wildcard. Press 'Search' without input to list all items."),
172 field=["name"])
173 else:
174 self.search = self.manager.search()
175
176
177 self._handler = Storage(options=self.__get_options,
178 fields=self.__get_fields,
179 export_tree=self.__get_tree,
180 import_tree=self.__put_tree)
181
182
183
185 """
186 Set a REST method handler for this resource
187
188 @param method: the method name
189 @param handler: the handler function
190 @type handler: handler(S3Request, **attr)
191
192 """
193
194 self._handler[method] = handler
195
196
197
199 """
200 Get a REST method handler for this resource
201
202 @param method: the method name
203 @returns: the handler function
204
205 """
206
207 return self._handler.get(method, None)
208
209
210
212 """
213 Add a REST method for this resource
214
215 @param method: the method name
216 @param handler: the handler function
217 @type handler: handler(S3Request, **attr)
218
219 """
220
221 model = self.manager.model
222
223 if self.parent:
224 model.set_method(self.parent.prefix, self.parent.name,
225 component=self.name, method=method, action=handler)
226 else:
227 model.set_method(self.prefix,self.name,
228 method=method, action=handler)
229
230
231
232
233 - def build_query(self, id=None, uid=None, filter=None, vars=None):
234 """
235 Query builder
236
237 @param id: record ID or list of record IDs to include
238 @param uid: record UID or list of record UIDs to include
239 @param filter: filtering query (DAL only)
240 @param vars: dict of URL query variables
241
242 """
243
244
245 self._length = None
246
247
248 return self.query_builder.query(self,
249 id=id,
250 uid=uid,
251 filter=filter,
252 vars=vars)
253
254
255
257 """
258 Extend the current query by a filter query
259
260 @param filter: a web2py Query object
261
262 """
263
264 if filter is not None:
265 if self._query:
266 query = self._query
267 self.clear()
268 self.clear_query()
269 self._query = (query) & (filter)
270 else:
271 self.build_query(filter=filter)
272 return self._query
273
274
275
277 """
278 Extend the filter query of a particular component
279
280 @param name: the name of the component
281 @param filter: a web2py Query object
282
283 """
284
285 component = self.components.get(name, None)
286 if component is not None:
287 if component.filter is not None:
288 component.filter = (component.filter) & (filter)
289 else:
290 component.filter = filter
291
292
293
295 """
296 Get the current query for this resource
297
298 """
299
300 if not self._query:
301 self.build_query()
302 return self._query
303
304
305
307 """
308 Removes the current query (does not remove the set!)
309
310 """
311
312 self._query = None
313 if self.components:
314 for c in self.components:
315 self.components[c].resource.clear_query()
316
317
318
319
320 - def select(self, *fields, **attributes):
321 """
322 Select records with the current query
323
324 @param fields: fields to select
325 @param attributes: select attributes
326
327 """
328
329 table = self.table
330 if self._query is None:
331 self.build_query()
332
333
334 rows = self.db(self._query).select(*fields, **attributes)
335
336
337 audit = self.manager.audit
338 try:
339
340 if self.tablename in rows:
341 ids = [r[str(self.table.id)] for r in rows]
342 else:
343 ids = [r.id for r in rows]
344 for i in ids:
345 audit("read", self.prefix, self.name, record=i)
346 except KeyError:
347
348 audit("list", self.prefix, self.name)
349
350
351 self._rows = rows
352
353 return rows
354
355
356
357 - def load(self, start=None, limit=None):
358 """
359 Simplified syntax for select():
360 - reads all fields
361 - start+limit instead of limitby
362
363 @param start: the index of the first record to load
364 @param limit: the maximum number of records to load
365
366 """
367
368 if self._rows is not None:
369 self.clear()
370
371 if not self._query:
372 self.build_query()
373 if not self._multiple:
374 limitby = (0, 1)
375 else:
376 limitby = self.limitby(start=start, limit=limit)
377
378 if limitby:
379 rows = self.select(self.table.ALL, limitby=limitby)
380 else:
381 rows = self.select(self.table.ALL)
382
383 self._ids = [row.id for row in rows]
384 uid = self.manager.UID
385 if uid in self.table.fields:
386 self._uids = [row[uid] for row in rows]
387
388 return self
389
390
391
393 """
394 Insert records into this resource
395
396 @param fields: dict of fields to insert
397
398 """
399
400
401 authorised = self.permit("create", self.tablename)
402 if not authorised:
403 raise IOError("Operation not permitted: INSERT INTO %s" % self.tablename)
404
405
406 record_id = self.table.insert(**fields)
407
408
409 if record_id:
410 record = Storage(fields).update(id=record_id)
411 self.audit("create", self.prefix, self.name, form=record)
412
413 return record_id
414
415
416
417 - def delete(self, ondelete=None, format=None):
418 """
419 Delete all (deletable) records in this resource
420
421 @param ondelete: on-delete callback
422 @param format: the representation format of the request (optional)
423
424 @returns: number of records deleted
425
426 """
427
428 model = self.manager.model
429
430 settings = self.manager.s3.crud
431 archive_not_delete = settings.archive_not_delete
432
433 records = self.select(self.table.id)
434
435 numrows = 0
436 for row in records:
437
438
439 if not self.permit("delete", self.table, record_id=row.id):
440 continue
441
442
443 if self.manager.get_session(prefix=self.prefix, name=self.name) == row.id:
444 self.manager.clear_session(prefix=self.prefix, name=self.name)
445
446
447 try:
448 del self.table[row.id]
449 except:
450 self.manager.error = self.ERROR.INTEGRITY_ERROR
451 finally:
452
453 self.db.rollback()
454
455 if self.manager.error != self.ERROR.INTEGRITY_ERROR:
456
457 if archive_not_delete and "deleted" in self.table:
458 fields = dict(deleted=True)
459 if "deleted_fk" in self.table:
460
461
462 record = self.table[row.id]
463 fk = []
464 for f in self.table.fields:
465 ftype = str(self.table[f].type)
466 if record[f] is not None and \
467 (ftype[:9] == "reference" or \
468 ftype[:14] == "list:reference"):
469 fk.append(dict(f=f, k=record[f]))
470 fields.update({f:None})
471 else:
472 continue
473 if fk:
474 fields.update(deleted_fk=json.dumps(fk))
475 self.db(self.table.id == row.id).update(**fields)
476 numrows += 1
477 self.audit("delete", self.prefix, self.name,
478 record=row.id, representation=format)
479 model.delete_super(self.table, row)
480 if ondelete:
481 callback(ondelete, row)
482
483 else:
484 del self.table[row.id]
485 numrows += 1
486 self.audit("delete", self.prefix, self.name,
487 record=row.id, representation=format)
488 model.delete_super(self.table, row)
489 if ondelete:
490 callback(ondelete, row)
491
492 return numrows
493
494
495
496 - def update(self, **update_fields):
497 """
498 Update all records in this resource
499
500 @todo: permission check
501 @todo: audit
502
503 @status: uncompleted
504
505 """
506
507 if not self._query:
508 self.build_query()
509
510 success = self.db(self._query).update(**update_fields)
511 return success
512
513
514
515 - def search_simple(self, fields=None, label=None, filterby=None):
516 """
517 Simple fulltext search
518
519 @param fields: list of fields to search for the label
520 @param label: label to be found
521 @param filterby: filter query for results
522
523 """
524
525 table = self.table
526 prefix = self.prefix
527 name = self.name
528
529 model = self.manager.model
530
531 mq = Storage()
532 search_fields = Storage()
533
534 if fields and not isinstance(fields, (list, tuple)):
535 fields = [fields]
536 elif not fields:
537 raise SyntaxError("No search fields specified.")
538
539 for f in fields:
540 _table = None
541 component = None
542
543 if f.find(".") != -1:
544 cname, f = f.split(".", 1)
545 component, pkey, fkey = model.get_component(prefix, name, cname)
546 if component:
547 _table = component.table
548 tablename = component.tablename
549
550 if not self.db(_table.id>0).select(_table.id, limitby=(0,1)).first():
551 continue
552 else:
553 _table = table
554 tablename = table._tablename
555
556 if _table and tablename not in mq:
557 query = (self.accessible_query("read", _table))
558 if "deleted" in _table.fields:
559 query = (query & (_table.deleted == "False"))
560 if component:
561 join = (table[pkey] == _table[fkey])
562 query = (query & join)
563 mq[_table._tablename] = query
564
565 if _table and f in _table.fields:
566 if _table._tablename not in search_fields:
567 search_fields[tablename] = [_table[f]]
568 else:
569 search_fields[tablename].append(_table[f])
570
571 if not search_fields:
572 return None
573
574 if label and isinstance(label,str):
575 labels = label.split()
576 results = []
577
578 for l in labels:
579 wc = "%"
580 _l = "%s%s%s" % (wc, l.lower(), wc)
581
582 query = None
583 for tablename in search_fields:
584 hq = mq[tablename]
585 fq = None
586 fields = search_fields[tablename]
587 for f in fields:
588 if fq:
589 fq = (f.lower().like(_l)) | fq
590 else:
591 fq = (f.lower().like(_l))
592 q = hq & fq
593 if query is None:
594 query = q
595 else:
596 query = query | q
597
598 if results:
599 query = (table.id.belongs(results)) & query
600 if filterby:
601 query = (filterby) & (query)
602
603 records = self.db(query).select(table.id)
604 results = [r.id for r in records]
605 if not results:
606 return None
607
608 return results
609 else:
610 return None
611
612
613
614 - def count(self, left=None):
615 """
616 Get the total number of available records in this resource
617
618 @param left: left joins, if required
619
620 """
621
622 if not self._query:
623 self.build_query()
624
625 if self._length is None:
626 cnt = self.table[self.table.fields[0]].count()
627 row = self.db(self._query).select(cnt, left=left).first()
628 if row:
629 self._length = row[cnt]
630
631 return self._length
632
633
634
636 """
637 Removes the current set
638
639 """
640
641 self._rows = None
642 self._length = None
643 self._ids = []
644 self._uids = []
645 self.files = Storage()
646 self._slice = False
647
648 if self.components:
649 for c in self.components:
650 self.components[c].resource.clear()
651
652
653
655 """
656 Get the current set
657
658 @returns: a Set or an empty list if no set is loaded
659
660 """
661
662 if self._rows is None:
663 return Rows(self.db)
664 else:
665 if fields is not None:
666 self._rows.colnames = map(str, fields)
667 return self._rows
668
669
670
672 """
673 Retrieves a record from the current set by its ID
674
675 @param key: the record ID
676 @returns: a Row
677
678 """
679
680 if self._rows is None:
681 self.load()
682 for i in xrange(len(self._rows)):
683 row = self._rows[i]
684 if str(row.id) == str(key):
685 return row
686
687 raise IndexError
688
689
690
692 """
693 Iterate over the selected rows
694
695 """
696
697 if self._rows is None:
698 self.load()
699 for i in xrange(len(self._rows)):
700 yield self._rows[i]
701 return
702
703
704
705 - def __call__(self, key, component=None):
706 """
707 Retrieves component records of a record in the current set
708
709 @param key: the record ID
710 @param component: the name of the component
711 (None to get the primary record)
712 @returns: a record (if component is None) or a list of records
713
714 """
715
716 if not component:
717 return self[key]
718 else:
719 if isinstance(key, Row):
720 master = key
721 else:
722 master = self[key]
723 if component in self.components:
724 c = self.components[component]
725 r = c.resource
726 pkey, fkey = c.pkey, c.fkey
727 l = [record for record in r if master[pkey] == record[fkey]]
728 return l
729 else:
730 raise AttributeError
731
732
733
735 """
736 Returns all IDs of the current set, or, if no set is loaded,
737 all IDs of the resource
738
739 @returns: a list of record IDs
740
741 """
742
743 if not self._ids:
744 self.__load_ids()
745 if not self._ids:
746 return None
747 elif len(self._ids) == 1:
748 return self._ids[0]
749 else:
750 return self._ids
751
752
753
755 """
756 Returns all UIDs of the current set, or, if no set is loaded,
757 all UIDs of the resource
758
759 @returns: a list of record UIDs
760
761 """
762
763 if self.manager.UID not in self.table.fields:
764 return None
765
766 if not self._uids:
767 self.__load_ids()
768 if not self._uids:
769 return None
770 elif len(self._uids) == 1:
771 return self._uids[0]
772 else:
773 return self._uids
774
775
776
778 """
779 Loads the IDs of all records matching the master query, or,
780 if no query is given, all IDs in the primary table
781
782 """
783
784 uid = self.manager.UID
785
786 if self._query is None:
787 self.build_query()
788 if uid in self.table.fields:
789 fields = (self.table.id, self.table[uid])
790 else:
791 fields = (self.table.id,)
792
793 rows = self.db(self._query).select(*fields)
794
795 self._ids = [row.id for row in rows]
796
797 if uid in self.table.fields:
798 self._uids = [row[uid] for row in rows]
799
800
801
802
804 """
805 String representation of this resource
806
807 """
808
809 if self._rows:
810 ids = [r.id for r in self]
811 return "<S3Resource %s %s>" % (self.tablename, ids)
812 else:
813 return "<S3Resource %s>" % self.tablename
814
815
816
818 """
819 The number of currently loaded rows
820
821 """
822
823 if self._rows is not None:
824 return len(self._rows)
825 else:
826 return 0
827
828
829
831 """
832 Boolean test of this resource
833
834 """
835
836 return self is not None
837
838
839
841 """
842 Tests whether a record is currently loaded
843
844 """
845
846 id = item.get("id", None)
847 uid = item.get(self.manager.UID, None)
848
849 if (id or uid) and not self._ids:
850 self.__load_ids()
851 if id and id in self._ids:
852 return 1
853 elif uid and uid in self._uids:
854 return 1
855 else:
856 return 0
857
858
859
860
862 """
863 Execute a HTTP request
864
865 @param r: the request to execute
866 @type r: S3Request
867 @param attr: attributes to pass to method handlers
868
869 """
870
871 r.resource = self
872 r.next = None
873 hooks = r.response.get(self.HOOKS, None)
874 bypass = False
875 output = None
876 preprocess = None
877 postprocess = None
878
879
880 if not r.id and r.representation == "html":
881 if r.component or r.method in ("read", "update"):
882 count = self.count()
883 if self.vars is not None and count == 1:
884 self.load()
885 r.record = self._rows.first()
886 else:
887 model = self.manager.model
888 if self.search.interactive_search:
889 redirect(URL(r=r.request, f=self.name, args="search",
890 vars={"_next": r.same()}))
891 else:
892 r.session.error = self.ERROR.BAD_RECORD
893 redirect(URL(r=r.request, c=self.prefix, f=self.name))
894
895
896 if hooks is not None:
897 preprocess = hooks.get("prep", None)
898 if preprocess:
899 pre = preprocess(r)
900 if pre and isinstance(pre, dict):
901 bypass = pre.get("bypass", False) is True
902 output = pre.get("output", None)
903 if not bypass:
904 success = pre.get("success", True)
905 if not success:
906 if r.representation == "html" and output:
907 if isinstance(output, dict):
908 output.update(jr=r)
909 return output
910 else:
911 status = pre.get("status", 400)
912 message = pre.get("message", self.ERROR.BAD_REQUEST)
913 r.error(status, message)
914 elif not pre:
915 r.error(400, self.ERROR.BAD_REQUEST)
916
917
918 if r.representation != "html":
919 r.response.view = "xml.html"
920
921
922 if not r.custom_action:
923 model = self.manager.model
924 r.custom_action = model.get_method(r.prefix, r.name,
925 component_name=r.component_name,
926 method=r.method)
927
928
929 handler = None
930 if not bypass:
931
932 if r.method and r.custom_action:
933 handler = r.custom_action
934 elif r.http == "GET":
935 handler = self.__get(r)
936 elif r.http == "PUT":
937 handler = self.__put(r)
938 elif r.http == "POST":
939 handler = self.__post(r)
940 elif r.http == "DELETE":
941 handler = self.__delete(r)
942 else:
943 r.error(501, self.ERROR.BAD_METHOD)
944
945 if handler is not None:
946 output = handler(r, **attr)
947 elif r.method == "search":
948 output = self.search(r, **attr)
949 else:
950
951 output = self.crud(r, **attr)
952
953
954 if hooks is not None:
955 postprocess = hooks.get("postp", None)
956 if postprocess is not None:
957 output = postprocess(r, output)
958 if output is not None and isinstance(output, dict):
959
960 output.update(jr=r)
961
962
963 if r.next is not None and (r.http != "GET" or r.method == "clear"):
964 if isinstance(output, dict):
965 form = output.get("form", None)
966 if form:
967 if not hasattr(form, "errors"):
968 form = form[0]
969 if form.errors:
970 return output
971 r.session.flash = r.response.flash
972 r.session.confirmation = r.response.confirmation
973 r.session.error = r.response.error
974 r.session.warning = r.response.warning
975 redirect(r.next)
976
977 return output
978
979
980
982 """
983 Get the GET method handler
984
985 @param r: the S3Request
986
987 """
988
989 method = r.method
990 model = self.manager.model
991
992 tablename = r.component and r.component.tablename or r.tablename
993
994 if method is None or method in ("read", "display"):
995 if self.__transformable(r):
996 method = "export_tree"
997 elif r.component:
998 if r.interactive and self.count() == 1:
999
1000 if not self._rows:
1001 self.load(start=0, limit=1)
1002 if self._rows:
1003 r.record = self._rows[0]
1004 r.id = self.get_id()
1005 r.uid = self.get_uid()
1006 if r.multiple and not r.component_id:
1007 method = "list"
1008 else:
1009 method = "read"
1010 else:
1011 if r.id or method in ("read", "display"):
1012
1013 if not self._rows:
1014 self.load(start=0, limit=1)
1015 if self._rows:
1016 r.record = self._rows[0]
1017 r.id = self.get_id()
1018 r.uid = self.get_uid()
1019 else:
1020 r.error(404, self.ERROR.BAD_RECORD)
1021 method = "read"
1022 else:
1023 method = "list"
1024
1025 elif method in ("create", "update"):
1026 if self.__transformable(r, method="import"):
1027 method = "import_tree"
1028
1029 elif method == "delete":
1030 return self.__delete(r)
1031
1032 elif method == "clear" and not r.component:
1033 self.manager.clear_session(self.prefix, self.name)
1034 if "_next" in r.request.vars:
1035 request_vars = dict(_next=r.request.vars._next)
1036 else:
1037 request_vars = {}
1038 if r.representation == "html" and self.search.interactive_search:
1039 r.next = URL(r=r.request,
1040 f=self.name,
1041 args="search",
1042 vars=request_vars)
1043 else:
1044 r.next = URL(r=r.request, f=self.name)
1045 return lambda r, **attr: None
1046
1047 return self.get_handler(method)
1048
1049
1050
1052 """
1053 Get the PUT method handler
1054
1055 @param r: the S3Request
1056
1057 """
1058
1059 if self.__transformable(r, method="import"):
1060 return self.get_handler("import_tree")
1061 else:
1062 r.error(501, self.ERROR.BAD_FORMAT)
1063
1064
1065
1066 - def __post(self, r):
1067 """
1068 Get the POST method handler
1069
1070 @param r: the S3Request
1071
1072 """
1073
1074 method = r.method
1075
1076 if method == "delete":
1077 return self.__delete(r)
1078 else:
1079 if self.__transformable(r, method="import"):
1080 return self.__put(r)
1081 else:
1082 post_vars = r.request.post_vars
1083 table = r.target()[2]
1084 if "deleted" in table and \
1085 "id" not in post_vars and "uuid" not in post_vars:
1086 original = self.manager.original(table, post_vars)
1087 if original and original.deleted:
1088 r.request.post_vars.update(id=original.id)
1089 r.request.vars.update(id=original.id)
1090 return self.__get(r)
1091
1092
1093
1095 """
1096 Get the DELETE method handler
1097
1098 @param r: the S3Request
1099
1100 """
1101
1102 return self.get_handler("delete")
1103
1104
1105
1107 """
1108 Export this resource as XML
1109
1110 @param r: the request
1111 @param attr: request attributes
1112
1113 """
1114
1115 json_formats = self.manager.json_formats
1116 content_type = self.manager.content_type
1117
1118
1119 stylesheet = self.stylesheet(r, method="export")
1120
1121
1122 start = r.request.vars.get("start", None)
1123 if start is not None:
1124 try:
1125 start = int(start)
1126 except ValueError:
1127 start = None
1128 limit = r.request.vars.get("limit", None)
1129 if limit is not None:
1130 try:
1131 limit = int(limit)
1132 except ValueError:
1133 limit = None
1134
1135
1136 marker = r.request.vars.get("marker", None)
1137
1138
1139 msince = r.request.vars.get("msince", None)
1140 if msince is not None:
1141 tfmt = "%Y-%m-%dT%H:%M:%SZ"
1142 try:
1143 (y,m,d,hh,mm,ss,t0,t1,t2) = time.strptime(msince, tfmt)
1144 msince = datetime.datetime(y,m,d,hh,mm,ss)
1145 except ValueError:
1146 msince = None
1147
1148
1149 args = Storage()
1150 if stylesheet is not None:
1151 if r.component:
1152 args.update(id=r.id, component=r.component.tablename)
1153 mode = r.request.vars.get("xsltmode", None)
1154 if mode is not None:
1155 args.update(mode=mode)
1156
1157
1158 if r.representation in json_formats:
1159 as_json = True
1160 r.response.headers["Content-Type"] = \
1161 content_type.get(r.representation, "application/json")
1162 elif r.representation == "rss":
1163 as_json = False
1164 r.response.headers["Content-Type"] = \
1165 content_type.get(r.representation, "application/rss+xml")
1166 else:
1167 as_json = False
1168 r.response.headers["Content-Type"] = \
1169 content_type.get(r.representation, "text/xml")
1170
1171
1172 exporter = self.exporter.xml
1173 output = exporter(self,
1174 stylesheet=stylesheet,
1175 as_json=as_json,
1176 start=start,
1177 limit=limit,
1178 marker=marker,
1179 msince=msince,
1180 show_urls=True,
1181 dereference=True, **args)
1182
1183
1184 if not output:
1185 r.error(400, "XSLT Transformation Error: %s " % self.xml.error)
1186
1187 return output
1188
1189
1191 """
1192 Method handler to get field options in the current resource
1193
1194 @param r: the request
1195 @param attr: request attributes
1196
1197 """
1198
1199 if "field" in r.request.get_vars:
1200 items = r.request.get_vars["field"]
1201 if not isinstance(items, (list, tuple)):
1202 items = [items]
1203 fields = []
1204 for item in items:
1205 f = item.split(",")
1206 if f:
1207 fields.extend(f)
1208 else:
1209 fields = None
1210
1211 if r.representation == "xml":
1212 return self.options(component=r.component_name,
1213 fields=fields)
1214 elif r.representation == "s3json":
1215 return self.options(component=r.component_name,
1216 fields=fields,
1217 as_json=True)
1218 else:
1219 r.error(501, self.ERROR.BAD_FORMAT)
1220
1221
1222
1224 """
1225 Method handler to get all fields in the primary table
1226
1227 @param r: the request
1228 @param attr: the request attributes
1229
1230 """
1231
1232 if r.representation == "xml":
1233 return self.fields(component=r.component_name)
1234 elif r.representation == "s3json":
1235 return self.fields(component=r.component_name, as_json=True)
1236 else:
1237 r.error(501, self.ERROR.BAD_FORMAT)
1238
1239
1240
1241 - def __read_body(self, r):
1242 """
1243 Read data from request body
1244
1245 @param r: the S3Request
1246
1247 """
1248
1249 self.files = Storage()
1250 content_type = r.request.env.get("content_type", None)
1251
1252 source = []
1253 if content_type and content_type.startswith("multipart/"):
1254 ext = ".%s" % r.representation
1255 vars = r.request.post_vars
1256 for v in vars:
1257 p = vars[v]
1258 if isinstance(p, cgi.FieldStorage) and p.filename:
1259 self.files[p.filename] = p.file
1260 if p.filename.endswith(ext):
1261 source.append(tuple(v, p.file))
1262 elif v.endswith(ext):
1263 if isinstance(p, cgi.FieldStorage):
1264 source.append(tuple(v, p.value))
1265 elif isinstance(p, basestring):
1266 source.append(tuple(v, StringIO.StringIO(p)))
1267 else:
1268 s = r.request.body
1269 s.seek(0)
1270 source.append(s)
1271
1272 return source
1273
1274
1275
1277 """
1278 Import data using the XML Importer (transformable XML/JSON/CSV formats)
1279
1280 @param r: the S3Request
1281 @param attr: the request attributes
1282
1283 """
1284
1285 xml = self.xml
1286 vars = Storage(r.request.vars)
1287 auth = self.manager.auth
1288
1289
1290 def findnames(vars, name):
1291 nlist = []
1292 if name in vars:
1293 names = vars[name]
1294 if isinstance(names, (list, tuple)):
1295 names = ",".join(names)
1296 names = names.split(",")
1297 for n in names:
1298 if n[0] == "(" and ")" in n[1:]:
1299 nlist.append(n[1:].split(")", 1))
1300 else:
1301 nlist.append([None, n])
1302 return nlist
1303
1304 filenames = findnames(vars, "filename")
1305 fetchurls = findnames(vars, "fetchurl")
1306
1307
1308 json_formats = self.manager.json_formats
1309 csv_formats = self.manager.csv_formats
1310
1311
1312 source = []
1313 format = r.representation
1314 if format in json_formats or \
1315 format in csv_formats:
1316 if filenames:
1317 try:
1318 for f in filenames:
1319 source.append((f[0], open(f[1], "rb")))
1320 except:
1321 source = []
1322 elif fetchurls:
1323 import urllib
1324 try:
1325 for u in fetchurls:
1326 source.append((u[0], urllib.urlopen(u[1])))
1327 except:
1328 source = []
1329 elif r.http != "GET":
1330 source = self.__read_body(r)
1331 else:
1332 if filenames:
1333 source = filenames
1334 elif fetchurls:
1335 source = fetchurls
1336 elif r.http != "GET":
1337 source = self.__read_body(r)
1338
1339 if not source:
1340 if filenames or fetchurls:
1341
1342 r.error(400, "Invalid source")
1343 else:
1344
1345 opts = vars.get("options", False)
1346 refs = vars.get("references", False)
1347 stylesheet = self.stylesheet(r)
1348 if format in json_formats:
1349 as_json = True
1350 else:
1351 as_json = False
1352 output = self.struct(options=opts,
1353 references=refs,
1354 stylesheet=stylesheet,
1355 as_json=as_json)
1356 if output is None:
1357
1358 r.error(400, self.xml.error)
1359 return output
1360
1361
1362 stylesheet = self.stylesheet(r, method="import")
1363
1364
1365 if r.method == "create":
1366 id = None
1367 else:
1368 id = r.id
1369
1370
1371 if "ignore_errors" in r.request.vars:
1372 ignore_errors = True
1373 else:
1374 ignore_errors = False
1375
1376
1377 if "xsltmode" in r.request.vars:
1378 args = dict(mode=request.vars["xsltmode"])
1379 else:
1380 args = dict()
1381
1382
1383 if format in json_formats:
1384 format = "json"
1385 elif format in csv_formats:
1386 format = "csv"
1387 else:
1388 format = "xml"
1389
1390
1391 importer = S3Importer(self.manager)
1392 try:
1393 return importer.xml(self, source,
1394 id=id,
1395 format=format,
1396 stylesheet=stylesheet,
1397 ignore_errors=ignore_errors,
1398 **args)
1399 except IOError:
1400 auth.permission.fail()
1401 except SyntaxError:
1402 e = sys.exc_info()[1]
1403 r.error(400, e)
1404
1405
1406
1407
1408 - def export_xml(self,
1409 stylesheet=None,
1410 as_json=False,
1411 pretty_print=False, **args):
1412 """
1413 Export this resource as XML
1414
1415 @param stylesheet: path to the XSLT stylesheet (if not native S3-XML)
1416 @param as_json: convert the output into JSON
1417 @param pretty_print: insert newlines/indentation in the output
1418 @param args: arguments to pass to the XSLT stylesheet
1419 @returns: the XML as string
1420
1421 """
1422
1423 exporter = self.exporter.xml
1424
1425 return exporter(self,
1426 stylesheet=stylesheet,
1427 as_json=as_json,
1428 pretty_print=pretty_print, **args)
1429
1430
1431
1432 - def import_xml(self, source,
1433 files=None,
1434 id=None,
1435 stylesheet=None,
1436 as_json=False,
1437 ignore_errors=False, **args):
1438 """
1439 Import data from an XML source into this resource
1440
1441 @param source: the XML source (or ElementTree)
1442 @param files: file attachments as {filename:file}
1443 @param id: the ID or list of IDs of records to update (None for all)
1444 @param stylesheet: the XSLT stylesheet
1445 @param as_json: the source is JSONified XML
1446 @param ignore_errors: do not stop on errors (skip invalid elements)
1447 @param args: arguments to pass to the XSLT stylesheet
1448 @returns: a JSON message as string
1449
1450 @raise SyntaxError: in case of a parser or transformation error
1451 @raise IOError: at insufficient permissions
1452
1453 @todo 2.3: deprecate?
1454
1455 """
1456
1457
1458 importer = self.importer.xml
1459
1460 return importer(self, source,
1461 files=files,
1462 id=id,
1463 stylesheet=stylesheet,
1464 as_json=as_json,
1465 ignore_errors=ignore_errors, **args)
1466
1467
1468
1469 - def push(self, url,
1470 stylesheet=None,
1471 as_json=False,
1472 xsltmode=None,
1473 start=None,
1474 limit=None,
1475 marker=None,
1476 msince=None,
1477 show_urls=True,
1478 dereference=True,
1479 content_type=None,
1480 username=None,
1481 password=None,
1482 proxy=None):
1483 """
1484 Push (=POST) the current resource to a target URL
1485
1486 @param url: the URL to push to
1487 @param stylesheet: path to the XSLT stylesheet to be used by the exporter
1488 @param as_json: convert the output into JSON before push
1489 @param xsltmode: "mode" parameter for the XSLT stylesheet
1490 @param start: index of the first record to export (slicing)
1491 @param limit: maximum number of records to export (slicing)
1492 @param marker: default map marker URL
1493 @param msince: export only records which have been modified after
1494 this datetime
1495 @param show_urls: show URLs in the <resource> elements
1496 @param dereference: include referenced resources in the export
1497 @param content_type: content type specification for the export
1498 @param username: username to authenticate at the peer site
1499 @param password: password to authenticate at the peer site
1500 @param proxy: URL of the proxy server to use
1501
1502 @todo 2.3: error handling?
1503
1504 """
1505
1506 args = Storage()
1507 if stylesheet and xsltmode:
1508 args.update(mode=xsltmode)
1509
1510
1511 exporter = self.exporter.xml
1512 data = exporter(start=start,
1513 limit=limit,
1514 marker=marker,
1515 msince=msince,
1516 show_urls=show_urls,
1517 dereference=dereference,
1518 stylesheet=stylesheet,
1519 as_json=as_json,
1520 pretty_print=False,
1521 **args)
1522
1523 if data:
1524
1525 url_split = url.split("://", 1)
1526 if len(url_split) == 2:
1527 protocol, path = url_split
1528 else:
1529 protocol, path = "http", None
1530
1531
1532 import urllib2
1533 req = urllib2.Request(url=url, data=data)
1534 if content_type:
1535 req.add_header('Content-Type', content_type)
1536
1537 handlers = []
1538
1539
1540 if proxy:
1541 proxy_handler = urllib2.ProxyHandler({protocol:proxy})
1542 handlers.append(proxy_handler)
1543
1544
1545 if username and password:
1546
1547 import base64
1548 base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
1549 req.add_header("Authorization", "Basic %s" % base64string)
1550
1551 passwd_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
1552 passwd_manager.add_password(realm=None,
1553 uri=url,
1554 user=username,
1555 passwd=password)
1556 auth_handler = urllib2.HTTPBasicAuthHandler(passwd_manager)
1557 handlers.append(auth_handler)
1558
1559
1560 if handlers:
1561 opener = urllib2.build_opener(*handlers)
1562 urllib2.install_opener(opener)
1563
1564
1565 try:
1566 f = urllib2.urlopen(req)
1567 except urllib2.HTTPError, e:
1568
1569 code = e.code
1570 message = e.read()
1571 try:
1572
1573
1574 message_json = json.loads(message)
1575 message = message_json.get("message", message)
1576 except:
1577 pass
1578
1579 return xml.json_message(False, code, message)
1580 else:
1581
1582 response = f.read()
1583 return response
1584 else:
1585
1586 return None
1587
1588
1589
1590 - def fetch(self, url,
1591 username=None,
1592 password=None,
1593 proxy=None,
1594 as_json=False,
1595 stylesheet=None,
1596 ignore_errors=False, **args):
1597 """
1598 Fetch XML data from a remote URL into the current resource
1599
1600 @param url: the peer URL
1601 @param username: username to authenticate at the peer site
1602 @param password: password to authenticate at the peer site
1603 @param proxy: URL of the proxy server to use
1604 @param stylesheet: path to the XSLT stylesheet to transform the data
1605 @param as_json: source is JSONified XML
1606 @param ignore_errors: skip invalid records
1607
1608 """
1609
1610 xml = self.xml
1611
1612 response = None
1613 url_split = url.split("://", 1)
1614 if len(url_split) == 2:
1615 protocol, path = url_split
1616 else:
1617 protocol, path = "http", None
1618
1619
1620 import urllib2
1621 req = urllib2.Request(url=url)
1622 handlers = []
1623
1624
1625 if proxy:
1626 proxy_handler = urllib2.ProxyHandler({protocol:proxy})
1627 handlers.append(proxy_handler)
1628
1629
1630 if username and password:
1631
1632 import base64
1633 base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
1634 req.add_header("Authorization", "Basic %s" % base64string)
1635
1636 passwd_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
1637 passwd_manager.add_password(realm=None,
1638 uri=url,
1639 user=username,
1640 passwd=password)
1641 auth_handler = urllib2.HTTPBasicAuthHandler(passwd_manager)
1642 handlers.append(auth_handler)
1643
1644
1645 if handlers:
1646 opener = urllib2.build_opener(*handlers)
1647 urllib2.install_opener(opener)
1648
1649
1650 try:
1651 f = urllib2.urlopen(req)
1652 except urllib2.HTTPError, e:
1653
1654 code = e.code
1655 message = e.read()
1656 try:
1657
1658
1659 message_json = json.loads(message)
1660 message = message_json.get("message", message)
1661 except:
1662 pass
1663
1664
1665
1666 message = "<message>PEER ERROR: %s</message>" % message
1667 try:
1668 markup = etree.XML(message)
1669 message = markup.xpath(".//text()")
1670 if message:
1671 message = " ".join(message)
1672 else:
1673 message = ""
1674 except etree.XMLSyntaxError:
1675 pass
1676
1677
1678 return xml.json_message(False, code, message, tree=None)
1679 else:
1680
1681 response = f
1682
1683
1684 try:
1685 success = self.import_xml(response,
1686 stylesheet=stylesheet,
1687 as_json=as_json,
1688 ignore_errors=ignore_errors,
1689 args=args)
1690 except IOError, e:
1691 return xml.json_message(False, 400, "LOCAL ERROR: %s" % e)
1692 if not success:
1693 error = self.manager.error
1694 return xml.json_message(False, 400, "LOCAL ERROR: %s" % error)
1695 else:
1696 return xml.json_message()
1697
1698
1699
1700 - def options(self, component=None, fields=None, as_json=False):
1701 """
1702 Export field options of this resource as element tree
1703
1704 @param component: name of the component which the options are
1705 requested of, None for the primary table
1706 @param fields: list of names of fields for which the options
1707 are requested, None for all fields (which have options)
1708 @param as_json: convert the output into JSON
1709
1710 """
1711
1712 if component is not None:
1713 c = self.components.get(component, None)
1714 if c:
1715 tree = c.resource.options(fields=fields, as_json=as_json)
1716 return tree
1717 else:
1718 raise AttributeError
1719 else:
1720 tree = self.xml.get_options(self.prefix,
1721 self.name,
1722 fields=fields)
1723 tree = etree.ElementTree(tree)
1724
1725 if as_json:
1726 return self.xml.tree2json(tree, pretty_print=True)
1727 else:
1728 return self.xml.tostring(tree, pretty_print=True)
1729
1730
1731
1732 - def fields(self, component=None, as_json=False):
1733 """
1734 Export a list of fields in the resource as element tree
1735
1736 @param component: name of the component to lookup the fields
1737 (None for primary table)
1738 @param as_json: convert the output XML into JSON
1739
1740 """
1741
1742 if component is not None:
1743 c = self.components.get(component, None)
1744 if c:
1745 tree = c.resource.fields()
1746 return tree
1747 else:
1748 raise AttributeError
1749 else:
1750 tree = self.xml.get_fields(self.prefix, self.name)
1751
1752 if as_json:
1753 return self.xml.tree2json(tree, pretty_print=True)
1754 else:
1755 return self.xml.tostring(tree, pretty_print=True)
1756
1757
1758
1759 - def struct(self,
1760 options=False,
1761 references=False,
1762 stylesheet=None,
1763 as_json=False,
1764 as_tree=False):
1765 """
1766 Get the structure of the resource
1767
1768 @param options: include option lists in option fields
1769 @param references: include option lists even for reference fields
1770 @param stylesheet: the stylesheet to use for transformation
1771 @param as_json: convert into JSON after transformation
1772
1773 """
1774
1775
1776 root = etree.Element(self.xml.TAG.root)
1777 main = self.xml.get_struct(self.prefix, self.name,
1778 parent=root,
1779 options=options,
1780 references=references)
1781
1782
1783 for c in self.components.values():
1784 component = c.resource
1785 prefix = component.prefix
1786 name = component.name
1787 sub = self.xml.get_struct(prefix, name,
1788 parent=main,
1789 options=options,
1790 references=references)
1791
1792
1793 tree = etree.ElementTree(root)
1794 if stylesheet is not None:
1795 tfmt = self.xml.ISOFORMAT
1796 args = dict(domain=self.manager.domain,
1797 base_url=self.manager.s3.base_url,
1798 prefix=self.prefix,
1799 name=self.name,
1800 utcnow=datetime.datetime.utcnow().strftime(tfmt))
1801
1802 tree = self.xml.transform(tree, stylesheet, **args)
1803 if tree is None:
1804 return None
1805
1806
1807 if as_tree:
1808 return tree
1809
1810
1811 if as_json:
1812 return self.xml.tree2json(tree, pretty_print=True)
1813 else:
1814 return self.xml.tostring(tree, pretty_print=True)
1815
1816
1817
1818
1820 """
1821 Get a list of all readable fields in the resource table
1822
1823 @param subset: list of fieldnames to limit the selection to
1824
1825 """
1826
1827 fkey = None
1828 table = self.table
1829
1830 if self.parent:
1831 component = self.parent.components.get(self.name, None)
1832 if component:
1833 fkey = component.fkey
1834
1835 if subset:
1836 return [table[f] for f in subset
1837 if f in table.fields and table[f].readable and f != fkey]
1838 else:
1839 return [table[f] for f in table.fields
1840 if table[f].readable and f != fkey]
1841
1842
1843
1844 - def limitby(self, start=None, limit=None):
1845 """
1846 Convert start+limit parameters into a limitby tuple
1847 - limit without start => start = 0
1848 - start without limit => limit = ROWSPERPAGE
1849 - limit 0 (or less) => limit = 1
1850 - start less than 0 => start = 0
1851
1852 @param start: index of the first record to select
1853 @param limit: maximum number of records to select
1854
1855 """
1856
1857 if start is None:
1858 if not limit:
1859 return None
1860 else:
1861 start = 0
1862
1863 if not limit:
1864 limit = self.manager.ROWSPERPAGE
1865 if limit is None:
1866 return None
1867
1868 if limit <= 0:
1869 limit = 1
1870 if start < 0:
1871 start = 0
1872
1873 return (start, start + limit)
1874
1875
1876
1877 - def url(self,
1878 id=None,
1879 uid=None,
1880 prefix=None,
1881 format="html",
1882 method=None,
1883 vars=None):
1884 """
1885 URL of this resource
1886
1887 @param id: record ID or list of record IDs
1888 @param uid: record UID or list of record UIDs (ignored if id is specified)
1889 @param prefix: override current controller prefix
1890 @param format: representation format
1891 @param method: URL method
1892 @param vars: override current URL query
1893
1894 """
1895
1896 r = self.manager.request
1897 v = r.get_vars
1898 p = prefix or r.controller
1899 n = self.name
1900 x = n
1901 c = None
1902 args = []
1903 vars = vars or v or Storage()
1904
1905 if self.parent:
1906 n = self.parent.name
1907 c = self.name
1908 if c:
1909 x = c
1910 args.append(c)
1911 if id is not None and not id:
1912 if not self._multiple:
1913 ids = self.get_id()
1914 if not isinstance(ids, (list, tuple)):
1915 args.append(str(ids))
1916 elif self.parent:
1917 ids = self.parent.get_id()
1918 if not isinstance(ids, (list, tuple)):
1919 args.insert(0, str(ids))
1920 elif id:
1921 if not isinstance(id, (list, tuple)):
1922 id = [id]
1923 if len(id) > 1:
1924 vars["%s.id" % x] = ",".join(map(str, id))
1925 else:
1926 args.append(str(id[0]))
1927 elif uid:
1928 if isinstance(uid, (list, tuple)):
1929 uids = ",".join(map(str, uid))
1930 else:
1931 uids = str(uid)
1932 vars["%s.uid" % x] = uids
1933 if method:
1934 args.append(method)
1935 if format != "html":
1936 if args:
1937 args[-1] = "%s.%s" % (args[-1], format)
1938 else:
1939 n = "%s.%s" % (n, format)
1940 url = URL(r=r, c=p, f=n, args=args, vars=vars, extension="")
1941 return url
1942
1943
1944
1961
1962
1963
1964 - def stylesheet(self, r, method=None, skip_error=False):
1965 """
1966 Find the XSLT stylesheet for a request
1967
1968 @param r: the S3Request
1969 @param method: "import" for data imports, else None
1970
1971 """
1972
1973 stylesheet = None
1974 format = r.representation
1975 request = r.request
1976 if r.component:
1977 resourcename = r.component.name
1978 else:
1979 resourcename = r.name
1980
1981
1982 if format == "xml":
1983 return stylesheet
1984
1985
1986 if "transform" in request.vars:
1987 return request.vars["transform"]
1988
1989
1990 extension = self.XSLT_EXTENSION
1991 filename = "%s.%s" % (resourcename, extension)
1992 if filename in request.post_vars:
1993 p = request.post_vars[filename]
1994 if isinstance(p, cgi.FieldStorage) and p.filename:
1995 stylesheet = p.file
1996 return stylesheet
1997
1998
1999 folder = request.folder
2000 path = self.XSLT_PATH
2001 if method != "import":
2002 method = "export"
2003 filename = "%s.%s" % (method, extension)
2004 stylesheet = os.path.join(folder, path, format, filename)
2005 if not os.path.exists(stylesheet):
2006 if not skip_error:
2007 r.error(501, "%s: %s" % (self.ERROR.BAD_TEMPLATE, stylesheet))
2008 else:
2009 stylesheet = None
2010
2011 return stylesheet
2012
2016 """
2017 Class to handle HTTP requests
2018
2019 @todo: integrate into S3Resource
2020
2021 """
2022
2023 UNAUTHORISED = "Not Authorised"
2024
2025 DEFAULT_REPRESENTATION = "html"
2026 INTERACTIVE_FORMATS = ("html", "popup", "iframe")
2027
2028
2029 - def __init__(self, manager, prefix, name):
2030 """
2031 Constructor
2032
2033 @param manager: the S3ResourceController
2034 @param prefix: prefix of the resource name (=module name)
2035 @param name: name of the resource (=without prefix)
2036
2037 """
2038
2039 self.manager = manager
2040
2041
2042 self.session = manager.session or Storage()
2043 self.request = manager.request
2044 self.response = manager.response
2045
2046
2047 self.prefix = prefix or self.request.controller
2048 self.name = name or self.request.function
2049
2050
2051 self.http = self.request.env.request_method
2052 self.__parse()
2053
2054 self.custom_action = None
2055
2056
2057 self.interactive = self.representation in self.INTERACTIVE_FORMATS
2058
2059
2060 vars = Storage(self.request.get_vars)
2061 if self.component_name and self.component_id:
2062 varname = "%s.id" % self.component_name
2063 if varname in vars:
2064 var = vars[varname]
2065 if not isinstance(var, (list, tuple)):
2066 var = [var]
2067 var.append(self.component_id)
2068 vars[varname] = var
2069 else:
2070 vars[varname] = self.component_id
2071
2072
2073 self.resource = manager.define_resource(self.prefix, self.name,
2074 id=self.id,
2075 filter=self.response[manager.HOOKS].filter,
2076 vars=vars,
2077 components=self.component_name)
2078
2079 self.tablename = self.resource.tablename
2080 self.table = self.resource.table
2081
2082
2083 self.component = None
2084 self.pkey = None
2085 self.fkey = None
2086 self.multiple = True
2087 if self.component_name:
2088 c = self.resource.components.get(self.component_name, None)
2089 if c:
2090 self.component = c.component
2091 self.pkey, self.fkey = c.pkey, c.fkey
2092 self.multiple = self.component.multiple
2093 else:
2094 manager.error = "%s not a component of %s" % (
2095 self.component_name,
2096 self.resource.tablename)
2097 raise SyntaxError(manager.error)
2098
2099
2100 uid = self.request.vars.get("%s.uid" % self.name, None)
2101 if self.component_name:
2102 cuid = self.request.vars.get("%s.uid" % self.component_name, None)
2103 else:
2104 cuid = None
2105
2106
2107 self.record = None
2108 if self.id or self.component_id or \
2109 uid and not isinstance(uid, (list, tuple)) or \
2110 cuid and not isinstance(cuid, (list, tuple)):
2111
2112 self.resource.load()
2113 if len(self.resource) == 1:
2114 self.record = self.resource.records().first()
2115 self.id = self.record.id
2116 self.manager.store_session(self.resource.prefix,
2117 self.resource.name,
2118 self.id)
2119 else:
2120 manager.error = self.manager.ERROR.BAD_RECORD
2121 if self.representation == "html":
2122 self.session.error = manager.error
2123 self.component = None
2124 redirect(self.there())
2125 else:
2126 raise KeyError(manager.error)
2127
2128
2129
2131 """
2132 Action upon unauthorised request
2133
2134 """
2135
2136 auth = self.manager.auth
2137 auth.permission.fail()
2138
2139
2140
2141 - def error(self, status, message, tree=None, next=None):
2142 """
2143 Action upon error
2144
2145 @param status: HTTP status code
2146 @param message: the error message
2147 @param tree: the tree causing the error
2148
2149 """
2150
2151 xml = self.manager.xml
2152
2153 if self.representation == "html":
2154 self.session.error = message
2155 if next is not None:
2156 redirect(next)
2157 else:
2158 redirect(URL(r=self.request, f="index"))
2159 else:
2160 raise HTTP(status,
2161 body=xml.json_message(success=False,
2162 status_code=status,
2163 message=message,
2164 tree=tree))
2165
2166
2167
2168
2170 """
2171 Parses a web2py request for the REST interface
2172
2173 """
2174
2175 request = self.request
2176
2177 self.id = None
2178 self.component_name = None
2179 self.component_id = None
2180 self.method = None
2181
2182 representation = request.extension
2183
2184
2185 model = self.manager.model
2186 components = [c[0].name for c in
2187 model.get_components(self.prefix, self.name)]
2188
2189
2190
2191 f = []
2192 args = request["args"]
2193 if len(args) > 4:
2194 args = args[:4]
2195 method = self.name
2196 for arg in args:
2197 if "." in arg:
2198 arg, representation = arg.rsplit(".", 1)
2199 if method is None:
2200 method = arg
2201 elif arg.isdigit():
2202 f.append((method, arg))
2203 method = None
2204 else:
2205 f.append((method, None))
2206 method = arg
2207 if method:
2208 f.append((method, None))
2209
2210 self.id = f[0][1]
2211
2212
2213 l = len(f)
2214 if l > 1:
2215 m = f[1][0].lower()
2216 i = f[1][1]
2217 if m in components:
2218 self.component_name = m
2219 self.component_id = i
2220 else:
2221 self.method = m
2222 if not self.id:
2223 self.id = i
2224 if self.component_name and l > 2:
2225 self.method = f[2][0].lower()
2226 if not self.component_id:
2227 self.component_id = f[2][1]
2228
2229
2230 if "format" in request.vars:
2231 ext = request.vars["format"]
2232 if isinstance(ext, list):
2233 ext = ext[-1]
2234 representation = ext or representation
2235 self.representation = representation.lower()
2236
2237 if not self.representation:
2238 self.representation = self.DEFAULT_REPRESENTATION
2239
2240
2241
2242
2243 - def __next(self, id=None, method=None, representation=None, vars=None):
2244 """
2245 Returns a URL of the current request
2246
2247 @param id: the record ID for the URL
2248 @param method: an explicit method for the URL
2249 @param representation: the representation for the URL
2250 @param vars: the URL query variables
2251
2252 """
2253
2254 if vars is None:
2255 vars = self.request.get_vars
2256 if "format" in vars:
2257 del vars["format"]
2258
2259 args = []
2260 read = False
2261
2262 component_id = self.component_id
2263 if id is None:
2264 id = self.id
2265 else:
2266 read = True
2267
2268 if not representation:
2269 representation = self.representation
2270 if method is None:
2271 method = self.method
2272 elif method=="":
2273 method = None
2274 if not read:
2275 if self.component:
2276 component_id = None
2277 else:
2278 id = None
2279 else:
2280 if id is None:
2281 id = self.id
2282 else:
2283 id = str(id)
2284 if len(id) == 0:
2285 id = "[id]"
2286
2287 if self.component:
2288 if id:
2289 args.append(id)
2290 args.append(self.component_name)
2291 if component_id:
2292 args.append(component_id)
2293 if method:
2294 args.append(method)
2295 else:
2296 if id:
2297 args.append(id)
2298 if method:
2299 args.append(method)
2300
2301 f = self.request.function
2302 if not representation==self.DEFAULT_REPRESENTATION:
2303 if len(args) > 0:
2304 args[-1] = "%s.%s" % (args[-1], representation)
2305 else:
2306 f = "%s.%s" % (f, representation)
2307
2308 return URL(r=self.request,
2309 c=self.request.controller,
2310 f=f,
2311 args=args, vars=vars)
2312
2313
2314
2315 - def here(self, representation=None, vars=None):
2316 """
2317 URL of the current request
2318
2319 @param representation: the representation for the URL
2320 @param vars: the URL query variables
2321
2322 """
2323
2324 return self.__next(id=self.id, representation=representation, vars=vars)
2325
2326
2327
2328 - def other(self, method=None, record_id=None, representation=None, vars=None):
2329 """
2330 URL of a request with different method and/or record_id
2331 of the same resource
2332
2333 @param method: an explicit method for the URL
2334 @param record_id: the record ID for the URL
2335 @param representation: the representation for the URL
2336 @param vars: the URL query variables
2337
2338 """
2339
2340 return self.__next(method=method, id=record_id,
2341 representation=representation, vars=vars)
2342
2343
2344
2345 - def there(self, representation=None, vars=None):
2346 """
2347 URL of a HTTP/list request on the same resource
2348
2349 @param representation: the representation for the URL
2350 @param vars: the URL query variables
2351
2352 """
2353
2354 return self.__next(method="", representation=representation, vars=vars)
2355
2356
2357
2358 - def same(self, representation=None, vars=None):
2359 """
2360 URL of the same request with neutralized primary record ID
2361
2362 @param representation: the representation for the URL
2363 @param vars: the URL query variables
2364
2365 """
2366
2367 return self.__next(id="[id]", representation=representation, vars=vars)
2368
2369
2370
2371
2373 """
2374 Get the target table of the current request
2375
2376 @returns: a tuple of (prefix, name, table, tablename) of the target
2377 resource of this request
2378
2379 """
2380
2381 if self.component is not None:
2382 return (self.component.prefix,
2383 self.component.name,
2384 self.component.table,
2385 self.component.tablename)
2386 else:
2387 return (self.prefix,
2388 self.name,
2389 self.table,
2390 self.tablename)
2391
2395 """
2396 REST Method Handler Base Class
2397
2398 Method handler classes should inherit from this class and implement the
2399 apply_method() method.
2400
2401 """
2402
2403 - def __call__(self, r, method=None, **attr):
2404 """
2405 Entry point for the REST interface
2406
2407 @param r: the S3Request
2408 @param method: the method established by the REST interface
2409 @param attr: dict of parameters for the method handler
2410
2411 @returns: output object to send to the view
2412
2413 """
2414
2415
2416 self.manager = r.manager
2417 self.session = r.session
2418 self.request = r.request
2419 self.response = r.response
2420
2421 self.T = self.manager.T
2422 self.db = self.manager.db
2423
2424
2425 self.permit = self.manager.auth.s3_has_permission
2426 self.download_url = self.manager.s3.download_url
2427
2428
2429 self.next = None
2430
2431
2432 self.prefix, self.name, self.table, self.tablename = r.target()
2433
2434
2435 if method is not None:
2436 self.method = method
2437 else:
2438 self.method = r.method
2439 if r.component:
2440 self.record = r.component_id
2441 component = r.resource.components.get(r.component_name, None)
2442 self.resource = component.resource
2443 if not self.method:
2444 if r.multiple and not r.component_id:
2445 self.method = "list"
2446 else:
2447 self.method = "read"
2448 else:
2449 self.record = r.id
2450 self.resource = r.resource
2451 if not self.method:
2452 if r.id or r.method in ("read", "display"):
2453 self.method = "read"
2454 else:
2455 self.method = "list"
2456
2457
2458 output = self.apply_method(r, **attr)
2459
2460
2461 if self.next and self.resource.lastid:
2462 self.next = str(self.next)
2463 placeholder = "%5Bid%5D"
2464 self.next = self.next.replace(placeholder, self.resource.lastid)
2465 placeholder = "[id]"
2466 self.next = self.next.replace(placeholder, self.resource.lastid)
2467 r.next = self.next
2468
2469
2470 self._extend_view(output, r, **attr)
2471
2472 return output
2473
2474
2475
2477 """
2478 Stub for apply_method, to be implemented in subclass
2479
2480 @param r: the S3Request
2481 @param attr: dictionary of parameters for the method handler
2482
2483 @returns: output object to send to the view
2484
2485 """
2486
2487 output = dict()
2488 return output
2489
2490
2491
2492
2493 @staticmethod
2495 """
2496 Get the ID of the target record of a S3Request
2497
2498 @param r: the S3Request
2499
2500 """
2501
2502 if r.component:
2503 if r.multiple and not r.component_id:
2504 return None
2505 resource = r.resource.components.get(r.component_name).resource
2506 resource.load(start=0, limit=1)
2507 if len(resource):
2508 return resource.records().first().id
2509 else:
2510 return r.id
2511
2512 return None
2513
2514
2515
2516 - def _config(self, key, default=None):
2517 """
2518 Get a configuration setting of the current table
2519
2520 @param key: the setting key
2521 @param default: the default value
2522
2523 """
2524
2525 return self.manager.model.get_config(self.table, key, default)
2526
2527
2528
2529 @staticmethod
2530 - def _view(r, default, format=None):
2531 """
2532 Get the path to the view stylesheet file
2533
2534 @param r: the S3Request
2535 @param default: name of the default view stylesheet file
2536 @param format: format string (optional)
2537
2538 """
2539
2540 request = r.request
2541 folder = request.folder
2542 prefix = request.controller
2543
2544 if r.component:
2545 view = "%s_%s_%s" % (r.name, r.component_name, default)
2546 path = os.path.join(folder, "views", prefix, view)
2547 if os.path.exists(path):
2548 return "%s/%s" % (prefix, view)
2549 else:
2550 view = "%s_%s" % (r.name, default)
2551 path = os.path.join(folder, "views", prefix, view)
2552 else:
2553 if format:
2554 view = "%s_%s_%s" % (r.name, default, format)
2555 else:
2556 view = "%s_%s" % (r.name, default)
2557 path = os.path.join(folder, "views", prefix, view)
2558
2559 if os.path.exists(path):
2560 return "%s/%s" % (prefix, view)
2561 else:
2562 if format:
2563 return default.replace(".html", "_%s.html" % format)
2564 else:
2565 return default
2566
2567
2568
2569 @staticmethod
2571 """
2572 Add additional view variables (invokes all callables)
2573
2574 @param output: the output dict
2575 @param r: the S3Request
2576 @param attr: the view variables (e.g. 'rheader')
2577
2578 @note: overload this method in subclasses if you don't want
2579 additional view variables to be added automatically
2580
2581 """
2582
2583 if r.interactive and isinstance(output, dict):
2584 for key in attr:
2585 handler = attr[key]
2586 if callable(handler):
2587 resolve = True
2588 try:
2589 display = handler(r)
2590 except TypeError:
2591
2592 display = handler
2593 continue
2594 except:
2595
2596 raise
2597 else:
2598 display = handler
2599 if isinstance(display, dict) and resolve:
2600 output.update(**display)
2601 elif display is not None:
2602 output.update(**{key:display})
2603 elif key in output:
2604 del output[key]
2605
2606
2607
2608