{"id":855,"date":"2013-06-30T23:54:07","date_gmt":"2013-06-30T22:54:07","guid":{"rendered":"http:\/\/fragile.org.uk\/?p=855"},"modified":"2013-06-30T23:54:07","modified_gmt":"2013-06-30T22:54:07","slug":"twitter-clone-tutorial-in-web2py-part-3-the-wall-and-search-pages","status":"publish","type":"post","link":"https:\/\/fragile.org.uk\/index.php\/2013\/06\/30\/twitter-clone-tutorial-in-web2py-part-3-the-wall-and-search-pages\/","title":{"rendered":"Twitter Clone Tutorial in Web2py Part 3 &#8211; The Wall and Search Pages"},"content":{"rendered":"<p dir=\"ltr\">Welcome to my getting started with Web2py tutorial.<\/p>\n<p dir=\"ltr\">This tutorial assumes very little on the part of the reader, some knowledge of a programming language will certainly help, but if not don\u2019t worry. I\u2019ll take you from installation through to v1 of your application.<\/p>\n<p dir=\"ltr\">If you don\u2019t want to copy from the tutorial, the full source is hosted on <a title=\"Witter on Github\" href=\"https:\/\/github.com\/neilisfragile\/witter\" target=\"_blank\">github<\/a>.<\/p>\n<p dir=\"ltr\">Last time we created the Models as well as the Controller and View for the home page. The next page to look at is the Wall.\u00a0The Wall is specific to a given user and contains their profile details as well as their weet history.<\/p>\n<p dir=\"ltr\"><a href=\"http:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/wall_reduced.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-902\" alt=\"Wall_reduced\" src=\"http:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/wall_reduced.png\" width=\"600\" height=\"351\" srcset=\"https:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/wall_reduced.png 600w, https:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/wall_reduced-300x176.png 300w\" sizes=\"auto, (max-width: 600px) 100vw, 600px\" \/><\/a><\/p>\n<p>As before we start with in the default.py Controller, add the following method to manage the behaviour for the wall.<\/p>\n<pre># show user's wall, showing profile and posts\ndef wall():\n\u00a0\u00a0\u00a0#Determine which user's wall is to be displayed\n\u00a0\u00a0\u00a0user = db.auth_user(request.args(0) or me)\n\u00a0\u00a0\u00a0#If user is invalid, return to the home page\n\u00a0\u00a0\u00a0if not user:\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0redirect(URL('home'))\n\u00a0\u00a0\u00a0weets = db(db.weets.posted_by==user.id).select(orderby=~db.weets.posted_on,limitby=(0,100))\n\u00a0\u00a0\u00a0return locals()<\/pre>\n<p dir=\"ltr\">The first thing to do is determine whose wall it is that we need to display. The line responsible is worth a closer look.<\/p>\n<pre>user = db.auth_user(request.args(0) or me)<\/pre>\n<p dir=\"ltr\">We have already seen db used as a means to access the database but we have not yet come across \u2018request\u2019. Request is an object available to all Controller actions and provide details of the http request that instigated the Controller action being called.<\/p>\n<p dir=\"ltr\">In this case we check the first (technically the zeroth) argument of the request. An argument is best described by example<\/p>\n<pre>http:\/\/127.0.0.1:8000\/witter\/default\/wall\/myargument\/myotherargument\/<\/pre>\n<p dir=\"ltr\">In this specific instance the argument denotes the user id and so the line in question checks if a user id has been specified, if not it assumes that the requester are themselves logged in and trying to access their own wall. If neither case is true then the browser is redirected to the home page.<\/p>\n<p dir=\"ltr\">After that we pull out all the weets for the given user and return local variables, which are then made available to the View.<\/p>\n<p dir=\"ltr\">We will need a new View file called default\/wall.html. As before you can create it via the application admin page.<\/p>\n<p dir=\"ltr\">Replace the default code with :-<\/p>\n<pre>{{extend 'layout.html'}}\n&lt;h2&gt;Profile&lt;\/h2&gt;\n{{=crud.read(db.auth_user,user)}}\n&lt;h2&gt;Messages&lt;\/h2&gt;\n{{for weet in weets:}}\n<div style=\"background:#f0f0f0;margin-bottom:5px;padding:8px;\">\n<h3>{{=name_of(weet.posted_by)}} on {{=weet.posted_on}}:<\/h3>\n{{=MARKMIN(weet.body)}}\n<\/div>\n{{pass}}<\/pre>\n<p dir=\"ltr\">We\u2019ve met crud before, it provides a nice high level abstraction to access database information, in this instance we are using it to pull the profile information about the user. The next section loops through all the weets generated in the Controller and presents them in chronological order &#8211; this very much the same logic as we saw on the home page. You can a look for yourself at http:\/\/127.0.0.1:8000\/witter\/default\/wall or just navigate there from the Witter home page using the navigation bar at the top.<\/p>\n<h2>Search<\/h2>\n<p dir=\"ltr\">For the grand finale let&#8217;s look at search.<\/p>\n<p dir=\"ltr\">The purpose of this page is to allow a user to search for their friends and then follow them. In some senses it is the simplest of the pages, but as we shall see it contains some interesting functionality behind the scenes.<\/p>\n<p dir=\"ltr\">Firstly, let\u2019s take a look at the Controller, add the following to default.py<\/p>\n<pre># a page for searching for other users\n@auth.requires_login()\ndef search():\n\u00a0\u00a0\u00a0form = SQLFORM.factory(Field('name',requires=IS_NOT_EMPTY()))\n\u00a0\u00a0\u00a0if form.accepts(request):\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0tokens = form.vars.name.split()\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0query = reduce(lambda a,b:a&amp;b,\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0[db.auth_user.first_name.contains(k)|db.auth_user.last_name.contains(k) \n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0for k in tokens])\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0people = db(query).select(orderby=db.auth_user.first_name|db.auth_user.last_name,left=db.followers.on(db.followers.followee==db.auth_user.id))\n\u00a0\u00a0\u00a0else:\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0people = []\n\u00a0\u00a0\u00a0return locals()<\/pre>\n<p dir=\"ltr\">The first thing we do is create the search form, we do this like so<\/p>\n<pre>SQLFORM.factory(Field('name',requires=IS_NOT_EMPTY()))<\/pre>\n<p dir=\"ltr\">The SQLFORM.factory can be used to generate a from an existing database table, or, as in this case can be used to generate a form from scratch. The form is very simple and takes a single input \u2018name\u2019.<\/p>\n<p dir=\"ltr\">The rest of the Controller handles the action once the input has been received \u00a0&#8211; this is what the follow is checking for:-<\/p>\n<pre>if form.accepts(request):<\/pre>\n<p dir=\"ltr\">The remaining code searches for the the search terms amongst other users registered in witter and plugs them into \u2018people\u2019, it also determines which of those people the user already follows &#8211; the View is going to rely on this follower information.<\/p>\n<h2>The Search View<\/h2>\n<p dir=\"ltr\">So far so good, now let\u2019s take a look at the View, you&#8217;ll need to create a file called default\/search.html under the Views section. Replace the default code with:-<\/p>\n<pre>{{extend 'layout.html'}}\n&lt;h2&gt;Search for people to follow&lt;\/h2&gt;\n{{=form}}\n\n{{if people:}}\n&lt;h3&gt;Results&lt;\/h3&gt;\n\n{{for user in people:}}\n<div class=\"row\">\n<div class=\"span3 offset1\">{{=A(name_of(user.auth_user), _href=URL('wall',args=user.auth_user.id))}}<\/div>\n<div class=\"span1\">\n{{if user.followers.followee:}}\nUnfollow\n{{else:}}\nFollow\n{{pass}}\n<\/div>\n&lt;\/div&gt;\n{{pass}}\n{{pass}}<\/pre>\n<p dir=\"ltr\">Much of this will now be familiar &#8211; we pull in the standard template, then the form and then iterate over the \u2018people\u2019 (if any) returned by the Controller. Where it get\u2019s interesting is the button definitions.<\/p>\n<p dir=\"ltr\">Firstly we check if the searched for user is already being followed by the search, this means the button can offer to follow unfollowed witterers and unfollow the followed. So far so good, but what does this do?<\/p>\n<pre>&lt;button onclick=\"ajax('{{=URL('follow',args=('follow',user.auth_user.id))}}',[],null);$(this).parent().html('Followed')\"&gt;Follow&lt;\/button&gt;<\/pre>\n<p dir=\"ltr\">Before I can answer that I need to explain about AJAX. AJAX stands for Asynchronous JavaScript and XML, but this name is largely misleading since it needn\u2019t be asynchronous and needn\u2019t use XML. In short AJAX is a technique for creating dynamic web pages, it allows pages to be updated without needing to reload the entire page, and it does this by exchanging small amounts of information with the server behind the scenes.<\/p>\n<p dir=\"ltr\">Strictly speaking I could have passed on using AJAX in this tutorial, but it is a really useful technique a very simple to implement.<\/p>\n<p dir=\"ltr\">In layman\u2019s terms the button definition says, when the button is clicked make a poke to another Controller action called \u2018follow\u2019 and send the arguments \u2018follow\u2019 and the user id. Once this is done replace the button (which read \u2018Follow\u2019) to be some text that read \u2018Followed\u2019.<\/p>\n<p dir=\"ltr\">A final note about the search View is the use of the css classes &#8216;row&#8217; and &#8216;span&#8217;. These are provided by <a title=\"Bootstrap\" href=\"http:\/\/twitter.github.io\/bootstrap\/\">Bootstrap<\/a> which is a a popular front end frame work. Bootstrap is not part of Web2py but Web2py does support it out of the box. In this tutorial I&#8217;ve deliberately avoided focusing on the look and feel of the app, but Bootstrap provides all the tools necessary to create an attractive web application.<\/p>\n<h2>AJAX Callback<\/h2>\n<p dir=\"ltr\">And that&#8217;s it for the , however, what about the follow action in the Controller? We\u2019ve not implemented that yet. Here goes, add the following to the default.py Controller:-<\/p>\n<pre># this is the Ajax callback\n@auth.requires_login()\ndef follow():\n\u00a0\u00a0\u00a0if request.env.request_method!='POST': raise HTTP(400)\n\u00a0\u00a0\u00a0if request.args(0) =='follow' and not db.followers(follower=me,followee=request.args(1)):\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0# insert a new friendship request\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0db.followers.insert(follower=me,followee=request.args(1))\n\u00a0\u00a0\u00a0elif request.args(0)=='unfollow':\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0# delete a previous friendship request\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0db(db.followers.follower==me)(db.followers.followee==request.args(1)).delete()<\/pre>\n<p dir=\"ltr\">The most interesting line is the first one.<\/p>\n<pre>if request.env.request_method!='POST': raise HTTP(400)<\/pre>\n<p dir=\"ltr\">When a browser makes a request to read a web page it uses an HTTP method called GET, this is exactly what you do everytime you point your browser at Google. There are however other HTTP methods, POST being one of them, POST is used to transmit that is expected to change the state of the application. The AJAX call that we initiate in the search,html View is a POST request.<\/p>\n<p dir=\"ltr\">Since this Controller action is there solely to support AJAX calls from search.html, it does not expect to handle HTTP GET requests and rejects them. You can try this for yourself by trying to View\u00a0<a href=\"http:\/\/127.0.0.1:8000\/witter\/default\/follow\">http:\/\/127.0.0.1:8000\/witter\/default\/follow<\/a>\u00a0which should result in a blank page and a error message complaining of a \u2018bad request\u2019.<\/p>\n<p dir=\"ltr\"><a href=\"http:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/search_reduced.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-967\" alt=\"Search\" src=\"http:\/\/fragile.org.uk\/wp-content\/uploads\/2013\/06\/search_reduced.png\" width=\"600\" height=\"351\" \/><\/a><\/p>\n<p dir=\"ltr\">Right so there we have it &#8211; all pages are complete, in order to try out the search and folllow\/unfollow functionality you\u2019ll need to register to extra users and have them follow each other.\u00a0Be sure to notice that it is not necessary to refresh the search page every time to make a follow\/unfollow request.<\/p>\n<p dir=\"ltr\">So in conclusion what have we achieved?<\/p>\n<p dir=\"ltr\">We created a new web2py application called Witter, created three pages (and an AJAX method) supporting a home page, a user wall and a user search and all backed by a database that will persist the data over time.<\/p>\n<h2>Are we done?<\/h2>\n<p dir=\"ltr\">Well perhaps, I certainly am.<\/p>\n<p dir=\"ltr\">This tutorial has taken you from nothing at all to a functioning web2py application, that can be extended as you please.<\/p>\n<p dir=\"ltr\">If you are searching for ideas, you might want to move the search functionality into the home page and extend it so that search covers all weets (much like Twitter does). The UI could certainly do with a spruce up (and there are plenty of CSS tutorials out there) and as a final task you could put the thing live, gain awesome traction (due to your custom tweaks) and take over the social media world.<\/p>\n<p dir=\"ltr\">Feedback on this tutorial much appreciated, I&#8217;ll leave you with some useful links.<\/p>\n<ul>\n<li><a title=\"Web2py\" href=\"http:\/\/web2py.com\">web2py home page<\/a><\/li>\n<li><a title=\"Web2py manual\" href=\"http:\/\/www.web2py.com\/book\">web2py online book<\/a> &#8211; the most comprehensive web2py resource online<\/li>\n<li><a title=\"Web2Py\" href=\"https:\/\/groups.google.com\/forum\/#!forum\/web2py\">web2py\u00a0mailing list<\/a>,<\/li>\n<li><a title=\"Web2py Examples\" href=\"http:\/\/www.web2py.com\/init\/default\/examples\">web2py examples<\/a><\/li>\n<li>The Witter source on <a title=\"Witter on Github\" href=\"https:\/\/github.com\/neilisfragile\/witter\">Github<\/a><\/li>\n<li><a title=\"Learn Python the Hard Way\" href=\"http:\/\/learnpythonthehardway.org\/\">Learn Python the Hardway<\/a><\/li>\n<li><a title=\"Bootstrap\" href=\"http:\/\/twitter.github.io\/bootstrap\/\">Bootstrap<\/a> &#8211; Everything you need to get the site look great<\/li>\n<li><a title=\"Selenium Python\" href=\"https:\/\/selenium-python.readthedocs.org\/en\/latest\/\">Selenium<\/a> &#8211; Functional testing for your application<\/li>\n<li><a title=\"Web Development Check List\" href=\"http:\/\/webdevchecklist.com\/\">Web Development Check List<\/a> http:\/\/webdevchecklist.com\/<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<a href=\"https:\/\/fragile.org.uk\/index.php\/2013\/06\/30\/twitter-clone-tutorial-in-web2py-part-3-the-wall-and-search-pages\/\" rel=\"bookmark\" title=\"Permalink to Twitter Clone Tutorial in Web2py Part 3 &#8211; The Wall and Search Pages\"><p>Welcome to my getting started with Web2py tutorial. This tutorial assumes very little on the part of the reader, some knowledge of a programming language will certainly help, but if not don\u2019t worry. I\u2019ll take you from installation through to v1 of your application. If you don\u2019t want to copy from the tutorial, the full [&hellip;]<\/p>\n<\/a>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[32,52],"class_list":{"0":"post-855","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-development","7":"tag-python","8":"tag-web2","9":"h-entry","10":"hentry"},"_links":{"self":[{"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/posts\/855","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/comments?post=855"}],"version-history":[{"count":0,"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/posts\/855\/revisions"}],"wp:attachment":[{"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/media?parent=855"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/categories?post=855"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fragile.org.uk\/index.php\/wp-json\/wp\/v2\/tags?post=855"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}