Implementation for the [wiki:BluePrintREST 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')) exec("s3.crud_fields.%s=['name']" % table) db['%s' % table].exposes=s3.crud_fields['%s' % table] # Moved to Controller - allows us to redefine for different scenarios (& also better MVC separation) #db['%s' % table].displays=s3.crud_fields['%s' % table] # NB Beware of lambdas & %s substitution as they get evaluated when called, not when defined! #db['%s' % 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') exec('s3.crud_strings.%s=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)' % table) }}} These optional settings can also be set, if-desired: {{{ exec('s3.listonly.%s=1' % table) # Don't allow resources to be creatable from List view exec('s3.undeletable.%s=1' % table) # Don't allow resources to be deletable from List/Display views }}} 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() s3.listonly=Storage() s3.undeletable=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 shn_rest_controller(module,resource): """ RESTlike controller function. Anonymous users can Read. Authentication required for Create/Update/Delete. 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) - read-only for now AJAX (designed to be run asynchronously to refresh page elements) ToDo: Alternate Representations JSON create/update CSV create/update SMS,XML,PDF Customisable Security Policy """ table=db['%s_%s' % (module,resource)] if resource=='setting': s3.crud_strings=shn_crud_strings_lookup(resource) else: s3.crud_strings=shn_crud_strings_lookup(table) try: s3.deletable=not s3.undeletable['%s' % table] except: s3.deletable=True try: s3.listadd=not s3.listonly['%s' % table] except: s3.listadd=True # 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 s3.deletable: db['%s' % 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['%s' % table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display') list=t2.itemize(table) if list=="No data": list=s3.crud_strings.msg_list_empty title=s3.crud_strings.title_list subtitle=s3.crud_strings.subtitle_list if t2.logged_in and s3.listadd: # Display the Add form below List if s3.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 s3.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 s3.deletable: db['%s' % 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['%s' % table].represent=lambda table:shn_list_item(table,resource='%s' % resource,action='display') list=t2.itemize(table) if list=="No data": list=s3.crud_strings.msg_list_empty if s3.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['%s' % 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['%s' % table].displays=s3.crud_fields['%s' % 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 s3.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['%s' % 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 item='{"Status":"failed","Error":{"StatusCode":501,"Message":"JSON creates 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/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['%s' % 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['%s' % 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 s3.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') }}} == 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 '_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'}}
{{try:}} {{=form}} {{except:}} {{include}} {{pass}}

 

{{try:}} {{=list_btn}} {{except:}} {{pass}} }}} {{{display.html}}} {{{ {{extend 'layout.html'}} {{try:}} {{=H2(title)}} {{except:}} {{pass}} {{try:}} {{=edit}} {{except:}} {{pass}}
{{try:}} {{=item}} {{except:}} {{include}} {{pass}}
{{try:}} {{=delete}} {{except:}} {{pass}}

 

{{try:}} {{=list_btn}} {{except:}} {{pass}} }}} {{{list.html}}} {{{ {{extend 'layout.html'}} {{try:}} {{=H2(title)}} {{except:}} {{pass}} {{try:}} {{=H3(subtitle)}} {{except:}} {{pass}}
{{try:}} {{=list}} {{except:}} {{include}} {{pass}}

 

{{try:}} {{=add_btn}} {{except:}} {{pass}} }}} {{{list_create.html}}} {{{ {{extend 'layout.html'}} {{try:}} {{=H2(title)}} {{except:}} {{pass}} {{try:}} {{=H3(subtitle)}} {{except:}} {{pass}}
{{try:}} {{=list}} {{except:}} {{pass}}

 

{{try:}} {{=H3(addtitle)}} {{except:}} {{pass}}
{{try:}} {{=form}} {{except:}} {{pass}}
{{include 'key.html'}} }}} {{{update.html}}} {{{ {{extend 'layout.html'}} {{try:}} {{=H2(title)}} {{except:}} {{pass}} {{include 'key.html'}}
{{try:}} {{=form}} {{except:}} {{include}} {{pass}}

 

{{try:}} {{=list_btn}} {{except:}} {{pass}} }}} {{{key.html}}} {{{

{{=T('Key')}}: * - {{=T('Fields tagged with a star')}} ( * ) {{=T('are mandatory and must be filled')}}.

}}} {{{plain.html}}} {{{ {{=item}} }}}