wiki:BluePrintRESTImplementation

Version 37 (modified by Fran Boon, 13 years ago) ( diff )

Use method parameters, not globals

Implementation for the BluePrintREST:

Model

This is how Module writers need to add tables to their models/module.py:

resource='shelter'
table=module+'_'+resource
db.define_table(table,timestamp,uuidstamp,
                SQLField('name'))
s3.crud_fields[table]=['name']
db[table].exposes=s3.crud_fields[table]
# Moved to Controller - allows us to redefine for different scenarios (& also better MVC separation)
#db[table].displays=s3.crud_fields[table]
# NB Beware of lambdas & %s substitution as they get evaluated when called, not when defined! 
#db[table].represent=lambda table:shn_list_item(table,resource='shelter',action='display')
db[table].uuid.requires=IS_NOT_IN_DB(db,'%s.uuid' % table)
title_create=T('Add Shelter')
title_display=T('Shelter Details')
title_list=T('List Shelters')
title_update=T('Edit Shelter')
subtitle_create=T('Add New Shelter')
subtitle_list=T('Shelters')
label_list_button=T('List Shelters')
label_create_button=T('Add Shelter')
msg_record_created=T('Shelter added')
msg_record_modified=T('Shelter updated')
msg_record_deleted=T('Shelter deleted')
msg_list_empty=T('No Shelters currently registered')
s3.crud_strings[table]=Storage(title_create=title_create, title_display=title_display, title_list=title_list, title_update=title_update, subtitle_create=subtitle_create, subtitle_list=subtitle_list, label_list_button=label_list_button, label_create_button=label_create_button, msg_record_created=msg_record_created, msg_record_modified=msg_record_modified, msg_record_deleted=msg_record_deleted, msg_list_empty=msg_list_empty)

This is the supporting material in models/__db.py:

from gluon.storage import Storage
# Keep all S3 framework-level elements stored off here, so as to avoid polluting global namespace & to make it clear which part of the framework is being interacted with
s3=Storage()
s3.crud_fields=Storage()
s3.crud_strings=Storage()

def shn_crud_strings_lookup(resource):
    "Look up CRUD strings for a given resource based on the definitions in models/module.py."
    return getattr(s3.crud_strings,'%s' % resource)

def import_csv(table,file):
    "Import CSV file into Database. Comes from appadmin.py"
    import csv
    reader = csv.reader(file)
    colnames=None
    for line in reader:
        if not colnames: 
            colnames=[x[x.find('.')+1:] for x in line]
            c=[i for i in range(len(line)) if colnames[i]!='id']            
        else:
            items=[(colnames[i],line[i]) for i in c]
            table.insert(**dict(items))

def import_json(table,file):
    "Import JSON into Database."
    import gluon.contrib.simplejson as sj
    reader=sj.loads(file)
    # ToDo
    # Get column names
    # Insert records
    #table.insert(**dict(items))
    return
            
