12 min read

(For more resources related to this topic, see here.)

Improving the scaffolding application

In this recipe, we discuss how to create your own scaffolding application and add your own configuration file. The scaffolding application is the collection of files that come with any new web2py application.

How to do it…

The scaffolding app includes several files. One of them is models/db.py, which imports four classes from gluon.tools (Mail, Auth, Crud, and Service), and defines the following global objects: db, mail, auth, crud, and service.

The scaffolding application also defines tables required by the auth object, such as db.auth_user.

The default scaffolding application is designed to minimize the number of files, not to be modular. In particular, the model file, db.py, contains the configuration, which in a production environment, is best kept in separate files.

Here, we suggest creating a configuration file, models/0.py, that contains something like the following:

from gluon.storage import Storage settings = Storage() settings.production = False if settings.production: settings.db_uri = 'sqlite://production.sqlite' settings.migrate = False else: settings.db_uri = 'sqlite://development.sqlite' settings.migrate = True settings.title = request.application settings.subtitle = 'write something here' settings.author = 'you' settings.author_email = '[email protected]' settings.keywords = '' settings.description = '' settings.layout_theme = 'Default' settings.security_key = 'a098c897-724b-4e05-b2d8-8ee993385ae6' settings.email_server = 'localhost' settings.email_sender = '[email protected]' settings.email_login = '' settings.login_method = 'local' settings.login_config = ''

We also modify models/db.py, so that it uses the information from the configuration file, and it defines the auth_user table explicitly (this makes it easier to add custom fields):

from gluon.tools import * db = DAL(settings.db_uri) if settings.db_uri.startswith('gae'): session.connect(request, response, db = db) mail = Mail() # mailer auth = Auth(db) # authentication/authorization crud = Crud(db) # for CRUD helpers using auth service = Service() # for json, xml, jsonrpc, xmlrpc, amfrpc plugins = PluginManager() # enable generic views for all actions for testing purpose response.generic_patterns = ['*'] mail.settings.server = settings.email_server mail.settings.sender = settings.email_sender mail.settings.login = settings.email_login auth.settings.hmac_key = settings.security_key # add any extra fields you may want to add to auth_user auth.settings.extra_fields['auth_user'] = [] # user username as well as email auth.define_tables(migrate=settings.migrate,username=True) auth.settings.mailer = mail auth.settings.registration_requires_verification = False auth.settings.registration_requires_approval = False auth.messages.verify_email = 'Click on the link http://' + request.env.http_host + URL('default','user', args=['verify_email']) + '/%(key)s to verify your email' auth.settings.reset_password_requires_verification = True auth.messages.reset_password = 'Click on the link http://' + request.env.http_host + URL('default','user', args=['reset_password']) + '/%(key)s to reset your password' if settings.login_method=='janrain': from gluon.contrib.login_methods.rpx_account import RPXAccount auth.settings.actions_disabled=['register', 'change_password', 'request_reset_password'] auth.settings.login_form = RPXAccount(request, api_key = settings.login_config.split(':')[-1], domain = settings.login_config.split(':')[0], url = "http://%s/%s/default/user/login" % (request.env.http_host, request.application))

Normally, after a web2py installation or upgrade, the welcome application is tar-gzipped into welcome.w2p, and is used as the scaffolding application. You can create your own scaffolding application from an existing application using the following commands from a bash shell:

cd applications/app tar zcvf ../../welcome.w2p *

There’s more…

The web2py wizard uses a similar approach, and creates a similar 0.py configuration file. You can add more settings to the 0.py file as needed.

