source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/hypermedia.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 15.5 KB
Line 
1import json
2from collections import OrderedDict
3from gluon import URL, IS_SLUG
4from functools import reduce
5
6# compliant with https://github.com/collection-json/spec
7# also compliant with http://code.ge/media-types/collection-next-json/
8
9"""
10
11Example controller:
12
13def api():
14    from gluon.contrib.hypermedia import Collection
15    policies = {
16        'thing': {
17            'GET':{'query':None,'fields':['id', 'name']},
18            'POST':{'query':None,'fields':['name']},
19            'PUT':{'query':None,'fields':['name']},
20            'DELETE':{'query':None},
21            },
22        'attr': {
23            'GET':{'query':None,'fields':['id', 'name', 'thing']},
24            'POST':{'query':None,'fields':['name', 'thing']},
25            'PUT':{'query':None,'fields':['name', 'thing']},
26            'DELETE':{'query':None},
27            },
28        }
29    return Collection(db).process(request, response, policies)
30"""
31
32__all__ = ['Collection']
33
34class Collection(object):
35
36    VERSION = '1.0'
37    MAXITEMS = 100
38
39    def __init__(self,db, extensions=True, compact=False):
40        self.db = db
41        self.extensions = extensions
42        self.compact = compact
43
44    def row2data(self,table,row,text=False):
45        """ converts a DAL Row object into a collection.item """
46        data = []
47        if self.compact:
48            for fieldname in (self.table_policy.get('fields',table.fields)):
49                field = table[fieldname]
50                if not ((field.type=='text' and text==False) or
51                        field.type=='blob' or
52                        field.type.startswith('reference ') or
53                        field.type.startswith('list:reference ')) and field.name in row:
54                    data.append(row[field.name])
55        else:
56            for fieldname in (self.table_policy.get('fields',table.fields)):
57                field = table[fieldname]
58                if not ((field.type=='text' and text==False) or
59                        field.type=='blob' or
60                        field.type.startswith('reference ') or
61                        field.type.startswith('list:reference ')) and field.name in row:
62                    data.append({'name':field.name,'value':row[field.name],
63                                 'prompt':field.label, 'type':field.type})
64        return data
65
66    def row2links(self,table,row):
67        """ converts a DAL Row object into a set of links referencing the row """
68        links = []
69        for field in table._referenced_by:
70            if field._tablename in self.policies:
71                if row:
72                    href = URL(args=field._tablename,vars={field.name:row.id},scheme=True)
73                else:
74                    href = URL(args=field._tablename,scheme=True)+'?%s={id}' % field.name
75                links.append({'rel':'current','href':href,'prompt':str(field),
76                              'type':'children'})
77        if row:
78            fields = self.table_policy.get('fields', table.fields)
79            for fieldname in fields:
80                field = table[fieldname]
81                if field.type.startswith('reference '):
82                    href = URL(args=field.type[10:],vars={'id':row[fieldname]},
83                               scheme=True)
84                    links.append({'rel':'current','href':href,'prompt':str(field),
85                                  'type':'parent'})
86
87            for fieldname in fields:
88                field = table[fieldname]
89                if field.type=='upload' and row[fieldname]:
90                    href = URL('download',args=row[fieldname],scheme=True)
91                    links.append({'rel':'current','href':href,'prompt':str(field),
92                                  'type':'attachment'})
93
94            # should this be supported?
95            for rel,build in (self.table_policy.get('links',{}).items()):
96                links.append({'rel':'current','href':build(row),'prompt':rel})
97        # not sure
98        return links
99
100    def table2template(self,table):
101        """ confeverts a table into its form template """
102        data = []
103        fields = self.table_policy.get('fields', table.fields)
104        for fieldname in fields:
105            field = table[fieldname]
106            info = {'name': field.name, 'value': '', 'prompt': field.label}
107            policies = self.policies[table._tablename]
108            # https://github.com/collection-json/extensions/blob/master/template-validation.md
109            info['type'] = str(field.type) # FIX THIS
110            if hasattr(field,'regexp_validator'):
111                info['regexp'] = field.regexp_validator
112            info['required'] = field.required
113            info['post_writable'] = field.name in policies['POST'].get('fields',fields)
114            info['put_writable'] = field.name in policies['PUT'].get('fields',fields)
115            info['options'] = {} # FIX THIS
116            data.append(info)
117        return {'data':data}
118
119    def request2query(self,table,vars):
120        """ parses a request and converts it into a query """
121        if len(self.request.args)>1:
122            vars.id = self.request.args[1]
123
124        fieldnames = table.fields
125        queries = [table]
126        limitby = [0,self.MAXITEMS+1]
127        orderby = 'id'
128        for key,value in vars.items():
129            if key=='_offset':
130                limitby[0] = int(value) # MAY FAIL
131            elif key == '_limit':
132                limitby[1] = int(value)+1 # MAY FAIL
133            elif key=='_orderby':
134                orderby = value
135            elif key in fieldnames:
136                queries.append(table[key] == value)
137            elif key.endswith('.eq') and key[:-3] in fieldnames: # for completeness (useless)
138                queries.append(table[key[:-3]] == value)
139            elif key.endswith('.lt') and key[:-3] in fieldnames:
140                queries.append(table[key[:-3]] < value)
141            elif key.endswith('.le') and key[:-3] in fieldnames:
142                queries.append(table[key[:-3]] <= value)
143            elif key.endswith('.gt') and key[:-3] in fieldnames:
144                queries.append(table[key[:-3]] > value)
145            elif key.endswith('.ge') and key[:-3] in fieldnames:
146                queries.append(table[key[:-3]] >= value)
147            elif key.endswith('.contains') and key[:-9] in fieldnames:
148                queries.append(table[key[:-9]].contains(value))
149            elif key.endswith('.startswith') and key[:-11] in fieldnames:
150                queries.append(table[key[:-11]].startswith(value))
151            elif key.endswith('.ne') and key[:-3] in fieldnames:
152                queries.append(table[key][:-3] != value)
153            else:
154                raise ValueError("Invalid Query")
155        filter_query = self.table_policy.get('query')
156        if filter_query:
157            queries.append(filter_query)
158        query = reduce(lambda a,b:a&b,queries[1:]) if len(queries)>1 else queries[0]
159        orderby = [table[f] if f[0]!='~' else ~table[f[1:]] for f in orderby.split(',')]
160        return (query, limitby, orderby)
161
162    def table2queries(self,table, href):
163        """ generates a set of collection.queries examples for the table """
164        data = []
165        for fieldname in (self.table_policy.get('fields', table.fields)):
166            data.append({'name':fieldname,'value':''})
167            if self.extensions:
168                data.append({'name':fieldname+'.ne','value':''}) # NEW !!!
169                data.append({'name':fieldname+'.lt','value':''})
170                data.append({'name':fieldname+'.le','value':''})
171                data.append({'name':fieldname+'.gt','value':''})
172                data.append({'name':fieldname+'.ge','value':''})
173                if table[fieldname].type in ['string','text']:
174                    data.append({'name':fieldname+'.contains','value':''})
175                    data.append({'name':fieldname+'.startswith','value':''})
176                data.append({'name':'_limitby','value':''})
177                data.append({'name':'_offset','value':''})
178                data.append({'name':'_orderby','value':''})
179        return [{'rel' : 'search', 'href' : href, 'prompt' : 'Search', 'data' : data}]
180
181    def process(self,request,response,policies=None):
182        """ the main method, processes a request, filters by policies and produces a JSON response """
183        self.request = request
184        self.response = response
185        self.policies = policies
186        db = self.db
187        tablename = request.args(0)
188        r = OrderedDict()
189        r['version'] = self.VERSION
190        tablenames = policies.keys() if policies else db.tables
191        # if there is no tables
192        if not tablename:
193            r['href'] = URL(scheme=True),
194            # https://github.com/collection-json/extensions/blob/master/model.md
195            r['links'] = [{'rel' : t, 'href' : URL(args=t,scheme=True), 'model':t}
196                          for t in tablenames]
197            response.headers['Content-Type'] = 'application/vnd.collection+json'
198            return response.json({'collection':r})
199        # or if the tablenames is invalid
200        if not tablename in tablenames:
201            return self.error(400,'BAD REQUEST','Invalid table name')
202        # of if the method is invalid
203        if not request.env.request_method in policies[tablename]:
204            return self.error(400,'BAD REQUEST','Method not recognized')
205        # get the policies
206        self.table_policy = policies[tablename][request.env.request_method]
207        # process GET
208        if request.env.request_method=='GET':
209            table = db[tablename]
210            r['href'] = URL(args=tablename)
211            r['items'] = items = []
212            try:
213                (query, limitby, orderby) = self.request2query(table,request.get_vars)
214                fields = [table[fn] for fn in (self.table_policy.get('fields', table.fields))]
215                fields = filter(lambda field: field.readable, fields)
216                rows = db(query).select(*fields,**dict(limitby=limitby, orderby=orderby))
217            except:
218                db.rollback()
219                return self.error(400,'BAD REQUEST','Invalid Query')
220            r['items_found'] = db(query).count()
221            delta = limitby[1]-limitby[0]-1
222            r['links'] = self.row2links(table,None) if self.compact else []
223            text = r['items_found']<2
224            for row in rows[:delta]:
225                id = row.id
226                for name in ('slug','fullname','title','name'):
227                    if name in row:
228                        href = URL(args=(tablename,id),scheme=True)
229                        break
230                else:
231                    href = URL(args=(tablename,id),scheme=True)
232                if self.compact:
233                    items.append(self.row2data(table,row,text))
234                else:
235                    items.append({
236                            'href':href,
237                            'data':self.row2data(table,row,text),
238                            'links':self.row2links(table,row)
239                            });
240            if self.extensions and len(rows)>delta:
241                vars = dict(request.get_vars)
242                vars['_offset'] = limitby[1]-1
243                vars['_limit'] = limitby[1]-1+delta
244                r['next'] = {'rel':'next',
245                             'href':URL(args=request.args,vars=vars,scheme=True)}
246            if self.extensions and limitby[0]>0:
247                vars = dict(request.get_vars)
248                vars['_offset'] = max(0,limitby[0]-delta)
249                vars['_limit'] = limitby[0]
250                r['previous'] = {'rel':'previous',
251                                 'href':URL(args=request.args,vars=vars,scheme=True)}
252            data = []
253            if not self.compact:
254                r['queries'] = self.table2queries(table, r['href'])
255            r['template'] = self.table2template(table)
256            response.headers['Content-Type'] = 'application/vnd.collection+json'
257            return response.json({'collection':r})
258        # process DELETE
259        elif request.env.request_method=='DELETE':
260            table = db[tablename]
261            if not request.get_vars:
262                return self.error(400, "BAD REQUEST", "Nothing to delete")
263            else:
264                try:
265                    (query, limitby, orderby) = self.request2query(table, request.vars)
266                    n = db(query).delete() # MAY FAIL
267                    response.status = 204
268                    return ''
269                except:
270                    db.rollback()
271                    return self.error(400,'BAD REQUEST','Invalid Query')
272            return response.json(r)
273        # process POST and PUT (on equal footing!)
274        elif request.env.request_method in ('POST','PUT'): # we treat them the same!
275            table = db[tablename]
276            if 'json' in request.env.content_type:
277                data = request.post_vars.data
278            else:
279                data = request.post_vars
280            if request.get_vars or len(request.args)>1: # update
281                # ADD validate fields and return error
282                try:
283                    (query, limitby, orderby) = self.request2query(table, request.get_vars)
284                    fields = filter(lambda fn_value:table[fn_value[0]].writable,data.items())
285                    res = db(query).validate_and_update(**dict(fields)) # MAY FAIL
286                    if res.errors:
287                        return self.error(400,'BAD REQUEST','Validation Error',res.errors)
288                    else:
289                        response.status = 200
290                        return ''
291                except:
292                    db.rollback()
293                    return self.error(400,'BAD REQUEST','Invalid Query')
294            else: # create
295                # ADD validate fields and return error
296                try:
297                    fields = filter(lambda fn_value1:table[fn_value1[0]].writable,data.items())
298                    res = table.validate_and_insert(**dict(fields)) # MAY FAIL
299                    if res.errors:
300                        return self.error(400,'BAD REQUEST','Validation Error',res.errors)
301                    else:
302                        response.status = 201
303                        response.headers['location'] = \
304                            URL(args=(tablename,res.id),scheme=True)
305                        return ''
306                except SyntaxError as e: #Exception,e:
307                    db.rollback()
308                    return self.error(400,'BAD REQUEST','Invalid Query:'+e)
309
310    def error(self,code="400", title="BAD REQUEST", message="UNKNOWN", form_errors={}):
311        request, response = self.request, self.response
312        r = OrderedDict({
313                "version" : self.VERSION,
314                "href" : URL(args=request.args,vars=request.vars),
315                "error" : {
316                    "title" : title,
317                    "code" : code,
318                    "message" : message}})
319        if self.extensions and form_errors:
320            # https://github.com/collection-json/extensions/blob/master/errors.md
321            r['errors'] = errors = {}
322            for key, value in form_errors.items():
323                errors[key] = {'title':'Validation Error','code':'','message':value}
324                response.headers['Content-Type'] = 'application/vnd.collection+json'
325        response.status = 400
326        return response.json({'collection':r})
327
328example_policies = {
329    'thing': {
330        'GET':{'query':None,'fields':['id', 'name']},
331        'POST':{'query':None,'fields':['name']},
332        'PUT':{'query':None,'fields':['name']},
333        'DELETE':{'query':None},
334        },
335    'attr': {
336        'GET':{'query':None,'fields':['id', 'name', 'thing']},
337        'POST':{'query':None,'fields':['name', 'thing']},
338        'PUT':{'query':None,'fields':['name', 'thing']},
339        'DELETE':{'query':None},
340        },
341    }
Note: See TracBrowser for help on using the repository browser.