def shn_rest_controller(module,resource,deletable=True,listadd=True,extra=None):
    """
    RESTlike controller function.
    
    Provides CRUD operations for the given module/resource.
    Optional parameters:
    deletable=False: don't provide visible options for deletion
    listadd=False: don't provide an add form in the list view
    extra='field': extra field to display in the list view
    
    Anonymous users can Read.
    Authentication required for Create/Update/Delete.
    
    Auditing options for Read &/or Write.
    
    Supported Representations:
        HTML is the default (including full Layout)
        PLAIN is HTML with no layout
         - can be inserted into DIVs via AJAX calls
         - can be useful for clients on low-bandwidth or small screen sizes
        JSON
         - read-only for now
        CSV (useful for synchronization)
         - List/Display/Create for now
        AJAX (designed to be run asynchronously to refresh page elements)

    ToDo:
        Alternate Representations
            JSON create/update
            CSV update
            SMS,XML,PDF,LDIF
        Customisable Security Policy
    """
    
    _table='%s_%s' % (module,resource)
    table=db[_table]
    if resource=='setting':
        s3.crud_strings=shn_crud_strings_lookup(resource)
    else:
        s3.crud_strings=shn_crud_strings_lookup(table)
    
    # Which representation should output be in?
    if request.vars.format:
        representation=str.lower(request.vars.format)
    else:
        # Default to HTML
        representation="html"
    
    if len(request.args)==0:
        # No arguments => default to List (or list_create if logged_in)
        if session.s3.audit_read:
            db.s3_audit.insert(
                person=t2.person_id,
                operation='list',
                module=request.controller,
                resource=resource,
                old_value='',
                new_value=''
            )
        if representation=="html":
            if t2.logged_in and deletable:
                if extra:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="TD(db(db.gis_projection.id==%i).select()[0].%s),TD(INPUT(_type='checkbox',_class='delete_row',_name='%s',_id='%i'))" % (table.id,extra,resource,table.id))
                else:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="INPUT(_type='checkbox',_class='delete_row',_name='%s' % resource,_id='%i' % table.id)")
            else:
                if extra:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="db(db.gis_projection.id==%i).select()[0].%s" % (table.id,extra))
                else:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display')
            list=t2.itemize(table)
            if not list:
                list=s3.crud_strings.msg_list_empty
            title=s3.crud_strings.title_list
            subtitle=s3.crud_strings.subtitle_list
            if t2.logged_in and listadd:
                # Display the Add form below List
                if deletable:
                    # Add extra column header to explain the checkboxes
                    if isinstance(list,TABLE):
                        list.insert(0,TR('',B('Delete?')))
                form=t2.create(table)
                # Check for presence of Custom View
                custom_view='%s_list_create.html' % resource
                _custom_view=os.path.join(request.folder,'views',module,custom_view)
                if os.path.exists(_custom_view):
                    response.view=module+'/'+custom_view
                else:
                    response.view='list_create.html'
                addtitle=s3.crud_strings.subtitle_create
                return dict(module_name=module_name,modules=modules,options=options,list=list,form=form,title=title,subtitle=subtitle,addtitle=addtitle)
            else:
                # List only
                if listadd:
                    add_btn=A(s3.crud_strings.label_create_button,_href=t2.action(resource,'create'),_id='add-btn')
                else:
                    add_btn=''
                # Check for presence of Custom View
                custom_view='%s_list.html' % resource
                _custom_view=os.path.join(request.folder,'views',module,custom_view)
                if os.path.exists(_custom_view):
                    response.view=module+'/'+custom_view
                else:
                    response.view='list.html'
                return dict(module_name=module_name,modules=modules,options=options,list=list,title=title,subtitle=subtitle,add_btn=add_btn)
        elif representation=="ajax":
            if t2.logged_in and deletable:
                if extra:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="TD(db(db.gis_projection.id==%i).select()[0].%s),TD(INPUT(_type='checkbox',_class='delete_row',_name='%s',_id='%i'))" % (table.id,extra,resource,table.id))
                else:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="INPUT(_type='checkbox',_class='delete_row',_name='%s' % resource,_id='%i' % table.id)")
            else:
                if extra:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="db(db.gis_projection.id==%i).select()[0].%s" % (table.id,extra))
                else:
                    db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display')
            list=t2.itemize(table)
            if not list:
                list=s3.crud_strings.msg_list_empty
            if deletable:
                # Add extra column header to explain the checkboxes
                if isinstance(list,TABLE):
                    list.insert(0,TR('',B('Delete?')))
            response.view='plain.html'
            return dict(item=list)
        elif representation=="plain":
            list=t2.itemize(table)
            response.view='plain.html'
            return dict(item=list)
        elif representation=="json":
            list=db().select(table.ALL).json()
            response.view='plain.html'
            return dict(item=list)
        elif representation=="csv":
            import gluon.contenttype
            response.headers['Content-Type']=gluon.contenttype.contenttype('.csv')
            query=db[table].id>0
            response.headers['Content-disposition']="attachment; filename=%s_%s_list.csv" % (request.env.server_name,resource)
            return str(db(query).select())
        else:
            session.error=T("Unsupported format!")
            redirect(URL(r=request,f=resource))
    else:
        method=str.lower(request.args[0])
        if request.args[0].isdigit():
            # 1st argument is ID not method => Display.
            if session.s3.audit_read:
                db.s3_audit.insert(
                    person=t2.person_id,
                    operation='read',
                    representation=representation,
                    module=request.controller,
                    resource=resource,
                    record=t2.id,
                    old_value='',
                    new_value=''
                )
            if representation=="html":
                try:
                    db[table].displays=s3.crud_fields[table]
                except:
                    pass
                item=t2.display(table)
                # Check for presence of Custom View
                custom_view='%s_display.html' % resource
                _custom_view=os.path.join(request.folder,'views',module,custom_view)
                if os.path.exists(_custom_view):
                    response.view=module+'/'+custom_view
                else:
                    response.view='display.html'
                title=s3.crud_strings.title_display
                edit=A(T("Edit"),_href=t2.action(resource,['update',t2.id]),_id='edit-btn')
                if deletable:
                    delete=A(T("Delete"),_href=t2.action(resource,['delete',t2.id]),_id='delete-btn')
                else:
                    delete=''
                list_btn=A(s3.crud_strings.label_list_button,_href=t2.action(resource),_id='list-btn')
                return dict(module_name=module_name,modules=modules,options=options,item=item,title=title,edit=edit,delete=delete,list_btn=list_btn)
            elif representation=="plain":
                item=t2.display(table)
                response.view='plain.html'
                return dict(item=item)
            elif representation=="json":
                item=db(table.id==t2.id).select(table.ALL).json()
                response.view='plain.html'
                return dict(item=item)
            elif representation=="csv":
                import gluon.contenttype
                response.headers['Content-Type']=gluon.contenttype.contenttype('.csv')
                query=db[table].id==t2.id
                response.headers['Content-disposition']="attachment; filename=%s_%s_%d.csv" % (request.env.server_name,resource,t2.id)
                return str(db(query).select())
            elif representation=="rss":
                #if request.args and request.args[0] in settings.rss_procedures:
                #   feed=eval('%s(*request.args[1:],**dict(request.vars))'%request.args[0])
                #else:
                #   t2._error()
                #import gluon.contrib.rss2 as rss2
                #rss = rss2.RSS2(
                #   title=feed['title'],
                #   link = feed['link'],
                #   description = feed['description'],
                #   lastBuildDate = feed['created_on'],
                #   items = [
                #      rss2.RSSItem(
                #        title = entry['title'],
                #        link = entry['link'],
                #        description = entry['description'],
                #        pubDate = entry['created_on']) for entry in feed['entries']]
                #   )
                #response.headers['Content-Type']='application/rss+xml'
                #return rss2.dumps(rss)
                response.view='plain.html'
                return
            else:
                session.error=T("Unsupported format!")
                redirect(URL(r=request,f=resource))
        else:
            if method=="create":
                if t2.logged_in:
                    if session.s3.audit_write:
                        audit_id=db.s3_audit.insert(
                            person=t2.person_id,
                            operation='create',
                            representation=representation,
                            module=request.controller,
                            resource=resource,
                            record=t2.id,
                            old_value='',
                            new_value=''
                        )
                    if representation=="html":
                        t2.messages.record_created=s3.crud_strings.msg_record_created
                        form=t2.create(table)
                        # Check for presence of Custom View
                        custom_view='%s_create.html' % resource
                        _custom_view=os.path.join(request.folder,'views',module,custom_view)
                        if os.path.exists(_custom_view):
                            response.view=module+'/'+custom_view
                        else:
                            response.view='create.html'
                        title=s3.crud_strings.title_create
                        list_btn=A(s3.crud_strings.label_list_button,_href=t2.action(resource),_id='list-btn')
                        return dict(module_name=module_name,modules=modules,options=options,form=form,title=title,list_btn=list_btn)
                    elif representation=="plain":
                        form=t2.create(table)
                        response.view='plain.html'
                        return dict(item=form)
                    elif representation=="json":
                        # ToDo
                        # Read in POST
                        #file=request.body.read()
                        #import_json(table,file)
                        item='{"Status":"failed","Error":{"StatusCode":501,"Message":"JSON creates not yet supported!"}}'
                        response.view='plain.html'
                        return dict(item=item)
                    elif representation=="csv":
                        # Read in POST
                        file=request.vars.filename.file
                        try:
                            import_csv(table,file)
                            reply=T('Data uploaded')
                        except: 
                            reply=T('Unable to parse CSV file!')
                        return reply
                    else:
                        session.error=T("Unsupported format!")
                        redirect(URL(r=request,f=resource))
                else:
                    t2.redirect('login',vars={'_destination':'%s/create' % resource})
            elif method=="display":
                t2.redirect(resource,args=t2.id)
            elif method=="update":
                if t2.logged_in:
                    if session.s3.audit_write:
                        old_value = []
                        _old_value=db(db[table].id==t2.id).select()[0]
                        for field in _old_value:
                            old_value.append(field+':'+str(_old_value[field]))
                        audit_id=db.s3_audit.insert(
                            person=t2.person_id,
                            operation='update',
                            representation=representation,
                            module=request.controller,
                            resource=resource,
                            record=t2.id,
                            old_value=old_value,
                            new_value=''
                        )
                    if representation=="html":
                        t2.messages.record_modified=s3.crud_strings.msg_record_modified
                        form=t2.update(table,deletable=False)
                        # Check for presence of Custom View
                        custom_view='%s_update.html' % resource
                        _custom_view=os.path.join(request.folder,'views',module,custom_view)
                        if os.path.exists(_custom_view):
                            response.view=module+'/'+custom_view
                        else:
                            response.view='update.html'
                        title=s3.crud_strings.title_update
                        list_btn=A(s3.crud_strings.label_list_button,_href=t2.action(resource),_id='list-btn')
                        return dict(module_name=module_name,modules=modules,options=options,form=form,title=title,list_btn=list_btn)
                    elif representation=="plain":
                        form=t2.update(table,deletable=False)
                        response.view='plain.html'
                        return dict(item=form)
                    elif representation=="json":
                        # ToDo
                        item='{"Status":"failed","Error":{"StatusCode":501,"Message":"JSON updates not yet supported!"}}'
                        response.view='plain.html'
                        return dict(item=item)
                    else:
                        session.error=T("Unsupported format!")
                        redirect(URL(r=request,f=resource))
                else:
                    t2.redirect('login',vars={'_destination':'%s/update/%i' % (resource,t2.id)})
            elif method=="delete":
                if t2.logged_in:
                    if session.s3.audit_write:
                        old_value = []
                        _old_value=db(db[table].id==t2.id).select()[0]
                        for field in _old_value:
                            old_value.append(field+':'+str(_old_value[field]))
                        db.s3_audit.insert(
                            person=t2.person_id,
                            operation='delete',
                            representation=representation,
                            module=request.controller,
                            resource=resource,
                            record=t2.id,
                            old_value=old_value,
                            new_value=''
                        )
                    t2.messages.record_deleted=s3.crud_strings.msg_record_deleted
                    if representation=="ajax":
                        t2.delete(table,next='%s?format=ajax' % resource)
                    else:
                        t2.delete(table,next=resource)
                else:
                    t2.redirect('login',vars={'_destination':'%s/delete/%i' % (resource,t2.id)})
            elif method=="search":
                if session.s3.audit_read:
                    db.s3_audit.insert(
                        person=t2.person_id,
                        operation='search',
                        module=request.controller,
                        resource=resource,
                        old_value='',
                        new_value=''
                    )
                if representation=="html":
                    if t2.logged_in and deletable:
                        db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display',extra="INPUT(_type='checkbox',_class='delete_row',_name='%s' % resource,_id='%i' % table.id)")
                    else:
                        db[table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display')
                    search=t2.search(table)
                    # Check for presence of Custom View
                    custom_view='%s_search.html' % resource
                    _custom_view=os.path.join(request.folder,'views',module,custom_view)
                    if os.path.exists(_custom_view):
                        response.view=module+'/'+custom_view
                    else:
                        response.view='search.html'
                    title=s3.crud_strings.title_search
                    return dict(module_name=module_name,modules=modules,options=options,search=search,title=title)
                else:
                    session.error=T("Unsupported format!")
                    redirect(URL(r=request,f=resource))
            else:
                session.error=T("Unsupported method!")
                redirect(URL(r=request,f=resource))

Controller

If using a single table for a resource, Developers just need to add this to their Controllers to provide all necessary CRUD functions with support for multiple representations:

def shelter():
    "RESTful CRUD controller"
    return shn_rest_controller(module,'shelter')

These optional settings can also be set, if-desired:

# Don't allow resources to be creatable from List view
return shn_rest_controller(module,'shelter',listadd=False)
# Don't allow resources to be deletable from List/Display views
return shn_rest_controller(module,'shelter',deletable=False)
# Display extra field in the list view
return shn_rest_controller(module,'shelter',extra='epsg')

Views

If using a single table for a resource, Developers don't normally need to create any special views for CRUD. Default ones work fine.

If needing to create custom views (e.g. GIS Layer currently) then can extend these to add extra information in a maintainable way.

Custom views to replace the create.html, display.html, etc for each module and its resource(s) can be created in the regular 'views/module' directory as '<resource-name>_create.html' for example to replace create.html. If this view is not found for that module/resource, it will revert to the default one.

For example, to make a custom list_create view for the 'cr' module's 'shelter' resource, you would make a new view template file in 'views/cr/shelter_list_create.html' and make it look how you want. It will get picked up automatically if it exists, else the default one will be used.

create.html

{extend 'layout.html'}}
{{try:}}
 {{=H2(title)}}
{{except:}}
{{pass}}
{{include 'key.html'}}
<div class='form-container'>
{{try:}}
 {{=form}}
{{except:}}
  {{include}}
{{pass}}
</div>
<p>&nbsp;</p>
{{try:}}
 {{=list_btn}}
{{except:}}
{{pass}}

