Building Your First Application

Exclusive offer: get 50% off this eBook here
web2py Application Development Cookbook

web2py Application Development Cookbook — Save 50%

Over 110 recipes to master this full-stack Python web framework with this book and ebook

$29.99    $15.00
by Mariano Reingart Massimo Di Pierro | January 2013 | Cookbooks Open Source

In this article by Mariano Reingart and Massimo Di Pierro, the authors of web2py Application Development Cookbook we will cover:

  • Improving the scaffolding application

  • Building a simple contacts application

  • Building a Reddit clone

  • Building a Facebook clone

The recipes in this article will provide examples of complete applications, comprising models, views, and controllers. They range from simple contacts applications to a more complex Facebook clone. Other recipes in this article will show you how to solve some recurrent problems that new users typically encounter, from adding a logo to creating a navigation bar.

(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 = 'you@example.come' settings.keywords = '' settings.description = '' settings.layout_theme = 'Default' settings.security_key = 'a098c897-724b-4e05-b2d8-8ee993385ae6' settings.email_server = 'localhost' settings.email_sender = 'you@example.com' 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.

web2py Application Development Cookbook Over 110 recipes to master this full-stack Python web framework with this book and ebook
Published: March 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Building a Reddit clone

Here we show how to build an application to post and rank links to online news items, similar to the http://www.reddit.com/ website. The links are organized into categories, and users can post, vote, and comment on them. As in the previous recipe, the code only shows additions or changes to the default scaffolding application. We will call our application reddit.

In this recipe, we will not support threaded comments (as in the actual http://www.reddit.com/website), because it would be an unnecessary complication. We will discuss threaded comments in a subsequent recipe.

We will follow the same steps discussed in the previous recipe.

How to do it...

This application is very similar to the contacts of the previous recipe. In fact, the data model is almost identical, provided that we map table company into a table category, and table contact into a table news. The main differences are that news items do not have a name, but they have a title and a link instead. Moreover, news items must be sorted by user votes, and not alphabetically. We also need to add a mechanism to allow users to vote, record votes, and prevent double counting. We need an extra table for this.

Here is the complete model:

# in file: models/db_reddit.py db.define_table('category', Field('name' ,notnull=True, unique=True), format='%(name)s') db.define_table('news', Field('title', notnull=True), Field('link', requires=IS_URL()), Field('category', 'reference category', readable=False, writable=False), Field('votes', 'integer', readable=False, writable=False), Field('posted_on', 'datetime', readable=False, writable=False), Field('posted_by', 'reference auth_user', readable=False, writable=False), format='%(title)s') db.define_table('comment', Field('news', 'reference news', readable=False, writable=False), Field('body', 'text', notnull=True), Field('posted_on', 'datetime', readable=False, writable=False), Field('posted_by', 'reference auth_user', readable=False, writable=False)) db.define_table('vote', Field('news', 'reference news'), Field('value', 'integer'), Field('posted_on', 'datetime', readable=False, writable=False), Field('posted_by', 'reference auth_user', readable=False, writable=False))

  1. As discussed previously, many of the needed actions are equivalent to the contacts application of the previous recipe. In particular, we need actions to list categories, to list news for a given category, to create and edit categories, to create and edit news, to list comments, and vote for news items.

    def index(): return locals() def categories(): categories = db(db.category).select(orderby=db.category.name) return locals() def news(): category = db.category(request.args(0)) or redirect(URL('categories')) news = db(db.news.category==category.id).select( orderby=~db.news.votes, limitby=(0, 25)) return locals() @auth.requires_membership('manager') def category_create(): form = crud.create(db.category, next='categories') return locals() @auth.requires_membership('manager') def category_edit(): category = db.category(request.args(0)) or redirect(URL('categories')) form = crud.update(db.category, category, next='categories') return locals() @auth.requires_login() def news_create(): db.news.category.default = request.args(0) db.news.votes.default = 0 form = crud.create(db.news, next='news_comments/[id]') return locals() @auth.requires_login() def news_edit(): news = db.news(request.args(0)) or redirect(URL('categories')) if not news.posted_by==auth.user.id: redirect(URL('not_authorized')) form = crud.update(db.news, category, next='news_comments/[id]') return locals() def news_comments(): news = db.news(request.args(0)) or redirect(URL('categories')) if auth.user: db.comment.news.default = news.id db.comment.posted_on.default = request.now db.comment.posted_by.default = auth.user.id form = crud.create(db.comment) comments = db(db.comment.news==news.id).select( orderby=db.comment.posted_on) return locals() @auth.requires_login() def vote(): if not request.env.request_method=='POST': raise HTTP(400) news_id, mode = request.args(0), request.args(1) news = db.news(id=news_id) vote = db.vote(posted_by=auth.user.id, news=news_id) votes = news.votes value = (mode=='plus') and +1 or -1 if vote and value*vote.value==1: message = 'you voted already' else: if vote: votes += value - vote.value vote.update_record(value=value) else: votes += value db.vote.insert(value=value, posted_by=auth.user.id, posted_on=request.now, news=news_id) news.update_record(votes=votes) message = 'vote recorded' return "jQuery('#votes').html('%s');jQuery('.flash').\ html('%s').slideDown();" % (votes, message)

    Most of these actions are very standard, and composed of the usual select and crud forms.

  2. We used two types of decorators to make sure that only logged-in users can edit content, and only managers can create and edit categories. You can use appadmin to create a manager group and give membership to users:

    The only special action is the last vote. The vote action is designed to be an Ajax callback. To avoid indirect reference attacks, the first line makes sure the action is called with a POST request. Then we parse the request args: it expects a news ID as args(0), and plus or minus as args(0), depending on whether we want to vote the news item up or down. If we vote up (plus), it creates a new db.vote entry with value equal to +1. If we vote down (minus), it creates a new db.vote entry with value equal to -1. The action also checks whether we voted already. We are allowed to change our vote, but not to vote twice.

    This action returns a JavaScript string that updates the votes HTML element with the latest vote count, and flashes a new message. The last line of the action is tightly coupled with the view that will perform the Ajax call (views/default/news_comments.html).

  3. We also want to list all possible categories in the menu:

    # in file: models/menu.py" categories = db(db.category).select(orderby=db.category.name, cache=(cache.ram, 60)) response.menu = [(c.name, False, URL('default', 'news', args=c.id)) for c in categories]

  4. Finally, we need to create the following views:

    • views/default/categories.html:

      {{extend 'layout.html'}} <h2>Categories</h2> <table> {{for category in categories:}} <tr> <td>{{=A(category.name, _href=URL('news', args=category.id))}}</td> <td>{{=A('edit', _href=URL('category_edit', args=category.id))}} </td> </tr> {{pass}} <tr> <td>{{=A('add category', _href=URL('category_create'))}}</td> </tr> </table>

    • views/default/news.html:

      {{extend 'layout.html'}} <h2>News at {{=category.name}}</h2> <table> {{for news in news:}} <tr> <td>{{=A(news.title, _href=news.link)}}</td> <td>{{=A('comments', _href=URL('news_comments', args=news.id))}} </td> <td>{{=A('edit', _href=URL('news_edit', args=news.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('post news item', _href=URL('news_create', args=category.id))}} </td> <td></td> </tr> </table>

      Here is how this page looks:

    • views/default/category_create.html:

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

    • views/default/news_create.html:

      {{extend 'layout.html'}} <h2>Post news item</h2> {{=form}}

    • views/default/category_edit.html:

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

    • views/default/categories.html:

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

    • views/default/news_comments.html:

      {{extend 'layout.html'}} <h2>Comments for {{=A(news.title, _href=news.link)}}</h2> {{if auth.user:}} <span id="votes">{{=news.votes}}</span> <button id="plus" onclick="ajax('{{=URL('vote', args=(news.id, 'plus'))}}', [], ':eval')"> plus </button> <button id="minus" onclick="ajax('{{=URL('vote', args=(news.id, 'minus'))}}', [], ':eval')"> minus </button> {{=form}} {{pass}} <table> {{for comment in comments:}} <tr> <td>{{=comment.posted_on}}</td> <td>{{=comment.posted_by.first_name}} says </td> <td>{{=MARKMIN(comment.body)}}</td> </tr> {{pass}} </table>

      Notice the code:

      <button id="plus" onclick="ajax('{{=URL('vote', args=(news.id, 'plus'))}}', [], ':eval')"> plus </button>

      On clicking, it performs an Ajax request that records our vote. The return value of the Ajax request is evaluated (:eval). The URL(vote) returns a JavaScript code that will be evaluated:

      def vote(): ... return "jQuery('#votes').html('%s');jQuery('.flash'). html('%s').slideDown();" % (votes, message)

  5. In particular, it will alter the content of the following code, and flash a new message (slidedown):

    <span id="votes">{{=news.votes}}</span>

    Here is how this page looks:

Building a Facebook clone

At its fundamental level, Facebook handles friendship relations between users, and allows friends to see each other's posts. Users can register, log in, search for other users, request friendship, and accept friendship. When a user posts a message, the post will be visible on the wall (web page) of all his/her friends.

Of course, the real Facebook application is quite complex, and our version is greatly simplified, but it captures the most important features. In particular, we will omit the ability to attach comments after posts, and we will omit e-mail notification features. We will also omit the code to handle photos, videos, and chat. We are only interested in the friendship relations and display wall posts, based on friendship. We will call our application friends.

How to do it...

The core of our design is a table to link two people: a source and a target of a friendship relation. The friendship relation is requested by a source, and must be approved by a target. When approved, the source user can see the posts and profile info of the target. While the real Facebook friendship relations are bi-directional (although friends can be hidden/blocked), in our case we assume unidirectional friendship (two users must give friendship to each other to see each other's posts).

  1. The model is therefore quite simple, and we only need two tables:

    # in file: models: # a table to store posted messages db.define_table('post', Field('body', 'text', requires=IS_NOT_EMPTY(), label='What is on your mind?'), Field('posted_on', 'datetime', readable=False, writable=False), Field('posted_by', 'reference auth_user', readable=False, writable=False)) # a table to link two people db.define_table('link', Field('source', 'reference auth_user'), Field('target', 'reference auth_user'), Field('accepted', 'boolean', default=False)) # and define some global variables that will make code more compact User, Link, Post = db.auth_user, db.link, db.post me, a0, a1 = auth.user_id, request.args(0), request.args(1) myfriends = db(Link.source==me)(Link.accepted==True) alphabetical = User.first_name|User.last_name def name_of(user): return '%(first_name)s %(last_name)s' % user

    The last five lines define various shortcuts that will make our controllers and views more compact. For example, they allow the user to use User instead of db.user, and orderby=alphabetical instead of the more verbose equivalent.

    myfriends is the set of people that have accepted our friendship, which means we can see their posts.

    The following list line allows us to print the first name followed by last name of a user, given a user object or a user reference:

    {{=name_of(user)}}

  2. We are going to need the following pages:

    • An index page that, if we are logged in, redirects to our home page

    • A private home page that shows our messages, the posts of our friends, and allows us to post a new post

    • A page to search for new friends by name

    • A page to check who our current friends are, check pending friend requests, and approve or deny friendship

    • A wall page to see the status of one particular friend (or our own)

  3. We also need a callback action to implement to allow users to request friendship, to accept friendship, to deny a friendship request, and to cancel a previous request for friendship. We implement these through a single Ajax callback in a function called friendship:

    # in file: controllers/default.py def index(): if auth.user: redirect(URL('home')) return locals() def user(): return dict(form=auth()) def download(): return response.download(request, db) def call(): session.forget() return service() # our home page, will show our posts and posts by friends @auth.requires_login() def home(): Post.posted_by.default = me Post.posted_on.default = request.now crud.settings.formstyle = 'table2cols' form = crud.create(Post) friends = [me]+[row.target for row in myfriends.select(Link.target)] posts = db(Post.posted_by.belongs(friends))\ .select(orderby=~Post.posted_on, limitby=(0, 100)) return locals() # our wall will show our profile and our own posts @auth.requires_login() def wall(): user = User(a0 or me) if not user or not (user.id==me or \ myfriends(Link.target==user.id).count()): redirect(URL('home')) posts = db(Post.posted_by==user.id)\ .select(orderby=~Post.posted_on, limitby=(0, 100)) return locals() # a page for searching friends and requesting friendship @auth.requires_login() def search(): form = SQLFORM.factory(Field('name', requires=IS_NOT_EMPTY())) if form.accepts(request): tokens = form.vars.name.split() query = reduce(lambda a,b:a&b, [User.first_name.contains(k)|User.last_name.contains(k) \ for k in tokens]) people = db(query).select(orderby=alphabetical) else: people = [] return locals() # a page for accepting and denying friendship requests @auth.requires_login() def friends(): friends = db(User.id==Link.source)(Link.target==me)\ .select(orderby=alphabetical) requests = db(User.id==Link.target)(Link.source==me)\ .select(orderby=alphabetical) return locals() # this is the Ajax callback @auth.requires_login() def friendship(): """Ajax callback!""" if request.env.request_method != 'POST': raise HTTP(400) if a0=='request' and not Link(source=a1, target=me): # insert a new friendship request Link.insert(source=me, target=a1) elif a0=='accept': # accept an existing friendship request db(Link.target==me)(Link.source==a1).update(accepted=True) if not db(Link.source==me)(Link.target==a1).count(): Link.insert(source=me, target=a1) elif a0=='deny': # deny an existing friendship request db(Link.target==me)(Link.source==a1).delete() elif a0=='remove': # delete a previous friendship request db(Link.source==me)(Link.target==a1).delete()

  4. We also include the home, wall, friends, and search pages in the menu:

    # in file: models/menu.py response.menu = [ (T('Home'), False, URL('default', 'home')), (T('Wall'), False, URL('default', 'wall')), (T('Friends'), False, URL('default', 'friends')), (T('Search'), False, URL('default', 'search')), ]

    Most of the views are straightforward.

    • Here is views/default/home.html:

      {{extend 'layout.html'}} {{=form}} <script>jQuery('textarea').css('width','600px'). css('height','50px');</script> {{for post in posts:}} <div style="background: #f0f0f0; margin-bottom: 5px; padding: 8px;"> <h3>{{=name_of(post.posted_by)}} on {{=post.posted_on}}:</h3> {{=MARKMIN(post.body)}} </div> {{pass}}

      Notice the jQuery script that resizes the input message box, and the use of MARKMIN for rendering message markup.

    • Here is views/default/wall.html, which is very similar to the previous view (the difference is that there is no form, and the posts are relative to a single user, specified by request.args(0)):

      {{extend 'layout.html'}} <h2>Profile</h2> {{=crud.read(db.auth_user, user)}} <h2>Messages</h2> {{for post in posts:}} <div style="background: #f0f0f0; margin-bottom: 5px; padding: 8px;"> <h3>{{=name_of(post.posted_by)}} on {{=post.posted_on}}:</h3> {{=MARKMIN(post.body)}} </div> {{pass}}

      Here is what this page looks like:

    • Here is views/default/search.html:

      {{extend 'layout.html'}} <h2>Search for friends</h2> {{=form}} {{if people:}} <h3>Results</h3> <table> {{for user in people:}} <td> {{=A(name_of(user), _href=URL('wall', args=user.id))}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('request', user.id))}}', [], null); jQuery(this).parent().html('pending')"> request friendship </button> </td> {{pass}} </table> {{pass}}

      Here is what this page looks like:

      Notice how the buttons perform Ajax calls to request friendship to user.id. Upon click, the button is replaced by a message that says pending.

    • Below is views/default/friends.html. It lists current friends and pending friendship requests:

      {{extend 'layout.html'}} <h2>Friendship Offered</h2> <table> {{for friend in friends:}} <tr> <td> {{=A(name_of(friend.auth_user), _href=URL('wall', args=friend.auth_user.id))}} </td> <td> {{if friend.link.accepted:}}accepted{{else:}} <button onclick="ajax( '{{=URL('friendship', args=('accept', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('accepted')"> accept </button> {{pass}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('deny', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('denied')"> deny </button> </td> </tr> {{pass}} </table> <h2>Friendship Requested</h2> <table> {{for friend in requests:}} <tr> <td> {{=A(name_of(friend.auth_user), _href=URL('wall', args=friend.auth_user.id))}} </td> <td> {{if friend.link.accepted:}}accepted{{else:}} pending{{pass}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('deny', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('removed')"> remove </button> </td> </tr> {{pass}} </table>

      Here is what this page looks like:

      This view displays two tables: a list of friendships offered to us (by accepting, we give them the permission to see our profile and posts), and friendship requests that we sent (people we want to see profile and posts of). For each user in the first table, there are two buttons. A button that performs an Ajax call to accept a pending friendship request, and a button to deny friendship. For each user in the second table, there is a column that informs us of whether our request was accepted, and a column with a button to cancel the friendship relation (whether pending or established).

      Notice how {{=name_of(user)}} and {{=name_of(message.posted_by)}} require a database lookup. Our application can be sped up by caching the output of this function.

Summary

This article explains how we can:

  • Improving the scaffolding application

  • Building a simple contacts application

  • Building a Reddit clone

  • Building a Facebook clone

Resources for Article :


Further resources on this subject:


web2py Application Development Cookbook Over 110 recipes to master this full-stack Python web framework with this book and ebook
Published: March 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

About the Author :


Mariano Reingart

Mariano Reingart lives in Buenos Aires (Argentina), and is a specialist in database administration, and development of software applications and libraries (web services, PDF, replication, and so on), with more than 10 years of experience. Currently, he is the PostgreSQL regional contact for Argentina and a web2py contributor, with more than 14 open source projects, including interface for Free Electronic Invoice web services (PyAfipWs) and Pythonic Replication for PostgreSQL (PyReplica).

Mariano has a bachelor's degree in Computer Systems Analysis from the University of Morón, and currently works on his own funded startup formed by an open group of independent professionals, which is dedicated to software development, training, and technical support, focusing on open source tools (GNU/Linux, Python, PostgreSQL and web2py).

Mariano has worked for local Python companies in large business applications (ERP, SCM, and CRM) and mission critical systems (election counting, electronic voting, and 911 emergency events support). He has contributed to the book web2py Enterprise Web Framework 3rd Edition, and for several Spanish translation efforts of the PostgreSQL official documentation. You can find his resume at: http://reingart.blogspot.com/p/resume.html.

Massimo Di Pierro

Massimo Di Pierro is an associate professor at the School of Computing of DePaul University in Chicago, where he directs the Master's program in Computational Finance. He also teaches courses on various topics, including web frameworks, network programming, computer security, scientific computing, and parallel programming.

Massimo has a PhD in High Energy Theoretical Physics from the University of Southampton (UK), and he has previously worked as an associate researcher for Fermi National Accelerator Laboratory.

Massimo is the author of a book on web2py, and more than 50 publications in the fields of Physics and Computational Finance, and he has contributed to many open source projects. He started the web2py project in 2007, and is currently the lead developer.

Books From Packt


Python 2.6 Text Processing Beginners Guide
Python 2.6 Text Processing Beginners Guide

Python Text Processing with NLTK 2.0   Cookbook
Python Text Processing with NLTK 2.0 Cookbook

Python Geo-Spatial Development
Python Geo-Spatial Development

Python 3 Object Oriented Programming
Python 3 Object Oriented Programming

Python 2.6 Graphics Cookbook
Python 2.6 Graphics Cookbook

Spring Python 1.1
Spring Python 1.1

Python Multimedia
Python Multimedia

MySQL for Python
MySQL for Python


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
b
A
T
S
h
H
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software