The 0.py file may contain sensitive information, such as the security_key used to encrypt passwords, the email_login containing the password of your smtp account, and the login_config with your Janrain password (http://www.janrain.com/). You may want to write this sensitive information in a read-only file outside the web2py tree, and read them from your 0.py instead of hardcoding them. In this way, if you choose to commit your application to a version-control system, you will not be committing the sensitive information

The scaffolding application includes other files that you may want to customize, including views/layout.html and views/default/users.html. Some of them are the subject of upcoming recipes.

Building a simple contacts application

When you start designing a new web2py application, you go through three phases that are characterized by looking for the answer to the following three questions:

  • What data should the application store?

  • Which pages should be presented to the visitors?

  • How should the page content, for each page, be presented?

The answer to these three questions is implemented in the models, the controllers, and the views respectively.

It is important for a good application design to try answering those questions exactly in this order, and as accurately as possible. Such answers can later be revised, and more tables, more pages, and more bells and whistles can be added in an iterative fashion. A good web2py application is designed in such a way that you can change the table definitions (add and remove fields), add pages, and change page views, without breaking the application.

A distinctive feature of web2py is that everything has a default. This means you can work on the first of those three steps without the need to write code for the second and third step. Similarly, you can work on the second step without the need to code for the third. At each step, you will be able to immediately see the result of your work; thanks to appadmin (the default database administrative interface) and generic views (every action has a view by default, until you write a custom one).

Here we consider, as a first example, an application to manage our business contacts, a CRM. We will call it Contacts. The application needs to maintain a list of companies, and a list of people who work at those companies.

How to do it…

  1. First of all we create the model.

    In this step we identify which tables are needed and their fields. For each field, we determine whether they:

    • Must contain unique values (unique=True)

    • Contain empty values (notnull=True)

    • Are references (contain a list of a record in another table)

    • Are used to represent a record (format attribute)

    From now on, we will assume we are working with a copy of the default scaffolding application, and we only describe the code that needs to be added or replaced. In particular, we will assume the default views/layout.html and models/db.py.

    Here is a possible model representing the data we need to store in models/db_contacts.py:

    # in file: models/db_custom.py db.define_table('company', Field('name', notnull=True, unique=True), format='%(name)s') db.define_table('contact', Field('name', notnull=True), Field('company', 'reference company'), Field('picture', 'upload'), Field('email', requires=IS_EMAIL()), Field('phone_number', requires=IS_MATCH('[d-() ]+')), Field('address'), format='%(name)s') db.define_table('log', Field('body', 'text',notnull=True), Field('posted_on', 'datetime'), Field('contact', 'reference contact'))

    Of course, a more complex data representation is possible. You may want to allow, for example, multiple users for the system, allow the same person to work for multiple companies, and keep track of changes in time. Here, we will keep it simple.

    The name of this file is important. In particular, models are executed in alphabetical order, and this one must follow db.py.

  2. After this file has been created, you can try it by visiting the following url: http://127.0.0.1:8000/contacts/appadmin, to access the web2py database administrative interface, appadmin. Without any controller or view, it provides a way to insert, select, update, and delete records.

  3. Now we are ready to build the controller. We need to identify which pages are required by the application. This depends on the required workflow. At a minimum we need the following pages:

    • An index page (the home page)

    • A page to list all companies

    • A page that lists all contacts for one selected company

    • A page to create a company

    • A page to edit/delete a company

    • A page to create a contact

    • A page to edit/delete a contact

    • A page that allows to read the information about one contact and the communication logs, as well as add a new communication log

  4. Such pages can be implemented as follows:

    # in file: controllers/default.py def index(): return locals() def companies(): companies = db(db.company).select(orderby=db.company.name) return locals() def contacts(): company = db.company(request.args(0)) or redirect(URL('companies')) contacts = db(db.contact.company==company.id).select( orderby=db.contact.name) return locals() @auth.requires_login() def company_create(): form = crud.create(db.company, next='companies') return locals() @auth.requires_login() def company_edit(): company = db.company(request.args(0)) or redirect(URL('companies')) form = crud.update(db.company, company, next='companies') return locals() @auth.requires_login() def contact_create(): db.contact.company.default = request.args(0) form = crud.create(db.contact, next='companies') return locals() @auth.requires_login() def contact_edit(): contact = db.contact(request.args(0)) or redirect(URL('companies')) form = crud.update(db.contact, contact, next='companies') return locals() @auth.requires_login() def contact_logs(): contact = db.contact(request.args(0)) or redirect(URL('companies')) db.log.contact.default = contact.id db.log.contact.readable = False db.log.contact.writable = False db.log.posted_on.default = request.now db.log.posted_on.readable = False db.log.posted_on.writable = False form = crud.create(db.log) logs = db( db.log.contact==contact.id).select(orderby=db.log.posted_on) return locals() def download(): return response.download(request, db) def user(): return dict(form=auth())

  5. Make sure that you do not delete the existing user, download, and service functions in the scaffolding default.py.

  6. Notice how all pages are built using the same ingredients: select queries and crud forms. You rarely need anything else.

  7. Also notice the following:

    • Some pages require a request.args(0) argument (a company ID for contacts and company_edit, a contact ID for contact_edit, and contact_logs).

    • All selects have an orderby argument.

    • All crud forms have a next argument that determines the redirection after form submission.

    • All actions return locals(), which is a Python dictionary containing the local variables defined in the function. This is a shortcut. It is of course possible to return a dictionary with any subset of locals().

    • contact_create sets a default value for the new contact company to the value passed as args(0).

    • The contacts_logs retrieves past logs after processing crud.create for a new log entry. This avoid unnecessarily reloading of the page, when a new log is inserted.

  8. At this point our application is fully functional, although the look-and-feel and navigation can be improved.:

    • You can create a new company at:

      http://127.0.0.1:8000/contacts/default/company_create

    • You can list all companies at:

      http://127.0.0.1:8000/contacts/default/companies

    • You can edit company #1 at:

      http://127.0.0.1:8000/contacts/default/company_edit/1

    • You can create a new contact at:

      http://127.0.0.1:8000/contacts/default/contact_create

    • You can list all contacts for company #1 at:

      http://127.0.0.1:8000/contacts/default/contacts/1

    • You can edit contact #1 at:

      http://127.0.0.1:8000/contacts/default/contact_edit/1

    • And you can access the communication log for contact #1 at:

      http://127.0.0.1:8000/contacts/default/contact_logs/1

  9. You should also edit the models/menu.py file, and replace the content with the following:

    response.menu = [['Companies', False, URL('default', 'companies')]]

    The application now works, but we can improve it by designing a better look and feel for the actions. That’s done in the views.

  10. Create and edit file views/default/companies.html:

    {{extend 'layout.html'}} <h2>Companies</h2> <table> {{for company in companies:}} <tr> <td>{{=A(company.name, _href=URL('contacts', args=company.id))}}</td> <td>{{=A('edit', _href=URL('company_edit', args=company.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('add company', _href=URL('company_create'))}}</td> </tr> </table> response.menu = [['Companies', False, URL('default', 'companies')]]

    Here is how this page looks:

  11. Create and edit file views/default/contacts.html:

    {{extend 'layout.html'}} <h2>Contacts at {{=company.name}}</h2> <table> {{for contact in contacts:}} <tr> <td>{{=A(contact.name, _href=URL('contact_logs', args=contact.id))}}</td> <td>{{=A('edit', _href=URL('contact_edit', args=contact.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('add contact', _href=URL('contact_create', args=company.id))}}</td> </tr> </table>

    Here is how this page looks:

  12. Create and edit file views/default/company_create.html:

    {{extend 'layout.html'}} <h2>New company</h2> {{=form}}

  13. Create and edit file views/default/contact_create.html:

    {{extend 'layout.html'}} <h2>New contact</h2> {{=form}}

  14. Create and edit file: views/default/company_edit.html:

    {{extend 'layout.html'}} <h2>Edit company</h2> {{=form}}

  15. Create and edit file views/default/contact_edit.html:

    {{extend 'layout.html'}} <h2>Edit contact</h2> {{=form}}

  16. Create and edit file views/default/contact_logs.html:

    {{extend 'layout.html'}} <h2>Logs for contact {{=contact.name}}</h2> <table> {{for log in logs:}} <tr> <td>{{=log.posted_on}}</td> <td>{{=MARKMIN(log.body)}}</td> </tr> {{pass}} <tr> <td></td> <td>{{=form}}</td> </tr> </table>

    Here is how this page looks:

Notice that in the last view, we used the function MARKMIN to render the content of the db.log.body, using the MARKMIN markup. This allows embedding links, images, anchors, font formatting information, and tables in the logs. For details about the MARKMIN syntax we refer to: http://web2py.com/examples/static/markmin.html.

LEAVE A REPLY

Please enter your comment!
Please enter your name here