display.html

{{extend 'layout.html'}}
{{try:}}
 {{=H2(title)}}
{{except:}}
{{pass}}
{{try:}}
 {{=edit}}
{{except:}}
{{pass}}
<div class='item-container'>
{{try:}}
 {{=item}}
{{except:}}
  {{include}}
{{pass}}
</div>
{{try:}}
 {{=delete}}
{{except:}}
{{pass}}
<p>&nbsp;</p>
{{try:}}
 {{=list_btn}}
{{except:}}
{{pass}}

list.html

{{extend 'layout.html'}}
{{try:}}
 {{=H2(title)}}
{{except:}}
{{pass}}
{{try:}}
 {{=H3(subtitle)}}
{{except:}}
{{pass}}
<div id='list-container'>
{{try:}}
 {{=list}}
{{except:}}
  {{include}}
{{pass}}
</div>
<p>&nbsp;</p>
{{try:}}
 {{=add_btn}}
{{except:}}
{{pass}}

list_create.html

{{extend 'layout.html'}}
{{try:}}
 {{=H2(title)}}
{{except:}}
{{pass}}
{{try:}}
 {{=H3(subtitle)}}
{{except:}}
{{pass}}
<div id='list-container'>
{{try:}}
 {{=list}}
{{except:}}
{{pass}}
</div>
<p>&nbsp;</p>
{{try:}}
 {{=H3(addtitle)}}
{{except:}}
{{pass}}
<div class='form-container'>
{{try:}}
 {{=form}}
{{except:}}
{{pass}}
</div>
{{include 'key.html'}}

update.html

{{extend 'layout.html'}}

{{try:}}
 {{=H2(title)}}
{{except:}}
{{pass}}
{{include 'key.html'}}
<div class='form-container'>
{{try:}}
 {{=form}}
{{except:}}
  {{include}}
{{pass}}
</div>
<p>&nbsp;</p>
{{try:}}
 {{=list_btn}}
{{except:}}
{{pass}}

key.html

<p><b>{{=T('Key')}}:</b><b class='red'> * </b> - {{=T('Fields tagged with a star')}} &#040;<span class='red'> * </span>&#041; {{=T('are mandatory and must be filled')}}.</p>

plain.html

{{=item}}
Note: See TracWiki for help on using the wiki.